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

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 - 参数百科 ParamHub - 参数百科
AI大模型与硬件参数速查平台 AI大模型与硬件参数速查平台
v1.8.0 - 模块化重构 + 后台登录认证 v2.0.0 - 产品审核发布 + 后台通知系统
""" """
from flask import Flask, request, jsonify, session, redirect, url_for from flask import Flask, request, jsonify, session, redirect, url_for
from flask_cors import CORS from flask_cors import CORS
@@ -10,6 +10,9 @@ from datetime import timedelta
from config import SECRET_KEY from config import SECRET_KEY
from utils import load_config from utils import load_config
# 审核开关:设为 True 则所有产品需要审核
REQUIRE_REVIEW = True
# ─── Flask 应用创建 ──────────────────────────────────────────── # ─── Flask 应用创建 ────────────────────────────────────────────
app = Flask(__name__, static_folder='static', static_url_path='/static') 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 from modules.routes.api_pin import pin_bp
app.register_blueprint(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__': if __name__ == '__main__':
print("=" * 50) print("=" * 50)
print("ParamHub - 参数百科 v1.8.0") print("ParamHub - 参数百科 v2.0.0")
print("=" * 50) print("=" * 50)
print("模块化重构 + 后台登录认证") print("产品审核发布 + 后台通知系统")
print(f"访问地址: http://localhost:16041") print(f"访问地址: http://localhost:16041")
print(f"后台管理: http://localhost:16041/admin") print(f"后台管理: http://localhost:16041/admin")
print(f"默认密码: admin123 (可在 config.json 中修改)") print(f"默认密码: admin123 (可在 config.json 中修改)")
print(f"审核模式: {'开启' if REQUIRE_REVIEW else '关闭'}")
print("=" * 50) print("=" * 50)
app.run(host='0.0.0.0', port=16041, debug=False) 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 @@
================================================== Traceback (most recent call last):
ParamHub - 参数百科 v1.8.0 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>
访问地址: http://localhost:16041 @models_bp.route('/api/models')
后台管理: http://localhost:16041/admin File "/home/openclaw/.local/lib/python3.12/site-packages/flask/sansio/scaffold.py", line 43, in wrapper_func
默认密码: admin123 (可在 config.json 中修改) 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
* Serving Flask app 'app' raise AssertionError(
* Debug mode: off 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.
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. Make sure all imports, decorators, functions, etc. needed to set up the blueprint are done before registering it.
* 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
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__) 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): def _safe_sort_key(x, key):
val = x.get(key) val = x.get(key)
if val is None: if val is None:
@@ -54,6 +62,23 @@ def api_cpu_detail(cpu_id):
@cpus_bp.route('/api/cpus', methods=['POST']) @cpus_bp.route('/api/cpus', methods=['POST'])
def api_create_cpu(): def api_create_cpu():
data = request.get_json() 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) cpus = load_data(CPUS_FILE)
data['id'] = uuid.uuid4().hex[:12] data['id'] = uuid.uuid4().hex[:12]
data['created_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S') 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__) 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): def _safe_sort_key(x, key):
val = x.get(key) val = x.get(key)
if val is None: if val is None:
@@ -54,6 +62,23 @@ def api_gpu_detail(gpu_id):
@gpus_bp.route('/api/gpus', methods=['POST']) @gpus_bp.route('/api/gpus', methods=['POST'])
def api_create_gpu(): def api_create_gpu():
data = request.get_json() 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) gpus = load_data(GPUS_FILE)
data['id'] = uuid.uuid4().hex[:12] data['id'] = uuid.uuid4().hex[:12]
data['created_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S') 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__) 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>') @items_bp.route('/api/items/<category_id>')
def api_items(category_id): def api_items(category_id):
items_file = DATA_DIR / f'items_{category_id}.json' 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']) @items_bp.route('/api/items/<category_id>', methods=['POST'])
def api_create_item(category_id): def api_create_item(category_id):
data = request.get_json() 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_file = DATA_DIR / f'items_{category_id}.json'
items = load_data(items_file) items = load_data(items_file)
data['id'] = uuid.uuid4().hex[:12] 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__) 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') @models_bp.route('/api/models')
def api_models(): def api_models():
models = load_data(MODELS_FILE) models = load_data(MODELS_FILE)
@@ -47,6 +56,24 @@ def api_model_detail(model_id):
@models_bp.route('/api/models', methods=['POST']) @models_bp.route('/api/models', methods=['POST'])
def api_create_model(): def api_create_model():
data = request.get_json() 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) models = load_data(MODELS_FILE)
data['id'] = uuid.uuid4().hex[:12] data['id'] = uuid.uuid4().hex[:12]
data['created_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S') 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"> <section id="section-overview">
<h1 class="text-2xl font-bold text-gray-800 mb-6">管理概览</h1> <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="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"> <div class="bg-white rounded-xl p-6 shadow-sm mb-8">
<h2 class="text-lg font-semibold text-gray-800 mb-4">快捷操作</h2> <h2 class="text-lg font-semibold text-gray-800 mb-4">快捷操作</h2>
@@ -275,6 +290,49 @@
</table> </table>
</div> </div>
</section> </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> </main>
<!-- 编辑弹窗 --> <!-- 编辑弹窗 -->
@@ -766,6 +824,24 @@
</a> </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; document.getElementById('sidebarNav').innerHTML = html;
} }
@@ -882,6 +958,8 @@
if (section === 'gpus') loadAdminGpus(); if (section === 'gpus') loadAdminGpus();
if (section === 'cpus') loadAdminCpus(); if (section === 'cpus') loadAdminCpus();
if (section === 'knowledge') loadAdminKnowledge(); if (section === 'knowledge') loadAdminKnowledge();
if (section === 'notifications') loadNotifications();
if (section === 'reviews') loadReviews();
} }
// 加载网站配置 // 加载网站配置
@@ -990,6 +1068,9 @@
document.getElementById('recent-models').innerHTML = models.length > 0 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('') ? 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>'; : '<div class="text-gray-400">暂无数据</div>';
// 加载通知计数
loadNotificationCounts();
} }
// 内置分类列表 // 内置分类列表
@@ -2993,6 +3074,232 @@
} catch (e) { alert('导入失败: ' + e.message); } } 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(); init();
</script> </script>
</body> </body>