feat: 添加公告功能
This commit is contained in:
105
app.py
105
app.py
@@ -1720,6 +1720,48 @@ def logout():
|
||||
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 ====================
|
||||
|
||||
@app.route('/yuyx/api/debug-config', methods=['GET'])
|
||||
@@ -1812,6 +1854,69 @@ def admin_logout():
|
||||
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'])
|
||||
@admin_required
|
||||
def get_all_users():
|
||||
|
||||
184
database.py
184
database.py
@@ -54,7 +54,7 @@ config = get_config()
|
||||
DB_FILE = config.DB_FILE
|
||||
|
||||
# 数据库版本 (用于迁移管理)
|
||||
DB_VERSION = 5
|
||||
DB_VERSION = 6
|
||||
|
||||
# ==================== 时区处理工具函数 ====================
|
||||
# Bug fix: 统一时区处理,避免混用导致的问题
|
||||
@@ -219,6 +219,30 @@ def init_database():
|
||||
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('''
|
||||
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_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_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_enabled ON user_schedules(enabled)')
|
||||
@@ -346,6 +375,10 @@ def migrate_database():
|
||||
_migrate_to_v5(conn)
|
||||
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',
|
||||
(DB_VERSION,))
|
||||
@@ -514,6 +547,46 @@ def _migrate_to_v5(conn):
|
||||
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():
|
||||
@@ -1713,6 +1786,115 @@ def get_feedback_stats():
|
||||
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):
|
||||
|
||||
@@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
// 页面加载时初始化
|
||||
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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user