diff --git a/__pycache__/app.cpython-310.pyc b/__pycache__/app.cpython-310.pyc new file mode 100644 index 0000000..f51019b Binary files /dev/null and b/__pycache__/app.cpython-310.pyc differ diff --git a/app.py b/app.py index ce126c5..8d0583b 100644 --- a/app.py +++ b/app.py @@ -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) diff --git a/data/notifications.json b/data/notifications.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/data/notifications.json @@ -0,0 +1 @@ +[] diff --git a/data/pending_reviews.json b/data/pending_reviews.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/data/pending_reviews.json @@ -0,0 +1 @@ +[] diff --git a/logs/app.log b/logs/app.log index 21fe9dd..d95acc7 100644 --- a/logs/app.log +++ b/logs/app.log @@ -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 + 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 + @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. diff --git a/modules/routes/__pycache__/api_cpus.cpython-310.pyc b/modules/routes/__pycache__/api_cpus.cpython-310.pyc index e568bf6..acd4bc7 100644 Binary files a/modules/routes/__pycache__/api_cpus.cpython-310.pyc and b/modules/routes/__pycache__/api_cpus.cpython-310.pyc differ diff --git a/modules/routes/__pycache__/api_gpus.cpython-310.pyc b/modules/routes/__pycache__/api_gpus.cpython-310.pyc index 2a95b63..7d2a976 100644 Binary files a/modules/routes/__pycache__/api_gpus.cpython-310.pyc and b/modules/routes/__pycache__/api_gpus.cpython-310.pyc differ diff --git a/modules/routes/__pycache__/api_items.cpython-310.pyc b/modules/routes/__pycache__/api_items.cpython-310.pyc index 16e60a4..80e0f2f 100644 Binary files a/modules/routes/__pycache__/api_items.cpython-310.pyc and b/modules/routes/__pycache__/api_items.cpython-310.pyc differ diff --git a/modules/routes/__pycache__/api_models.cpython-310.pyc b/modules/routes/__pycache__/api_models.cpython-310.pyc index 1250244..f123538 100644 Binary files a/modules/routes/__pycache__/api_models.cpython-310.pyc and b/modules/routes/__pycache__/api_models.cpython-310.pyc differ diff --git a/modules/routes/__pycache__/api_notifications.cpython-310.pyc b/modules/routes/__pycache__/api_notifications.cpython-310.pyc new file mode 100644 index 0000000..575995d Binary files /dev/null and b/modules/routes/__pycache__/api_notifications.cpython-310.pyc differ diff --git a/modules/routes/__pycache__/api_reviews.cpython-310.pyc b/modules/routes/__pycache__/api_reviews.cpython-310.pyc new file mode 100644 index 0000000..44b9b18 Binary files /dev/null and b/modules/routes/__pycache__/api_reviews.cpython-310.pyc differ diff --git a/modules/routes/api_cpus.py b/modules/routes/api_cpus.py index 614253a..2329329 100644 --- a/modules/routes/api_cpus.py +++ b/modules/routes/api_cpus.py @@ -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') diff --git a/modules/routes/api_gpus.py b/modules/routes/api_gpus.py index c9deb4e..c009279 100644 --- a/modules/routes/api_gpus.py +++ b/modules/routes/api_gpus.py @@ -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') diff --git a/modules/routes/api_items.py b/modules/routes/api_items.py index f9a0650..1121596 100644 --- a/modules/routes/api_items.py +++ b/modules/routes/api_items.py @@ -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/') 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/', 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] diff --git a/modules/routes/api_models.py b/modules/routes/api_models.py index 6287dcd..0334bcf 100644 --- a/modules/routes/api_models.py +++ b/modules/routes/api_models.py @@ -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') diff --git a/modules/routes/api_notifications.py b/modules/routes/api_notifications.py new file mode 100644 index 0000000..aa4835a --- /dev/null +++ b/modules/routes/api_notifications.py @@ -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//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/', 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 \ No newline at end of file diff --git a/modules/routes/api_reviews.py b/modules/routes/api_reviews.py new file mode 100644 index 0000000..56b4b4a --- /dev/null +++ b/modules/routes/api_reviews.py @@ -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/') +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//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//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 \ No newline at end of file diff --git a/templates/admin.html b/templates/admin.html index 3f5c98b..875b603 100644 --- a/templates/admin.html +++ b/templates/admin.html @@ -41,6 +41,21 @@

管理概览

+ + + +
加载中...

快捷操作

@@ -275,6 +290,49 @@
+ + + + + + @@ -766,6 +824,24 @@ `; + // 通知中心(显示未读数) + html += ` + + + 通知中心 + + + `; + + // 审核管理(显示待审核数) + html += ` + + + 审核管理 + + + `; + 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 => `
${m.name}${m.organization}
${m.is_open_source ? '开源' : '商业'}
`).join('') : '
暂无数据
'; + + // 加载通知计数 + 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 = ` +
+ +
暂无通知
+
+ `; + 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 => ` +
+
+ +
+
${n.title}
+
${n.message}
+
${n.created_at}
+
+ ${!n.read ? `` : ''} +
+
+ `).join(''); + + document.getElementById('notificationsList').innerHTML = html; + } catch (e) { + document.getElementById('notificationsList').innerHTML = ` +
加载失败: ${e.message}
+ `; + } + } + + // 标记单个通知已读 + 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 = ` + 暂无数据 + `; + return; + } + + const statusLabels = { + pending: '待审核', + approved: '已通过', + rejected: '已拒绝' + }; + + const html = reviews.map(r => { + const catName = categories.find(c => c.id === r.category_id)?.name || r.category_id; + return ` + + ${r.data?.name || '-'} + ${catName} + + ${r.source === 'api' ? 'API' : '网页'} + + ${r.created_at} + ${statusLabels[r.status] || r.status} + + ${r.status === 'pending' ? ` + + + + ` : ` + + `} + + + `; + }).join(''); + + document.getElementById('reviewsTable').innerHTML = html; + } catch (e) { + document.getElementById('reviewsTable').innerHTML = ` + 加载失败: ${e.message} + `; + } + } + + // 通过审核 + 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 = '
'; + html += `
状态: ${review.status}
`; + html += `
分类: ${review.category_id}
`; + html += `
来源: ${review.source}
`; + html += `
提交时间: ${review.created_at}
`; + html += '

产品数据:

'; + html += '
' + JSON.stringify(review.data, null, 2) + '
'; + html += '
'; + + alert(html.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim()); + } catch (e) { + alert('加载失败: ' + e.message); + } + } + init();