新增后台通知中心和产品审核发布功能

This commit is contained in:
2026-07-11 01:55:01 +08:00
parent e03e514b70
commit 836acf1aa8
18 changed files with 703 additions and 18 deletions
Binary file not shown.
+13 -3
View File
@@ -1,7 +1,7 @@
"""
ParamHub - 参数百科
AI大模型与硬件参数速查平台
v1.8.0 - 模块化重构 + 后台登录认证
v2.0.0 - 产品审核发布 + 后台通知系统
"""
from flask import Flask, request, jsonify, session, redirect, url_for
from flask_cors import CORS
@@ -10,6 +10,9 @@ from datetime import timedelta
from config import SECRET_KEY
from utils import load_config
# 审核开关:设为 True 则所有产品需要审核
REQUIRE_REVIEW = True
# ─── Flask 应用创建 ────────────────────────────────────────────
app = Flask(__name__, static_folder='static', static_url_path='/static')
@@ -79,17 +82,24 @@ app.register_blueprint(upload_bp)
from modules.routes.api_pin import pin_bp
app.register_blueprint(pin_bp)
from modules.routes.api_notifications import notifications_bp
app.register_blueprint(notifications_bp)
from modules.routes.api_reviews import reviews_bp
app.register_blueprint(reviews_bp)
# ─── 启动 ──────────────────────────────────────────────────────
if __name__ == '__main__':
print("=" * 50)
print("ParamHub - 参数百科 v1.8.0")
print("ParamHub - 参数百科 v2.0.0")
print("=" * 50)
print("模块化重构 + 后台登录认证")
print("产品审核发布 + 后台通知系统")
print(f"访问地址: http://localhost:16041")
print(f"后台管理: http://localhost:16041/admin")
print(f"默认密码: admin123 (可在 config.json 中修改)")
print(f"审核模式: {'开启' if REQUIRE_REVIEW else '关闭'}")
print("=" * 50)
app.run(host='0.0.0.0', port=16041, debug=False)
+1
View File
@@ -0,0 +1 @@
[]
+1
View File
@@ -0,0 +1 @@
[]
+11 -15
View File
@@ -1,15 +1,11 @@
==================================================
ParamHub - 参数百科 v1.8.0
==================================================
模块化重构 + 后台登录认证
访问地址: http://localhost:16041
后台管理: http://localhost:16041/admin
默认密码: admin123 (可在 config.json 中修改)
==================================================
* Serving Flask app 'app'
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:16041
* Running on http://192.168.0.101:16041
Press CTRL+C to quit
Traceback (most recent call last):
File "/home/openclaw/.openclaw/workspace-hz4th_coder/works/param-hub-python/app.py", line 52, in <module>
from modules.routes.api_models import models_bp
File "/home/openclaw/.openclaw/workspace-hz4th_coder/works/param-hub-python/modules/routes/api_models.py", line 18, in <module>
@models_bp.route('/api/models')
File "/home/openclaw/.local/lib/python3.12/site-packages/flask/sansio/scaffold.py", line 43, in wrapper_func
self._check_setup_finished(f_name)
File "/home/openclaw/.local/lib/python3.12/site-packages/flask/sansio/blueprints.py", line 215, in _check_setup_finished
raise AssertionError(
AssertionError: The setup method 'route' can no longer be called on the blueprint 'api_models'. It has already been registered at least once, any changes will not be applied consistently.
Make sure all imports, decorators, functions, etc. needed to set up the blueprint are done before registering it.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+25
View File
@@ -11,6 +11,14 @@ from utils import load_data, save_data, parse_date_to_timestamp
cpus_bp = Blueprint('api_cpus', __name__)
def is_review_required():
try:
import app as app_module
return getattr(app_module, 'REQUIRE_REVIEW', False)
except:
return False
def _safe_sort_key(x, key):
val = x.get(key)
if val is None:
@@ -54,6 +62,23 @@ def api_cpu_detail(cpu_id):
@cpus_bp.route('/api/cpus', methods=['POST'])
def api_create_cpu():
data = request.get_json()
# 审核模式
if is_review_required():
from modules.routes.api_reviews import submit_for_review
from modules.routes.api_notifications import create_notification
review = submit_for_review('cpus', data, source='web')
product_name = data.get('name', '未知')
create_notification(
title='新产品待审核',
message=f'有新的CPU "{product_name}"待审核',
level='warning',
category='review',
data={'review_id': review['id'], 'category': 'cpus'}
)
return jsonify({'success': True, 'message': '已提交审核,请等待管理员确认', 'review_id': review['id']})
cpus = load_data(CPUS_FILE)
data['id'] = uuid.uuid4().hex[:12]
data['created_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
+25
View File
@@ -11,6 +11,14 @@ from utils import load_data, save_data, parse_date_to_timestamp
gpus_bp = Blueprint('api_gpus', __name__)
def is_review_required():
try:
import app as app_module
return getattr(app_module, 'REQUIRE_REVIEW', False)
except:
return False
def _safe_sort_key(x, key):
val = x.get(key)
if val is None:
@@ -54,6 +62,23 @@ def api_gpu_detail(gpu_id):
@gpus_bp.route('/api/gpus', methods=['POST'])
def api_create_gpu():
data = request.get_json()
# 审核模式
if is_review_required():
from modules.routes.api_reviews import submit_for_review
from modules.routes.api_notifications import create_notification
review = submit_for_review('gpus', data, source='web')
product_name = data.get('name', '未知')
create_notification(
title='新产品待审核',
message=f'有新的GPU "{product_name}"待审核',
level='warning',
category='review',
data={'review_id': review['id'], 'category': 'gpus'}
)
return jsonify({'success': True, 'message': '已提交审核,请等待管理员确认', 'review_id': review['id']})
gpus = load_data(GPUS_FILE)
data['id'] = uuid.uuid4().hex[:12]
data['created_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
+25
View File
@@ -11,6 +11,14 @@ from utils import load_data, save_data, parse_date_to_timestamp
items_bp = Blueprint('api_items', __name__)
def is_review_required():
try:
import app as app_module
return getattr(app_module, 'REQUIRE_REVIEW', False)
except:
return False
@items_bp.route('/api/items/<category_id>')
def api_items(category_id):
items_file = DATA_DIR / f'items_{category_id}.json'
@@ -49,6 +57,23 @@ def api_item_detail(category_id, item_id):
@items_bp.route('/api/items/<category_id>', methods=['POST'])
def api_create_item(category_id):
data = request.get_json()
# 审核模式
if is_review_required():
from modules.routes.api_reviews import submit_for_review
from modules.routes.api_notifications import create_notification
review = submit_for_review(category_id, data, source='web')
product_name = data.get('name', '未知')
create_notification(
title='新产品待审核',
message=f'有新的产品"{product_name}"待审核',
level='warning',
category='review',
data={'review_id': review['id'], 'category': category_id}
)
return jsonify({'success': True, 'message': '已提交审核,请等待管理员确认', 'review_id': review['id']})
items_file = DATA_DIR / f'items_{category_id}.json'
items = load_data(items_file)
data['id'] = uuid.uuid4().hex[:12]
+27
View File
@@ -11,6 +11,15 @@ from utils import load_data, save_data, parse_date_to_timestamp, safe_sort_key
models_bp = Blueprint('api_models', __name__)
def is_review_required():
"""检查是否需要审核"""
try:
import app as app_module
return getattr(app_module, 'REQUIRE_REVIEW', False)
except:
return False
@models_bp.route('/api/models')
def api_models():
models = load_data(MODELS_FILE)
@@ -47,6 +56,24 @@ def api_model_detail(model_id):
@models_bp.route('/api/models', methods=['POST'])
def api_create_model():
data = request.get_json()
# 审核模式:提交到审核队列
if is_review_required():
from modules.routes.api_reviews import submit_for_review
from modules.routes.api_notifications import create_notification
review = submit_for_review('ai-models', data, source='web')
product_name = data.get('name', '未知')
create_notification(
title='新产品待审核',
message=f'有新的AI模型"{product_name}"待审核',
level='warning',
category='review',
data={'review_id': review['id'], 'category': 'ai-models'}
)
return jsonify({'success': True, 'message': '已提交审核,请等待管理员确认', 'review_id': review['id']})
# 直接创建模式
models = load_data(MODELS_FILE)
data['id'] = uuid.uuid4().hex[:12]
data['created_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
+109
View File
@@ -0,0 +1,109 @@
"""
通知管理 API
"""
import uuid
import json
from datetime import datetime
from flask import Blueprint, request, jsonify
from config import DATA_DIR
from utils import load_data, save_data
notifications_bp = Blueprint('api_notifications', __name__)
NOTIFICATIONS_FILE = DATA_DIR / 'notifications.json'
@notifications_bp.route('/api/notifications')
def api_notifications():
"""获取通知列表"""
notifications = load_data(NOTIFICATIONS_FILE)
# 筛选参数
unread_only = request.args.get('unread', '0') == '1'
limit = int(request.args.get('limit', 50))
if unread_only:
notifications = [n for n in notifications if not n.get('read', False)]
# 按时间倒序
notifications = sorted(notifications, key=lambda x: x.get('created_at', ''), reverse=True)
return jsonify(notifications[:limit])
@notifications_bp.route('/api/notifications/unread-count')
def api_unread_count():
"""获取未读通知数量"""
notifications = load_data(NOTIFICATIONS_FILE)
count = len([n for n in notifications if not n.get('read', False)])
return jsonify({'count': count})
@notifications_bp.route('/api/notifications/<notification_id>/read', methods=['POST'])
def api_mark_read(notification_id):
"""标记通知为已读"""
notifications = load_data(NOTIFICATIONS_FILE)
notification = next((n for n in notifications if n['id'] == notification_id), None)
if not notification:
return jsonify({'error': 'Notification not found'}), 404
notification['read'] = True
notification['read_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
save_data(NOTIFICATIONS_FILE, notifications)
return jsonify({'success': True})
@notifications_bp.route('/api/notifications/read-all', methods=['POST'])
def api_mark_all_read():
"""标记所有通知为已读"""
notifications = load_data(NOTIFICATIONS_FILE)
for n in notifications:
if not n.get('read', False):
n['read'] = True
n['read_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
save_data(NOTIFICATIONS_FILE, notifications)
return jsonify({'success': True})
@notifications_bp.route('/api/notifications/<notification_id>', methods=['DELETE'])
def api_delete_notification(notification_id):
"""删除通知"""
notifications = load_data(NOTIFICATIONS_FILE)
notifications = [n for n in notifications if n['id'] != notification_id]
save_data(NOTIFICATIONS_FILE, notifications)
return jsonify({'success': True})
# ─── 内部函数:创建通知 ───────────────────────────────────────────────────────
def create_notification(title, message, level='info', category='system', data=None):
"""
创建一条通知
参数:
title: 通知标题
message: 通知内容
level: 级别 info/warning/error/success
category: 分类 system/api/review/statistics
data: 附加数据(dict)
"""
notifications = load_data(NOTIFICATIONS_FILE)
notification = {
'id': uuid.uuid4().hex[:12],
'title': title,
'message': message,
'level': level, # info, warning, error, success
'category': category, # system, api, review, statistics
'data': data or {},
'read': False,
'created_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
}
notifications.append(notification)
# 只保留最近500条通知
if len(notifications) > 500:
notifications = sorted(notifications, key=lambda x: x.get('created_at', ''), reverse=True)[:500]
save_data(NOTIFICATIONS_FILE, notifications)
return notification
+159
View File
@@ -0,0 +1,159 @@
"""
产品审核 API
"""
import uuid
import json
from datetime import datetime
from flask import Blueprint, request, jsonify, session
from config import DATA_DIR, MODELS_FILE, GPUS_FILE, CPUS_FILE
from utils import load_data, save_data
reviews_bp = Blueprint('api_reviews', __name__)
PENDING_FILE = DATA_DIR / 'pending_reviews.json'
@reviews_bp.route('/api/reviews')
def api_reviews():
"""获取待审核列表"""
reviews = load_data(PENDING_FILE)
# 筛选参数
status = request.args.get('status', 'pending') # pending, approved, rejected, all
limit = int(request.args.get('limit', 100))
if status != 'all':
reviews = [r for r in reviews if r.get('status', 'pending') == status]
# 按时间倒序
reviews = sorted(reviews, key=lambda x: x.get('created_at', ''), reverse=True)
return jsonify(reviews[:limit])
@reviews_bp.route('/api/reviews/count')
def api_reviews_count():
"""获取待审核数量"""
reviews = load_data(PENDING_FILE)
pending_count = len([r for r in reviews if r.get('status', 'pending') == 'pending'])
return jsonify({'count': pending_count})
@reviews_bp.route('/api/reviews/<review_id>')
def api_review_detail(review_id):
"""获取审核详情"""
reviews = load_data(PENDING_FILE)
review = next((r for r in reviews if r['id'] == review_id), None)
if not review:
return jsonify({'error': 'Review not found'}), 404
return jsonify(review)
@reviews_bp.route('/api/reviews/<review_id>/approve', methods=['POST'])
def api_approve_review(review_id):
"""通过审核"""
reviews = load_data(PENDING_FILE)
review = next((r for r in reviews if r['id'] == review_id), None)
if not review:
return jsonify({'error': 'Review not found'}), 404
if review.get('status') != 'pending':
return jsonify({'error': '该申请已处理'}), 400
# 获取数据文件
category_id = review.get('category_id')
data_file = get_data_file(category_id)
if not data_file:
return jsonify({'error': 'Unknown category'}), 400
# 添加数据到正式文件
items = load_data(data_file)
item_data = review.get('data', {})
# 确保有ID和时间戳
if 'id' not in item_data or not item_data['id']:
item_data['id'] = uuid.uuid4().hex[:12]
item_data['created_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
item_data['visible'] = True
item_data['approved'] = True
item_data['approved_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
items.append(item_data)
save_data(data_file, items)
# 更新审核状态
review['status'] = 'approved'
review['approved_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
review['approved_by'] = session.get('username', 'admin')
save_data(PENDING_FILE, reviews)
return jsonify({'success': True, 'item': item_data})
@reviews_bp.route('/api/reviews/<review_id>/reject', methods=['POST'])
def api_reject_review(review_id):
"""拒绝审核"""
reviews = load_data(PENDING_FILE)
review = next((r for r in reviews if r['id'] == review_id), None)
if not review:
return jsonify({'error': 'Review not found'}), 404
if review.get('status') != 'pending':
return jsonify({'error': '该申请已处理'}), 400
data = request.get_json() or {}
reason = data.get('reason', '')
# 更新审核状态
review['status'] = 'rejected'
review['rejected_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
review['rejected_by'] = session.get('username', 'admin')
review['reject_reason'] = reason
save_data(PENDING_FILE, reviews)
return jsonify({'success': True})
def get_data_file(category_id):
"""获取类别对应的数据文件"""
builtin_map = {
'ai-models': MODELS_FILE,
'gpus': GPUS_FILE,
'cpus': CPUS_FILE
}
if category_id in builtin_map:
return builtin_map[category_id]
# 动态分类
return DATA_DIR / f'items_{category_id}.json'
# ─── 内部函数:提交审核 ───────────────────────────────────────────────────────
def submit_for_review(category_id, data, source='web', submitter=None):
"""
提交产品到审核队列
参数:
category_id: 分类ID
data: 产品数据(dict)
source: 来源 web/api
submitter: 提交者
"""
reviews = load_data(PENDING_FILE)
review = {
'id': uuid.uuid4().hex[:12],
'category_id': category_id,
'data': data,
'source': source,
'submitter': submitter or 'anonymous',
'status': 'pending', # pending, approved, rejected
'created_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
}
reviews.append(review)
save_data(PENDING_FILE, reviews)
return review
+307
View File
@@ -41,6 +41,21 @@
<!-- 概览 -->
<section id="section-overview">
<h1 class="text-2xl font-bold text-gray-800 mb-6">管理概览</h1>
<!-- 待办提醒 -->
<div id="todoAlert" class="mb-6 hidden">
<div class="bg-orange-50 border border-orange-200 rounded-lg p-4 flex items-center justify-between">
<div class="flex items-center gap-3">
<i class="ri-notification-badge-line text-2xl text-orange-600"></i>
<div>
<div class="font-medium text-orange-800"><span id="todoCount">0</span> 条待处理事项</div>
<div class="text-sm text-orange-600" id="todoDetail"></div>
</div>
</div>
<button onclick="showSection('reviews')" class="px-4 py-2 bg-orange-600 text-white rounded-lg hover:bg-orange-700">立即处理</button>
</div>
</div>
<div class="grid grid-cols-5 gap-4 mb-8" id="statsCards"><div class="text-center text-gray-400 py-4">加载中...</div></div>
<div class="bg-white rounded-xl p-6 shadow-sm mb-8">
<h2 class="text-lg font-semibold text-gray-800 mb-4">快捷操作</h2>
@@ -275,6 +290,49 @@
</table>
</div>
</section>
<!-- 通知中心 -->
<section id="section-notifications" class="hidden">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-800">通知中心</h1>
<div class="flex gap-2">
<button onclick="markAllRead()" class="px-4 py-2 bg-gray-200 text-gray-600 rounded-lg hover:bg-gray-300"><i class="ri-check-double-line mr-1"></i>全部已读</button>
</div>
</div>
<div id="notificationsList" class="space-y-3">
<div class="text-center text-gray-400 py-8">加载中...</div>
</div>
</section>
<!-- 审核管理 -->
<section id="section-reviews" class="hidden">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-800">审核管理</h1>
<div class="flex gap-2">
<select id="reviewStatusFilter" onchange="loadReviews()" class="px-4 py-2 border rounded-lg">
<option value="pending">待审核</option>
<option value="approved">已通过</option>
<option value="rejected">已拒绝</option>
<option value="all">全部</option>
</select>
</div>
</div>
<div class="bg-white rounded-xl shadow-sm overflow-hidden">
<table class="w-full">
<thead class="bg-gray-50 border-b">
<tr>
<th class="px-4 py-3 text-left text-sm font-medium text-gray-600">产品名称</th>
<th class="px-4 py-3 text-left text-sm font-medium text-gray-600">分类</th>
<th class="px-4 py-3 text-left text-sm font-medium text-gray-600">来源</th>
<th class="px-4 py-3 text-left text-sm font-medium text-gray-600">提交时间</th>
<th class="px-4 py-3 text-left text-sm font-medium text-gray-600">状态</th>
<th class="px-4 py-3 text-center text-sm font-medium text-gray-600">操作</th>
</tr>
</thead>
<tbody id="reviewsTable"><tr><td colspan="6" class="text-center text-gray-400 py-8">加载中...</td></tr></tbody>
</table>
</div>
</section>
</main>
<!-- 编辑弹窗 -->
@@ -766,6 +824,24 @@
</a>
`;
// 通知中心(显示未读数)
html += `
<a href="#notifications" onclick="showSection('notifications')" class="sidebar-link flex items-center gap-2 px-3 py-2 rounded-lg text-gray-300" data-section="notifications">
<i class="ri-notification-line"></i>
<span>通知中心</span>
<span id="navNotificationBadge" class="ml-auto px-1.5 py-0.5 bg-red-500 text-white text-xs rounded-full hidden">0</span>
</a>
`;
// 审核管理(显示待审核数)
html += `
<a href="#reviews" onclick="showSection('reviews')" class="sidebar-link flex items-center gap-2 px-3 py-2 rounded-lg text-gray-300" data-section="reviews">
<i class="ri-checkbox-circle-line"></i>
<span>审核管理</span>
<span id="navReviewBadge" class="ml-auto px-1.5 py-0.5 bg-orange-500 text-white text-xs rounded-full hidden">0</span>
</a>
`;
document.getElementById('sidebarNav').innerHTML = html;
}
@@ -882,6 +958,8 @@
if (section === 'gpus') loadAdminGpus();
if (section === 'cpus') loadAdminCpus();
if (section === 'knowledge') loadAdminKnowledge();
if (section === 'notifications') loadNotifications();
if (section === 'reviews') loadReviews();
}
// 加载网站配置
@@ -990,6 +1068,9 @@
document.getElementById('recent-models').innerHTML = models.length > 0
? models.map(m => `<div class="flex items-center justify-between p-3 bg-gray-50 rounded-lg"><div><span class="font-medium text-gray-800">${m.name}</span><span class="text-sm text-gray-500 ml-2">${m.organization}</span></div><div class="text-sm text-gray-400">${m.is_open_source ? '开源' : '商业'}</div></div>`).join('')
: '<div class="text-gray-400">暂无数据</div>';
// 加载通知计数
loadNotificationCounts();
}
// 内置分类列表
@@ -2993,6 +3074,232 @@
} catch (e) { alert('导入失败: ' + e.message); }
}
// ─── 通知和审核功能 ─────────────────────────────────────────────────
// 加载未读通知和待审核数
async function loadNotificationCounts() {
try {
const [notifRes, reviewRes] = await Promise.all([
fetch('/api/notifications/unread-count'),
fetch('/api/reviews/count')
]);
const notifData = await notifRes.json();
const reviewData = await reviewRes.json();
// 更新导航栏徽章
const notifBadge = document.getElementById('navNotificationBadge');
const reviewBadge = document.getElementById('navReviewBadge');
if (notifData.count > 0) {
notifBadge.textContent = notifData.count > 99 ? '99+' : notifData.count;
notifBadge.classList.remove('hidden');
} else {
notifBadge.classList.add('hidden');
}
if (reviewData.count > 0) {
reviewBadge.textContent = reviewData.count > 99 ? '99+' : reviewData.count;
reviewBadge.classList.remove('hidden');
} else {
reviewBadge.classList.add('hidden');
}
// 更新概览页的待办提醒
const todoAlert = document.getElementById('todoAlert');
const totalCount = notifData.count + reviewData.count;
if (totalCount > 0) {
todoAlert.classList.remove('hidden');
document.getElementById('todoCount').textContent = totalCount;
document.getElementById('todoDetail').textContent =
`${notifData.count} 条未读通知,${reviewData.count} 条待审核产品`;
} else {
todoAlert.classList.add('hidden');
}
} catch (e) {
console.error('加载通知数失败:', e);
}
}
// 加载通知列表
async function loadNotifications() {
try {
const res = await fetch('/api/notifications?limit=50');
const notifications = await res.json();
if (notifications.length === 0) {
document.getElementById('notificationsList').innerHTML = `
<div class="bg-white rounded-xl p-8 text-center text-gray-400">
<i class="ri-notification-off-line text-4xl mb-2"></i>
<div>暂无通知</div>
</div>
`;
return;
}
const levelColors = {
info: 'bg-blue-50 border-blue-200',
warning: 'bg-orange-50 border-orange-200',
error: 'bg-red-50 border-red-200',
success: 'bg-green-50 border-green-200'
};
const levelIcons = {
info: 'ri-information-line text-blue-600',
warning: 'ri-alert-line text-orange-600',
error: 'ri-error-warning-line text-red-600',
success: 'ri-checkbox-circle-line text-green-600'
};
const html = notifications.map(n => `
<div class="bg-white rounded-xl p-4 border ${n.read ? 'opacity-60' : ''} ${levelColors[n.level] || 'bg-gray-50 border-gray-200'}" id="notif-${n.id}">
<div class="flex items-start gap-3">
<i class="${levelIcons[n.level] || 'ri-notification-line text-gray-600'} text-xl mt-0.5"></i>
<div class="flex-1">
<div class="font-medium text-gray-800">${n.title}</div>
<div class="text-sm text-gray-600 mt-1">${n.message}</div>
<div class="text-xs text-gray-400 mt-2">${n.created_at}</div>
</div>
${!n.read ? `<button onclick="markRead('${n.id}')" class="text-xs text-indigo-600 hover:text-indigo-800">标为已读</button>` : ''}
</div>
</div>
`).join('');
document.getElementById('notificationsList').innerHTML = html;
} catch (e) {
document.getElementById('notificationsList').innerHTML = `
<div class="bg-white rounded-xl p-8 text-center text-red-500">加载失败: ${e.message}</div>
`;
}
}
// 标记单个通知已读
async function markRead(notifId) {
await fetch(`/api/notifications/${notifId}/read`, {method: 'POST'});
loadNotifications();
loadNotificationCounts();
}
// 标记全部已读
async function markAllRead() {
await fetch('/api/notifications/read-all', {method: 'POST'});
loadNotifications();
loadNotificationCounts();
}
// 加载审核列表
async function loadReviews() {
const status = document.getElementById('reviewStatusFilter').value;
try {
const res = await fetch(`/api/reviews?status=${status}`);
const reviews = await res.json();
if (reviews.length === 0) {
document.getElementById('reviewsTable').innerHTML = `
<tr><td colspan="6" class="text-center text-gray-400 py-8">暂无数据</td></tr>
`;
return;
}
const statusLabels = {
pending: '<span class="px-2 py-1 bg-orange-100 text-orange-700 rounded text-xs">待审核</span>',
approved: '<span class="px-2 py-1 bg-green-100 text-green-700 rounded text-xs">已通过</span>',
rejected: '<span class="px-2 py-1 bg-red-100 text-red-700 rounded text-xs">已拒绝</span>'
};
const html = reviews.map(r => {
const catName = categories.find(c => c.id === r.category_id)?.name || r.category_id;
return `
<tr class="border-b hover:bg-gray-50">
<td class="px-4 py-3 font-medium text-gray-800">${r.data?.name || '-'}</td>
<td class="px-4 py-3 text-gray-600">${catName}</td>
<td class="px-4 py-3 text-sm">
${r.source === 'api' ? '<span class="text-indigo-600">API</span>' : '<span class="text-gray-500">网页</span>'}
</td>
<td class="px-4 py-3 text-sm text-gray-500">${r.created_at}</td>
<td class="px-4 py-3">${statusLabels[r.status] || r.status}</td>
<td class="px-4 py-3 text-center">
${r.status === 'pending' ? `
<button onclick="approveReview('${r.id}')" class="text-green-600 hover:text-green-800 mr-2" title="通过"><i class="ri-check-line"></i></button>
<button onclick="rejectReview('${r.id}')" class="text-red-600 hover:text-red-800" title="拒绝"><i class="ri-close-line"></i></button>
<button onclick="viewReviewDetail('${r.id}')" class="text-blue-600 hover:text-blue-800 ml-2" title="查看详情"><i class="ri-eye-line"></i></button>
` : `
<button onclick="viewReviewDetail('${r.id}')" class="text-blue-600 hover:text-blue-800" title="查看详情"><i class="ri-eye-line"></i></button>
`}
</td>
</tr>
`;
}).join('');
document.getElementById('reviewsTable').innerHTML = html;
} catch (e) {
document.getElementById('reviewsTable').innerHTML = `
<tr><td colspan="6" class="text-center text-red-500 py-8">加载失败: ${e.message}</td></tr>
`;
}
}
// 通过审核
async function approveReview(reviewId) {
if (!confirm('确认通过该产品审核?')) return;
try {
const res = await fetch(`/api/reviews/${reviewId}/approve`, {method: 'POST'});
const data = await res.json();
if (data.error) {
alert('操作失败: ' + data.error);
} else {
alert('审核通过,产品已发布!');
loadReviews();
loadNotificationCounts();
loadOverview();
}
} catch (e) {
alert('操作失败: ' + e.message);
}
}
// 拒绝审核
async function rejectReview(reviewId) {
const reason = prompt('请输入拒绝原因(可选):');
if (reason === null) return; // 用户取消
try {
const res = await fetch(`/api/reviews/${reviewId}/reject`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({reason})
});
const data = await res.json();
if (data.error) {
alert('操作失败: ' + data.error);
} else {
alert('已拒绝该产品!');
loadReviews();
loadNotificationCounts();
}
} catch (e) {
alert('操作失败: ' + e.message);
}
}
// 查看审核详情
async function viewReviewDetail(reviewId) {
try {
const res = await fetch(`/api/reviews/${reviewId}`);
const review = await res.json();
let html = '<div class="space-y-4">';
html += `<div><span class="text-gray-500">状态:</span> ${review.status}</div>`;
html += `<div><span class="text-gray-500">分类:</span> ${review.category_id}</div>`;
html += `<div><span class="text-gray-500">来源:</span> ${review.source}</div>`;
html += `<div><span class="text-gray-500">提交时间:</span> ${review.created_at}</div>`;
html += '<div class="border-t pt-4"><h3 class="font-medium mb-2">产品数据:</h3>';
html += '<pre class="bg-gray-50 p-3 rounded text-sm overflow-auto max-h-60">' + JSON.stringify(review.data, null, 2) + '</pre>';
html += '</div></div>';
alert(html.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim());
} catch (e) {
alert('加载失败: ' + e.message);
}
}
init();
</script>
</body>