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

105
app.py
View File

@@ -1720,6 +1720,48 @@ def logout():
return jsonify({"success": True}) return jsonify({"success": True})
# ==================== 公告API ====================
@app.route('/api/announcements/active', methods=['GET'])
@login_required
def get_active_announcement():
"""获取当前用户应展示的公告若无则返回announcement=null"""
try:
user_id = int(current_user.id)
except Exception:
return jsonify({"announcement": None})
announcement = database.get_active_announcement_for_user(user_id)
if not announcement:
return jsonify({"announcement": None})
return jsonify({
"announcement": {
"id": announcement.get("id"),
"title": announcement.get("title", ""),
"content": announcement.get("content", ""),
"created_at": announcement.get("created_at")
}
})
@app.route('/api/announcements/<int:announcement_id>/dismiss', methods=['POST'])
@login_required
def dismiss_announcement(announcement_id):
"""用户永久关闭某条公告(本次公告不再弹窗)"""
try:
user_id = int(current_user.id)
except Exception:
return jsonify({"error": "请先登录"}), 401
announcement = database.get_announcement_by_id(announcement_id)
if not announcement:
return jsonify({"error": "公告不存在"}), 404
database.dismiss_announcement_for_user(user_id, announcement_id)
return jsonify({"success": True})
# ==================== 管理员认证API ==================== # ==================== 管理员认证API ====================
@app.route('/yuyx/api/debug-config', methods=['GET']) @app.route('/yuyx/api/debug-config', methods=['GET'])
@@ -1812,6 +1854,69 @@ def admin_logout():
return jsonify({"success": True}) return jsonify({"success": True})
# ==================== 公告管理API管理员 ====================
@app.route('/yuyx/api/announcements', methods=['GET'])
@admin_required
def admin_get_announcements():
"""获取公告列表"""
try:
limit = int(request.args.get('limit', 50))
offset = int(request.args.get('offset', 0))
except (TypeError, ValueError):
limit, offset = 50, 0
limit = max(1, min(200, limit))
offset = max(0, offset)
return jsonify(database.get_announcements(limit=limit, offset=offset))
@app.route('/yuyx/api/announcements', methods=['POST'])
@admin_required
def admin_create_announcement():
"""创建公告(默认启用并替换旧公告)"""
data = request.json or {}
title = (data.get('title') or '').strip()
content = (data.get('content') or '').strip()
is_active = bool(data.get('is_active', True))
announcement_id = database.create_announcement(title, content, is_active=is_active)
if not announcement_id:
return jsonify({"error": "标题和内容不能为空"}), 400
return jsonify({"success": True, "id": announcement_id})
@app.route('/yuyx/api/announcements/<int:announcement_id>/activate', methods=['POST'])
@admin_required
def admin_activate_announcement(announcement_id):
"""启用公告(会自动停用其他公告)"""
if not database.get_announcement_by_id(announcement_id):
return jsonify({"error": "公告不存在"}), 404
ok = database.set_announcement_active(announcement_id, True)
return jsonify({"success": ok})
@app.route('/yuyx/api/announcements/<int:announcement_id>/deactivate', methods=['POST'])
@admin_required
def admin_deactivate_announcement(announcement_id):
"""停用公告"""
if not database.get_announcement_by_id(announcement_id):
return jsonify({"error": "公告不存在"}), 404
ok = database.set_announcement_active(announcement_id, False)
return jsonify({"success": ok})
@app.route('/yuyx/api/announcements/<int:announcement_id>', methods=['DELETE'])
@admin_required
def admin_delete_announcement(announcement_id):
"""删除公告"""
if not database.get_announcement_by_id(announcement_id):
return jsonify({"error": "公告不存在"}), 404
ok = database.delete_announcement(announcement_id)
return jsonify({"success": ok})
@app.route('/yuyx/api/users', methods=['GET']) @app.route('/yuyx/api/users', methods=['GET'])
@admin_required @admin_required
def get_all_users(): def get_all_users():

View File

@@ -54,7 +54,7 @@ config = get_config()
DB_FILE = config.DB_FILE DB_FILE = config.DB_FILE
# 数据库版本 (用于迁移管理) # 数据库版本 (用于迁移管理)
DB_VERSION = 5 DB_VERSION = 6
# ==================== 时区处理工具函数 ==================== # ==================== 时区处理工具函数 ====================
# Bug fix: 统一时区处理,避免混用导致的问题 # Bug fix: 统一时区处理,避免混用导致的问题
@@ -219,6 +219,30 @@ def init_database():
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
) )
''') ''')
# 公告表
cursor.execute('''
CREATE TABLE IF NOT EXISTS announcements (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
content TEXT NOT NULL,
is_active INTEGER DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
# 公告永久关闭记录表(用户维度)
cursor.execute('''
CREATE TABLE IF NOT EXISTS announcement_dismissals (
user_id INTEGER NOT NULL,
announcement_id INTEGER NOT NULL,
dismissed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (user_id, announcement_id),
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
FOREIGN KEY (announcement_id) REFERENCES announcements (id) ON DELETE CASCADE
)
''')
# 用户定时任务表 # 用户定时任务表
cursor.execute(''' cursor.execute('''
CREATE TABLE IF NOT EXISTS user_schedules ( CREATE TABLE IF NOT EXISTS user_schedules (
@@ -263,6 +287,11 @@ def init_database():
cursor.execute('CREATE INDEX IF NOT EXISTS idx_bug_feedbacks_user_id ON bug_feedbacks(user_id)') cursor.execute('CREATE INDEX IF NOT EXISTS idx_bug_feedbacks_user_id ON bug_feedbacks(user_id)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_bug_feedbacks_status ON bug_feedbacks(status)') cursor.execute('CREATE INDEX IF NOT EXISTS idx_bug_feedbacks_status ON bug_feedbacks(status)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_bug_feedbacks_created_at ON bug_feedbacks(created_at)') cursor.execute('CREATE INDEX IF NOT EXISTS idx_bug_feedbacks_created_at ON bug_feedbacks(created_at)')
# 公告表索引
cursor.execute('CREATE INDEX IF NOT EXISTS idx_announcements_active ON announcements(is_active)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_announcements_created_at ON announcements(created_at)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_announcement_dismissals_user ON announcement_dismissals(user_id)')
# 用户定时任务表索引 # 用户定时任务表索引
cursor.execute('CREATE INDEX IF NOT EXISTS idx_user_schedules_user_id ON user_schedules(user_id)') cursor.execute('CREATE INDEX IF NOT EXISTS idx_user_schedules_user_id ON user_schedules(user_id)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_user_schedules_enabled ON user_schedules(enabled)') cursor.execute('CREATE INDEX IF NOT EXISTS idx_user_schedules_enabled ON user_schedules(enabled)')
@@ -346,6 +375,10 @@ def migrate_database():
_migrate_to_v5(conn) _migrate_to_v5(conn)
current_version = 5 current_version = 5
if current_version < 6:
_migrate_to_v6(conn)
current_version = 6
# 更新版本号 # 更新版本号
cursor.execute('UPDATE db_version SET version = ?, updated_at = CURRENT_TIMESTAMP WHERE id = 1', cursor.execute('UPDATE db_version SET version = ?, updated_at = CURRENT_TIMESTAMP WHERE id = 1',
(DB_VERSION,)) (DB_VERSION,))
@@ -514,6 +547,46 @@ def _migrate_to_v5(conn):
conn.commit() conn.commit()
def _migrate_to_v6(conn):
"""迁移到版本6 - 添加公告功能相关表"""
cursor = conn.cursor()
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='announcements'")
if not cursor.fetchone():
cursor.execute('''
CREATE TABLE IF NOT EXISTS announcements (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
content TEXT NOT NULL,
is_active INTEGER DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
print(" ✓ 创建 announcements 表 (公告)")
cursor.execute('CREATE INDEX IF NOT EXISTS idx_announcements_active ON announcements(is_active)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_announcements_created_at ON announcements(created_at)')
print(" ✓ 创建 announcements 表索引")
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='announcement_dismissals'")
if not cursor.fetchone():
cursor.execute('''
CREATE TABLE IF NOT EXISTS announcement_dismissals (
user_id INTEGER NOT NULL,
announcement_id INTEGER NOT NULL,
dismissed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (user_id, announcement_id),
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
FOREIGN KEY (announcement_id) REFERENCES announcements (id) ON DELETE CASCADE
)
''')
print(" ✓ 创建 announcement_dismissals 表 (公告永久关闭记录)")
cursor.execute('CREATE INDEX IF NOT EXISTS idx_announcement_dismissals_user ON announcement_dismissals(user_id)')
print(" ✓ 创建 announcement_dismissals 表索引")
conn.commit()
# ==================== 管理员相关 ==================== # ==================== 管理员相关 ====================
def ensure_default_admin(): def ensure_default_admin():
@@ -1713,6 +1786,115 @@ def get_feedback_stats():
return dict(row) if row else {'total': 0, 'pending': 0, 'replied': 0, 'closed': 0} return dict(row) if row else {'total': 0, 'pending': 0, 'replied': 0, 'closed': 0}
# ==================== 公告管理 ====================
def create_announcement(title, content, is_active=True):
"""创建公告(默认启用;启用时会自动停用其他公告)"""
title = (title or '').strip()
content = (content or '').strip()
if not title or not content:
return None
with db_pool.get_db() as conn:
cursor = conn.cursor()
cst_time = get_cst_now_str()
if is_active:
cursor.execute('UPDATE announcements SET is_active = 0, updated_at = ? WHERE is_active = 1', (cst_time,))
cursor.execute('''
INSERT INTO announcements (title, content, is_active, created_at, updated_at)
VALUES (?, ?, ?, ?, ?)
''', (title, content, 1 if is_active else 0, cst_time, cst_time))
conn.commit()
return cursor.lastrowid
def get_announcement_by_id(announcement_id):
"""根据ID获取公告"""
with db_pool.get_db() as conn:
cursor = conn.cursor()
cursor.execute('SELECT * FROM announcements WHERE id = ?', (announcement_id,))
row = cursor.fetchone()
return dict(row) if row else None
def get_announcements(limit=50, offset=0):
"""获取公告列表(管理员用)"""
with db_pool.get_db() as conn:
cursor = conn.cursor()
cursor.execute('''
SELECT * FROM announcements
ORDER BY created_at DESC, id DESC
LIMIT ? OFFSET ?
''', (limit, offset))
return [dict(row) for row in cursor.fetchall()]
def set_announcement_active(announcement_id, is_active):
"""启用/停用公告;启用时会自动停用其他公告"""
with db_pool.get_db() as conn:
cursor = conn.cursor()
cst_time = get_cst_now_str()
if is_active:
cursor.execute('UPDATE announcements SET is_active = 0, updated_at = ? WHERE is_active = 1', (cst_time,))
cursor.execute('''
UPDATE announcements
SET is_active = 1, updated_at = ?
WHERE id = ?
''', (cst_time, announcement_id))
else:
cursor.execute('''
UPDATE announcements
SET is_active = 0, updated_at = ?
WHERE id = ?
''', (cst_time, announcement_id))
conn.commit()
return cursor.rowcount > 0
def delete_announcement(announcement_id):
"""删除公告(同时清理用户关闭记录)"""
with db_pool.get_db() as conn:
cursor = conn.cursor()
cursor.execute('DELETE FROM announcement_dismissals WHERE announcement_id = ?', (announcement_id,))
cursor.execute('DELETE FROM announcements WHERE id = ?', (announcement_id,))
conn.commit()
return cursor.rowcount > 0
def get_active_announcement_for_user(user_id):
"""获取当前用户应展示的启用公告(已永久关闭的不再返回)"""
with db_pool.get_db() as conn:
cursor = conn.cursor()
cursor.execute('''
SELECT a.*
FROM announcements a
LEFT JOIN announcement_dismissals d
ON d.announcement_id = a.id AND d.user_id = ?
WHERE a.is_active = 1 AND d.announcement_id IS NULL
ORDER BY a.created_at DESC, a.id DESC
LIMIT 1
''', (user_id,))
row = cursor.fetchone()
return dict(row) if row else None
def dismiss_announcement_for_user(user_id, announcement_id):
"""用户永久关闭某条公告(幂等)"""
with db_pool.get_db() as conn:
cursor = conn.cursor()
cst_time = get_cst_now_str()
cursor.execute('''
INSERT OR IGNORE INTO announcement_dismissals (user_id, announcement_id, dismissed_at)
VALUES (?, ?, ?)
''', (user_id, announcement_id, cst_time))
conn.commit()
return cursor.rowcount >= 0
# ==================== 用户定时任务管理 ==================== # ==================== 用户定时任务管理 ====================
def get_user_schedules(user_id): def get_user_schedules(user_id):

View File

@@ -264,6 +264,18 @@
font-size: 14px; 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 { .empty-message {
text-align: center; text-align: center;
padding: 30px 15px; 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('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('stats')">统计</button>
<button class="tab" onclick="switchTab('logs')">任务日志</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('email')">邮件配置</button>
<button class="tab" onclick="switchTab('system')">系统配置</button> <button class="tab" onclick="switchTab('system')">系统配置</button>
<button class="tab" onclick="switchTab('settings')">设置</button> <button class="tab" onclick="switchTab('settings')">设置</button>
@@ -1200,6 +1213,38 @@
</div> </div>
</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"> <div id="tab-email" class="tab-content">
<h3 style="margin-bottom: 15px; font-size: 16px;">邮件功能设置</h3> <h3 style="margin-bottom: 15px; font-size: 16px;">邮件功能设置</h3>
@@ -1455,12 +1500,23 @@
<script> <script>
let allUsers = []; let allUsers = [];
let pendingUsers = []; 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', () => { window.addEventListener('load', () => {
loadStats(); loadStats();
loadPendingUsers(); loadPendingUsers();
loadAllUsers(); loadAllUsers();
loadAnnouncements();
loadSystemConfig(); loadSystemConfig();
loadProxyConfig(); loadProxyConfig();
loadPasswordResets(); // 修复: 初始化时也加载密码重置申请 loadPasswordResets(); // 修复: 初始化时也加载密码重置申请
@@ -1506,6 +1562,11 @@
loadFeedbacks(); loadFeedbacks();
} }
// 切换到公告管理标签时加载公告
if (tabName === 'announcements') {
loadAnnouncements();
}
// 切换到邮件配置标签时加载邮件相关数据 // 切换到邮件配置标签时加载邮件相关数据
if (tabName === 'email') { if (tabName === 'email') {
loadEmailSettings(); 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 // VIP functions
function isVip(user) { function isVip(user) {
if (!user.vip_expire_time) return false; if (!user.vip_expire_time) return false;
@@ -3250,4 +3465,4 @@
} }
</script> </script>
</body> </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> <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="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-overlay" id="addAccountModal">
<div class="modal"> <div class="modal">
@@ -1040,10 +1056,63 @@
updateAccountLimitDisplay(); 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() { document.addEventListener('DOMContentLoaded', function() {
initTabs(); initTabs();
loadVipStatus(); loadVipStatus();
loadAccounts(); // 主动加载账号列表 loadAccounts(); // 主动加载账号列表
checkAnnouncement();
loadStats(); loadStats();
loadSchedules(); loadSchedules();
loadScreenshots(); loadScreenshots();
@@ -2086,6 +2155,16 @@
} }
function logout() { 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'}) fetch('/api/logout', {method: 'POST'})
.then(() => { window.location.href = '/login'; }) .then(() => { window.location.href = '/login'; })
.catch(() => { window.location.href = '/login'; }); .catch(() => { window.location.href = '/login'; });
@@ -2298,4 +2377,4 @@
}); });
</script> </script>
</body> </body>
</html> </html>