БЫЛО: Каждая из 14 страниц имела своё собственное вшитое меню
с разным набором кнопок (от 4 до 10 пунктов)
СТАЛО: Все 14 страниц наследуют base.html с единым меню из 14 пунктов
в 4 разделах: Пользователи / Коммуникации / Контент / Система
Убрано 1949 строк дублирующегося кода, добавлено 1323 строки чистого контента.
Экономия: -626 строк (32% сокращение).
Переписанные страницы:
- dashboard.html, users.html, verification.html
- phones.html, polls.html, events.html
- schedules.html, ads.html, scheduled_posts.html, export.html
Результат: На ЛЮБОЙ странице пользователь видит ВСЕ 14 пунктов меню.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
269 lines
12 KiB
HTML
269 lines
12 KiB
HTML
{% extends "base.html" %}
|
||
|
||
{% block title %}Запланированные посты - Домовой Бот{% endblock %}
|
||
|
||
{% block nav_scheduled_posts %}active{% endblock %}
|
||
|
||
{% block extra_style %}
|
||
#preview { max-width: 400px; margin-top: 1rem; display: none; }
|
||
#preview img { max-width: 100%; border-radius: 8px; }
|
||
.post-card { transition: transform 0.2s; }
|
||
.post-card:hover { transform: translateY(-2px); box-shadow: 0 4px 8px rgba(0,0,0,0.1); }
|
||
{% endblock %}
|
||
|
||
{% block content %}
|
||
<h2 class="mb-4"><i class="bi bi-calendar2-plus"></i> Запланированные посты</h2>
|
||
|
||
<!-- Форма создания поста -->
|
||
<div class="card mb-4">
|
||
<div class="card-header bg-primary text-white">
|
||
<h5 class="mb-0"><i class="bi bi-plus-circle"></i> Создать новый пост</h5>
|
||
</div>
|
||
<div class="card-body">
|
||
<form id="scheduledPostForm" enctype="multipart/form-data">
|
||
<div class="mb-3">
|
||
<label class="form-label">Текст поста</label>
|
||
<textarea class="form-control" id="postText" rows="5"
|
||
placeholder="Введите текст поста..." required></textarea>
|
||
<small class="text-muted">Поддерживается HTML: <b>жирный</b>, <i>курсив</i>, <emoji></small>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label class="form-label">Картинка (необязательно)</label>
|
||
<input type="file" class="form-control" id="postPhoto" accept="image/*"
|
||
onchange="previewImage()">
|
||
<small class="text-muted">JPG, PNG до 5MB</small>
|
||
<div id="preview">
|
||
<img id="previewImg" alt="Предпросмотр">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="row">
|
||
<div class="col-md-6 mb-3">
|
||
<label class="form-label">Тема форума</label>
|
||
<select class="form-select" id="postTopic" required>
|
||
<option value="general">💬 Общий чат</option>
|
||
<option value="memes">😂 Мемы</option>
|
||
<option value="ads">📢 Важные объявления</option>
|
||
<option value="meetings">🏛️ Собрания собственников</option>
|
||
<option value="kgm">🗑️ Вывоз КГМ</option>
|
||
<option value="lostfound">🔍 Потеряшки</option>
|
||
<option value="auto">🚗 Для автовладельцев</option>
|
||
<option value="phones">📞 Важные телефоны</option>
|
||
<option value="links">🔗 Ссылки</option>
|
||
<option value="uo">📝 Переписка с УО</option>
|
||
</select>
|
||
<small class="text-muted">В какую тему отправить пост</small>
|
||
</div>
|
||
|
||
<div class="col-md-6 mb-3">
|
||
<label class="form-label">Получатели</label>
|
||
<select class="form-select" id="postRecipients">
|
||
<option value="chat_only">📢 Только в чат</option>
|
||
<option value="all_verified">✅ Всем верифицированным в личку</option>
|
||
<option value="all_and_chat">✅ Всем + в чат</option>
|
||
</select>
|
||
<small class="text-muted">Кому отправить пост</small>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="row">
|
||
<div class="col-md-6 mb-3">
|
||
<label class="form-label">Дата публикации</label>
|
||
<input type="date" class="form-control" id="postDate" required>
|
||
<small class="text-muted">🕐 По твоему времени (UTC+4)</small>
|
||
</div>
|
||
|
||
<div class="col-md-6 mb-3">
|
||
<label class="form-label">Время публикации</label>
|
||
<input type="time" class="form-control" id="postTime" required>
|
||
<small class="text-muted">🕐 По твоему времени (UTC+4)</small>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="alert alert-info">
|
||
<i class="bi bi-info-circle"></i>
|
||
<b>Часовой пояс:</b> Все времена указаны в твоём локальном времени (UTC+4, Екатеринбург/Ульяновск).
|
||
<br>Бот автоматически конвертирует время в UTC для публикации по расписанию.
|
||
</div>
|
||
|
||
<button type="submit" class="btn btn-success btn-lg">
|
||
<i class="bi bi-calendar-check"></i> Запланировать пост
|
||
</button>
|
||
</form>
|
||
|
||
<div id="result" class="mt-4"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Список запланированных постов -->
|
||
<div class="card">
|
||
<div class="card-header bg-info text-white">
|
||
<h5 class="mb-0"><i class="bi bi-list-check"></i> Запланированные посты</h5>
|
||
</div>
|
||
<div class="card-body">
|
||
<div id="postsList"></div>
|
||
<div id="noPosts" class="text-center text-muted py-5">
|
||
<i class="bi bi-calendar-x" style="font-size: 3rem;"></i>
|
||
<p class="mt-3">Пока нет запланированных постов</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endblock %}
|
||
|
||
{% block extra_script %}
|
||
<script>
|
||
// Установка минимальной даты (сегодня)
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
const today = new Date();
|
||
const todayStr = today.toISOString().split('T')[0];
|
||
document.getElementById('postDate').setAttribute('min', todayStr);
|
||
|
||
// Показываем текущее время в браузере пользователя
|
||
const tzOffset = -today.getTimezoneOffset() / 60;
|
||
const tzLabel = tzOffset >= 0 ? `UTC+${tzOffset}` : `UTC${tzOffset}`;
|
||
console.log(`🕐 Твой часовой пояс: ${tzLabel}`);
|
||
|
||
loadPosts();
|
||
});
|
||
|
||
function previewImage() {
|
||
const file = document.getElementById('postPhoto').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('scheduledPostForm').addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
|
||
const text = document.getElementById('postText').value;
|
||
const photo = document.getElementById('postPhoto').files[0];
|
||
const topic = document.getElementById('postTopic').value;
|
||
const recipients = document.getElementById('postRecipients').value;
|
||
const date = document.getElementById('postDate').value;
|
||
const time = document.getElementById('postTime').value;
|
||
const resultDiv = document.getElementById('result');
|
||
|
||
if (!text.trim()) {
|
||
alert('Введите текст поста!');
|
||
return;
|
||
}
|
||
|
||
if (!date || !time) {
|
||
alert('Выберите дату и время!');
|
||
return;
|
||
}
|
||
|
||
const scheduledTime = `${date}T${time}:00`;
|
||
|
||
const formData = new FormData();
|
||
formData.append('text', text);
|
||
formData.append('topic_name', topic);
|
||
formData.append('recipients', recipients);
|
||
formData.append('scheduled_time', scheduledTime);
|
||
if (photo) {
|
||
formData.append('photo', photo);
|
||
}
|
||
|
||
try {
|
||
const r = await fetch('/api/scheduled_posts/create', {
|
||
method: 'POST',
|
||
body: formData
|
||
});
|
||
const result = await r.json();
|
||
|
||
if (result.success) {
|
||
resultDiv.innerHTML = `<div class="alert alert-success">✅ ${result.message}</div>`;
|
||
document.getElementById('scheduledPostForm').reset();
|
||
document.getElementById('preview').style.display = 'none';
|
||
loadPosts();
|
||
} else {
|
||
resultDiv.innerHTML = `<div class="alert alert-danger">❌ Ошибка: ${result.error || 'Неизвестная'}</div>`;
|
||
}
|
||
} catch (err) {
|
||
resultDiv.innerHTML = `<div class="alert alert-danger">❌ Ошибка: ${err.message}</div>`;
|
||
}
|
||
});
|
||
|
||
// Загрузка постов
|
||
async function loadPosts() {
|
||
try {
|
||
const r = await fetch('/api/scheduled_posts/list');
|
||
const result = await r.json();
|
||
|
||
const postsList = document.getElementById('postsList');
|
||
const noPosts = document.getElementById('noPosts');
|
||
|
||
if (result.success && result.posts.length > 0) {
|
||
noPosts.style.display = 'none';
|
||
postsList.innerHTML = '';
|
||
|
||
result.posts.forEach(post => {
|
||
const card = document.createElement('div');
|
||
card.className = 'card mb-3 post-card';
|
||
card.innerHTML = `
|
||
<div class="card-body">
|
||
<div class="d-flex justify-content-between align-items-start mb-2">
|
||
<h6 class="mb-1">${post.topic_emoji} ${post.topic_name || 'Без темы'}</h6>
|
||
<span class="badge bg-${post.status === 'pending' ? 'warning' : post.status === 'sent' ? 'success' : post.status === 'failed' ? 'danger' : 'secondary'}">
|
||
${post.status_emoji} ${post.status}
|
||
</span>
|
||
</div>
|
||
<p class="mb-2">${post.text.substring(0, 100)}${post.text.length > 100 ? '...' : ''}</p>
|
||
<div class="d-flex justify-content-between align-items-center">
|
||
<small class="text-muted">
|
||
📅 ${post.scheduled_time}
|
||
</small>
|
||
${post.status === 'pending' ? `
|
||
<button class="btn btn-sm btn-danger" onclick="cancelPost(${post.id})">
|
||
<i class="bi bi-x-circle"></i> Отменить
|
||
</button>
|
||
` : ''}
|
||
</div>
|
||
</div>
|
||
`;
|
||
postsList.appendChild(card);
|
||
});
|
||
} else {
|
||
noPosts.style.display = 'block';
|
||
postsList.innerHTML = '';
|
||
}
|
||
} catch (err) {
|
||
console.error('Ошибка загрузки постов:', err);
|
||
}
|
||
}
|
||
|
||
// Отмена поста
|
||
async function cancelPost(postId) {
|
||
if (!confirm('Отменить этот пост?')) return;
|
||
|
||
try {
|
||
const r = await fetch(`/api/scheduled_posts/${postId}/cancel`, {
|
||
method: 'POST'
|
||
});
|
||
const result = await r.json();
|
||
|
||
if (result.success) {
|
||
alert('✅ Пост отменён');
|
||
loadPosts();
|
||
} else {
|
||
alert('❌ Ошибка: ' + (result.error || 'Неизвестная'));
|
||
}
|
||
} catch (err) {
|
||
alert('❌ Ошибка: ' + err.message);
|
||
}
|
||
}
|
||
</script>
|
||
{% endblock %}
|