🐛 ИСПРАВЛЕНИЯ: • chat_monitor.py: message.system → hasattr проверка • База данных: добавлена колонка achievement_data • Удалена кнопка /settings из всех шаблонов • Настройки перенесены в /broadcast 📁 ФАЙЛЫ: • handlers/chat_monitor.py • web/templates/*.html (удалена /settings) • database: ALTER TABLE achievements Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
251 lines
14 KiB
HTML
251 lines
14 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<title>Рассылки - Домовой Бот</title>
|
||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css" rel="stylesheet">
|
||
<style>
|
||
.sidebar { min-height: 100vh; background: linear-gradient(180deg, #2c3e50 0%, #1a252f 100%); }
|
||
.sidebar .nav-link { color: rgba(255,255,255,0.8); padding: 1rem; }
|
||
.sidebar .nav-link:hover, .sidebar .nav-link.active { color: white; background-color: rgba(255,255,255,0.1); }
|
||
.content { padding: 2rem; }
|
||
#preview { max-width: 400px; margin-top: 1rem; display: none; }
|
||
#preview img { max-width: 100%; border-radius: 8px; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container-fluid">
|
||
<div class="row">
|
||
<div class="col-md-2 sidebar p-0">
|
||
<div class="p-3 text-white text-center">
|
||
<h4><i class="bi bi-house-door"></i> Домовой Бот</h4>
|
||
</div>
|
||
<nav class="nav flex-column">
|
||
<a class="nav-link" href="/"><i class="bi bi-speedometer2"></i> Дашборд</a>
|
||
<a class="nav-link" href="/users"><i class="bi bi-people"></i> Пользователи</a>
|
||
<a class="nav-link" href="/verification"><i class="bi bi-shield-check"></i> Верификация</a>
|
||
<a class="nav-link" href="/phones"><i class="bi bi-telephone"></i> Телефоны</a>
|
||
<a class="nav-link active" href="/broadcast"><i class="bi bi-broadcast"></i> Рассылки</a>
|
||
<a class="nav-link" href="/polls"><i class="bi bi-bar-chart"></i> Опросы</a>
|
||
<a class="nav-link" href="/events"><i class="bi bi-calendar-event"></i> События</a>
|
||
<a class="nav-link" href="/schedules"><i class="bi bi-clock"></i> Расписания</a>
|
||
<a class="nav-link" href="/export"><i class="bi bi-download"></i> Экспорт</a>
|
||
</nav>
|
||
</div>
|
||
<div class="col-md-10 content">
|
||
<h2 class="mb-4"><i class="bi bi-broadcast"></i> Рассылка объявлений</h2>
|
||
|
||
<div class="card">
|
||
<div class="card-header bg-primary text-white">
|
||
<h5 class="mb-0"><i class="bi bi-send"></i> Создать рассылку</h5>
|
||
</div>
|
||
<div class="card-body">
|
||
<form id="broadcastForm" enctype="multipart/form-data">
|
||
<div class="mb-3">
|
||
<label class="form-label">Текст объявления</label>
|
||
<textarea class="form-control" id="broadcastText" rows="5"
|
||
placeholder="Введите текст объявления..." required></textarea>
|
||
<small class="text-muted">Поддерживается HTML: <b>жирный</b>, <i>курсив</i></small>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label class="form-label">Картинка (необязательно)</label>
|
||
<input type="file" class="form-control" id="broadcastPhoto" accept="image/*"
|
||
onchange="previewImage()">
|
||
<small class="text-muted">JPG, PNG до 5MB</small>
|
||
<div id="preview">
|
||
<img id="previewImg" alt="Предпросмотр">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label class="form-label">Кому отправить:</label>
|
||
<select class="form-select" id="broadcastRecipients">
|
||
<option value="all_verified">✅ Всем верифицированным</option>
|
||
<option value="ig_only">🔵 Только Инициативной Группе</option>
|
||
<option value="chat_only">📢 Только в общий чат</option>
|
||
<option value="all_and_chat">✅ Всем + в чат (по умолчанию)</option>
|
||
</select>
|
||
<small class="text-muted">Выберите получателей рассылки</small>
|
||
</div>
|
||
|
||
<div class="alert alert-info">
|
||
<i class="bi bi-info-circle"></i>
|
||
<b>Получатели:</b> <span id="recipientsInfo">Все верифицированные пользователи + общий чат</span>
|
||
</div>
|
||
|
||
<button type="submit" class="btn btn-primary btn-lg">
|
||
<i class="bi bi-send"></i> Отправить рассылку
|
||
</button>
|
||
</form>
|
||
|
||
<div id="result" class="mt-4"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card mt-4">
|
||
<div class="card-body">
|
||
<h5>Как использовать:</h5>
|
||
<ol>
|
||
<li>Введите текст объявления</li>
|
||
<li>Прикрепите картинку (необязательно)</li>
|
||
<li>Выберите получателей</li>
|
||
<li>Нажмите "Отправить рассылку"</li>
|
||
<li>Бот отправит выбранным получателям</li>
|
||
</ol>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- IG Broadcast Limit -->
|
||
<div class="card mt-4">
|
||
<div class="card-header bg-info text-white">
|
||
<h5 class="mb-0"><i class="bi bi-people"></i> Настройка рассылки ИГ</h5>
|
||
</div>
|
||
<div class="card-body">
|
||
<form id="igLimitForm">
|
||
<div class="mb-3">
|
||
<label class="form-label">Минимальный интервал между рассылками ИГ (секунды)</label>
|
||
<input type="number" class="form-control" id="igBroadcastLimit" value="0" min="0" step="60">
|
||
<small class="text-muted">
|
||
<ul class="mb-0">
|
||
<li><code>0</code> — Без ограничений</li>
|
||
<li><code>3600</code> — 1 раз в час</li>
|
||
<li><code>86400</code> — 1 раз в сутки</li>
|
||
</ul>
|
||
</small>
|
||
</div>
|
||
<button type="submit" class="btn btn-primary">
|
||
<i class="bi bi-save"></i> Сохранить настройку
|
||
</button>
|
||
</form>
|
||
<div id="igLimitResult" class="mt-3"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||
<script>
|
||
// Загрузка настройки лимита ИГ
|
||
async function loadIgLimit() {
|
||
try {
|
||
const r = await fetch('/api/settings/ig_broadcast_limit');
|
||
const result = await r.json();
|
||
if (result.success) {
|
||
document.getElementById('igBroadcastLimit').value = parseInt(result.value) || 0;
|
||
}
|
||
} catch (err) {
|
||
console.error('Ошибка загрузки настройки:', err);
|
||
}
|
||
}
|
||
|
||
// Сохранение настройки лимита ИГ
|
||
document.getElementById('igLimitForm').addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
const limit = parseInt(document.getElementById('igBroadcastLimit').value);
|
||
const resultDiv = document.getElementById('igLimitResult');
|
||
|
||
resultDiv.innerHTML = '<div class="alert alert-info">💾 Сохранение...</div>';
|
||
|
||
try {
|
||
const r = await fetch('/api/settings/ig_broadcast_limit', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ limit: limit })
|
||
});
|
||
const result = await r.json();
|
||
|
||
if (result.success) {
|
||
resultDiv.innerHTML = '<div class="alert alert-success">✅ Настройка сохранена!</div>';
|
||
} else {
|
||
resultDiv.innerHTML = '<div class="alert alert-danger">❌ Ошибка: ' + result.error + '</div>';
|
||
}
|
||
} catch (err) {
|
||
resultDiv.innerHTML = '<div class="alert alert-danger">❌ Ошибка: ' + err.message + '</div>';
|
||
}
|
||
});
|
||
|
||
// Загружаем настройку при загрузке страницы
|
||
document.addEventListener('DOMContentLoaded', loadIgLimit);
|
||
|
||
// Обновление информации о получателях
|
||
document.getElementById('broadcastRecipients').addEventListener('change', function() {
|
||
const info = {
|
||
'all_verified': 'Все верифицированные пользователи (в личку)',
|
||
'ig_only': 'Только участники Инициативной Группы (в личку)',
|
||
'chat_only': 'Только в общий чат (одно сообщение)',
|
||
'all_and_chat': 'Все верифицированные (в личку) + общий чат'
|
||
};
|
||
document.getElementById('recipientsInfo').textContent = info[this.value] || '';
|
||
});
|
||
|
||
function previewImage() {
|
||
const file = document.getElementById('broadcastPhoto').files[0];
|
||
const preview = document.getElementById('preview');
|
||
const previewImg = document.getElementById('previewImg');
|
||
|
||
if (file) {
|
||
const reader = new FileReader();
|
||
reader.onload = function(e) {
|
||
previewImg.src = e.target.result;
|
||
preview.style.display = 'block';
|
||
}
|
||
reader.readAsDataURL(file);
|
||
} else {
|
||
preview.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
document.getElementById('broadcastForm').addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
|
||
const text = document.getElementById('broadcastText').value;
|
||
const photo = document.getElementById('broadcastPhoto').files[0];
|
||
const recipients = document.getElementById('broadcastRecipients').value;
|
||
const resultDiv = document.getElementById('result');
|
||
|
||
if (!text.trim()) {
|
||
alert('Введите текст объявления!');
|
||
return;
|
||
}
|
||
|
||
const recipientNames = {
|
||
'all_verified': 'всем верифицированным',
|
||
'ig_only': 'Инициативной Группе',
|
||
'chat_only': 'в общий чат',
|
||
'all_and_chat': 'всем верифицированным + в чат'
|
||
};
|
||
|
||
if (!confirm(`Отправить рассылку ${recipientNames[recipients] || 'всем'}?`)) return;
|
||
|
||
resultDiv.innerHTML = '<div class="alert alert-info">📤 Отправка рассылки...</div>';
|
||
|
||
const formData = new FormData();
|
||
formData.append('text', text);
|
||
formData.append('recipients', recipients);
|
||
if (photo) {
|
||
formData.append('photo', photo);
|
||
}
|
||
|
||
try {
|
||
const r = await fetch('/api/broadcast/send', {
|
||
method: 'POST',
|
||
body: formData
|
||
});
|
||
const result = await r.json();
|
||
|
||
if (result.success) {
|
||
resultDiv.innerHTML = `<div class="alert alert-success">✅ ${result.message}</div>`;
|
||
document.getElementById('broadcastForm').reset();
|
||
document.getElementById('preview').style.display = 'none';
|
||
} else {
|
||
resultDiv.innerHTML = `<div class="alert alert-danger">❌ Ошибка: ${result.error || 'Неизвестная'}</div>`;
|
||
}
|
||
} catch (err) {
|
||
resultDiv.innerHTML = `<div class="alert alert-danger">❌ Ошибка: ${err.message}</div>`;
|
||
}
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|