feat: 添加公告功能

This commit is contained in:
2025-12-13 18:40:42 +08:00
parent d7d878dc08
commit 7015de0055
4 changed files with 584 additions and 3 deletions

View File

@@ -264,6 +264,18 @@
font-size: 14px;
}
.form-group textarea {
width: 100%;
max-width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 14px;
font-family: inherit;
resize: vertical;
min-height: 120px;
}
.empty-message {
text-align: center;
padding: 30px 15px;
@@ -732,6 +744,7 @@
<button class="tab" onclick="switchTab('feedbacks')">反馈管理 <span id="feedbackBadge" style="background:#e74c3c; color:white; padding:2px 6px; border-radius:10px; font-size:11px; margin-left:3px; display:none;">0</span></button>
<button class="tab" onclick="switchTab('stats')">统计</button>
<button class="tab" onclick="switchTab('logs')">任务日志</button>
<button class="tab" onclick="switchTab('announcements')">公告管理</button>
<button class="tab" onclick="switchTab('email')">邮件配置</button>
<button class="tab" onclick="switchTab('system')">系统配置</button>
<button class="tab" onclick="switchTab('settings')">设置</button>
@@ -1200,6 +1213,38 @@
</div>
</div>
<!-- 公告管理 -->
<div id="tab-announcements" class="tab-content">
<h3 style="margin-bottom: 15px; font-size: 16px;">公告管理</h3>
<!-- 创建公告 -->
<div style="background: #f8f9fa; padding: 15px; border-radius: 8px; margin-bottom: 20px;">
<div class="form-group">
<label>公告标题</label>
<input type="text" id="announcementTitle" placeholder="请输入公告标题">
</div>
<div class="form-group">
<label>公告内容</label>
<textarea id="announcementContent" rows="5" placeholder="请输入公告内容(将以弹窗形式展示)"></textarea>
</div>
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
<button class="btn btn-primary" onclick="createAnnouncement(true)">发布并启用</button>
<button class="btn btn-secondary" onclick="createAnnouncement(false)">保存但不启用</button>
<button class="btn" onclick="clearAnnouncementForm()" style="background: #eee;">清空</button>
</div>
<div style="font-size: 12px; color: #666; margin-top: 10px;">
说明:启用公告后,用户登录进入系统将弹窗提示;用户可选择“当次关闭”或“永久关闭本次公告”。
</div>
</div>
<!-- 公告列表 -->
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:10px; flex-wrap: wrap; gap: 10px;">
<h4 style="font-size: 14px; margin: 0;">公告列表</h4>
<button class="btn btn-primary" onclick="loadAnnouncements()" style="padding:8px 15px;">刷新</button>
</div>
<div id="announcementsList"></div>
</div>
<!-- 邮件配置 -->
<div id="tab-email" class="tab-content">
<h3 style="margin-bottom: 15px; font-size: 16px;">邮件功能设置</h3>
@@ -1455,12 +1500,23 @@
<script>
let allUsers = [];
let pendingUsers = [];
let announcements = [];
function escapeHtml(text) {
return String(text ?? '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
// 页面加载时初始化
window.addEventListener('load', () => {
loadStats();
loadPendingUsers();
loadAllUsers();
loadAnnouncements();
loadSystemConfig();
loadProxyConfig();
loadPasswordResets(); // 修复: 初始化时也加载密码重置申请
@@ -1506,6 +1562,11 @@
loadFeedbacks();
}
// 切换到公告管理标签时加载公告
if (tabName === 'announcements') {
loadAnnouncements();
}
// 切换到邮件配置标签时加载邮件相关数据
if (tabName === 'email') {
loadEmailSettings();
@@ -1515,6 +1576,160 @@
}
}
// ==================== 公告管理 ====================
async function loadAnnouncements() {
try {
const response = await fetch('/yuyx/api/announcements');
if (!response.ok) {
showNotification('加载公告失败', 'error');
return;
}
announcements = await response.json();
renderAnnouncements();
} catch (e) {
showNotification('加载公告失败', 'error');
}
}
function renderAnnouncements() {
const container = document.getElementById('announcementsList');
if (!container) return;
if (!announcements || announcements.length === 0) {
container.innerHTML = '<div class="empty-message">暂无公告</div>';
return;
}
container.innerHTML = `
<div class="table-container">
<table>
<thead>
<tr>
<th style="width: 70px;">ID</th>
<th>标题</th>
<th style="width: 90px;">状态</th>
<th style="width: 170px;">创建时间</th>
<th style="width: 220px;">操作</th>
</tr>
</thead>
<tbody>
${announcements.map(a => `
<tr>
<td>${a.id}</td>
<td>${escapeHtml(a.title || '')}</td>
<td>
<span class="status-badge ${a.is_active ? 'status-approved' : 'status-rejected'}">
${a.is_active ? '启用' : '停用'}
</span>
</td>
<td>${a.created_at || '-'}</td>
<td>
<div class="action-buttons">
<button class="btn btn-small btn-secondary" onclick="viewAnnouncement(${a.id})">查看</button>
${a.is_active
? `<button class="btn btn-small btn-secondary" onclick="deactivateAnnouncement(${a.id})">停用</button>`
: `<button class="btn btn-small btn-success" onclick="activateAnnouncement(${a.id})">启用</button>`
}
<button class="btn btn-small btn-danger" onclick="deleteAnnouncement(${a.id})">删除</button>
</div>
</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
`;
}
function clearAnnouncementForm() {
const title = document.getElementById('announcementTitle');
const content = document.getElementById('announcementContent');
if (title) title.value = '';
if (content) content.value = '';
}
function viewAnnouncement(id) {
const announcement = announcements.find(a => a.id === id);
if (!announcement) return;
alert(`标题:${announcement.title || ''}\n\n内容:\n${announcement.content || ''}`);
}
async function createAnnouncement(isActive) {
const title = (document.getElementById('announcementTitle')?.value || '').trim();
const content = (document.getElementById('announcementContent')?.value || '').trim();
if (!title || !content) {
showNotification('标题和内容不能为空', 'error');
return;
}
try {
const response = await fetch('/yuyx/api/announcements', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, content, is_active: !!isActive })
});
const data = await response.json();
if (!response.ok) {
showNotification(data.error || '发布失败', 'error');
return;
}
showNotification('保存成功', 'success');
clearAnnouncementForm();
await loadAnnouncements();
} catch (e) {
showNotification('发布失败', 'error');
}
}
async function activateAnnouncement(id) {
if (!confirm('确定启用该公告吗?启用后将自动停用其他公告。')) return;
try {
const response = await fetch(`/yuyx/api/announcements/${id}/activate`, { method: 'POST' });
const data = await response.json();
if (!response.ok || !data.success) {
showNotification(data.error || '启用失败', 'error');
return;
}
showNotification('已启用', 'success');
await loadAnnouncements();
} catch (e) {
showNotification('启用失败', 'error');
}
}
async function deactivateAnnouncement(id) {
if (!confirm('确定停用该公告吗?')) return;
try {
const response = await fetch(`/yuyx/api/announcements/${id}/deactivate`, { method: 'POST' });
const data = await response.json();
if (!response.ok || !data.success) {
showNotification(data.error || '停用失败', 'error');
return;
}
showNotification('已停用', 'success');
await loadAnnouncements();
} catch (e) {
showNotification('停用失败', 'error');
}
}
async function deleteAnnouncement(id) {
if (!confirm('确定删除该公告吗?删除后无法恢复。')) return;
try {
const response = await fetch(`/yuyx/api/announcements/${id}`, { method: 'DELETE' });
const data = await response.json();
if (!response.ok || !data.success) {
showNotification(data.error || '删除失败', 'error');
return;
}
showNotification('已删除', 'success');
await loadAnnouncements();
} catch (e) {
showNotification('删除失败', 'error');
}
}
// VIP functions
function isVip(user) {
if (!user.vip_expire_time) return false;
@@ -3250,4 +3465,4 @@
}
</script>
</body>
</html>
</html>

View File

@@ -621,6 +621,22 @@
<button class="fab" onclick="openAddAccountModal()" title="添加账号"><span class="fab-icon">+</span><span class="fab-text">添加账号</span></button>
<div class="toast-container" id="toastContainer"></div>
<!-- 公告弹窗 -->
<div class="modal-overlay" id="announcementModal" onclick="if(event.target===this)closeAnnouncementOnce()">
<div class="modal" style="max-width: 560px;">
<div class="modal-header">
<h3 class="modal-title" id="announcementModalTitle">系统公告</h3>
</div>
<div class="modal-body">
<div id="announcementModalContent" style="white-space: pre-wrap;"></div>
</div>
<div class="modal-footer">
<button class="btn btn-text" onclick="closeAnnouncementOnce()">当次关闭</button>
<button class="btn btn-primary" onclick="dismissAnnouncementPermanently()">永久关闭</button>
</div>
</div>
</div>
<!-- 添加账号弹窗 -->
<div class="modal-overlay" id="addAccountModal">
<div class="modal">
@@ -1040,10 +1056,63 @@
updateAccountLimitDisplay();
});
}
let currentAnnouncementId = null;
async function checkAnnouncement() {
try {
const response = await fetch('/api/announcements/active');
if (!response.ok) return;
const data = await response.json();
const announcement = data?.announcement;
if (!announcement || !announcement.id) return;
const sessionKey = `announcement_closed_${announcement.id}`;
try {
if (sessionStorage.getItem(sessionKey)) return;
} catch (e) {
// ignore
}
currentAnnouncementId = announcement.id;
const titleEl = document.getElementById('announcementModalTitle');
const contentEl = document.getElementById('announcementModalContent');
if (titleEl) titleEl.textContent = announcement.title || '系统公告';
if (contentEl) contentEl.textContent = announcement.content || '';
openModal('announcementModal');
} catch (e) {
// ignore
}
}
function closeAnnouncementOnce() {
if (currentAnnouncementId) {
try {
sessionStorage.setItem(`announcement_closed_${currentAnnouncementId}`, '1');
} catch (e) {
// ignore
}
}
closeModal('announcementModal');
}
async function dismissAnnouncementPermanently() {
if (!currentAnnouncementId) {
closeModal('announcementModal');
return;
}
try {
await fetch(`/api/announcements/${currentAnnouncementId}/dismiss`, { method: 'POST' });
} catch (e) {
// ignore
}
closeAnnouncementOnce();
}
document.addEventListener('DOMContentLoaded', function() {
initTabs();
loadVipStatus();
loadAccounts(); // 主动加载账号列表
checkAnnouncement();
loadStats();
loadSchedules();
loadScreenshots();
@@ -2086,6 +2155,16 @@
}
function logout() {
try {
for (let i = sessionStorage.length - 1; i >= 0; i--) {
const key = sessionStorage.key(i);
if (key && key.startsWith('announcement_closed_')) {
sessionStorage.removeItem(key);
}
}
} catch (e) {
// ignore
}
fetch('/api/logout', {method: 'POST'})
.then(() => { window.location.href = '/login'; })
.catch(() => { window.location.href = '/login'; });
@@ -2298,4 +2377,4 @@
});
</script>
</body>
</html>
</html>