Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
836acf1aa8 |
Binary file not shown.
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
[]
|
||||
@@ -0,0 +1 @@
|
||||
[]
|
||||
+11
-15
@@ -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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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')
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user