Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01abe7f86f | ||
|
|
f83bf11669 | ||
|
|
bc2720257d | ||
|
|
cfe463ae8b | ||
|
|
7d376533f3 |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -87,9 +87,9 @@ if __name__ == '__main__':
|
||||
print("ParamHub - 参数百科 v1.8.0")
|
||||
print("=" * 50)
|
||||
print("模块化重构 + 后台登录认证")
|
||||
print(f"访问地址: http://localhost:16031")
|
||||
print(f"后台管理: http://localhost:16031/admin")
|
||||
print(f"访问地址: http://localhost:16041")
|
||||
print(f"后台管理: http://localhost:16041/admin")
|
||||
print(f"默认密码: admin123 (可在 config.json 中修改)")
|
||||
print("=" * 50)
|
||||
|
||||
app.run(host='0.0.0.0', port=16031, debug=False)
|
||||
app.run(host='0.0.0.0', port=16041, debug=False)
|
||||
@@ -0,0 +1,16 @@
|
||||
==================================================
|
||||
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
|
||||
127.0.0.1 - - [11/Jul/2026 00:34:38] "GET /api/models/export HTTP/1.1" 200 -
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -2,8 +2,9 @@
|
||||
分类管理 API
|
||||
"""
|
||||
import uuid
|
||||
import json
|
||||
from datetime import datetime
|
||||
from flask import Blueprint, request, jsonify
|
||||
from flask import Blueprint, request, jsonify, Response
|
||||
from config import CATEGORIES_FILE
|
||||
from utils import load_data, save_data
|
||||
|
||||
@@ -70,3 +71,157 @@ def api_toggle_category_visible(category_id):
|
||||
category['visible'] = not category.get('visible', True)
|
||||
save_data(CATEGORIES_FILE, categories)
|
||||
return jsonify({'success': True, 'visible': category['visible']})
|
||||
|
||||
|
||||
# ─── 导出分类数据 ───────────────────────────────────────────────────────
|
||||
|
||||
@cat_bp.route('/api/categories/export', methods=['GET'])
|
||||
def api_export_categories():
|
||||
"""导出所有分类配置(仅分类本身,不含数据项)"""
|
||||
try:
|
||||
categories = load_data(CATEGORIES_FILE)
|
||||
|
||||
export_data = {
|
||||
'categories': categories,
|
||||
'export_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'version': '1.0'
|
||||
}
|
||||
|
||||
json_str = json.dumps(export_data, ensure_ascii=False, indent=2)
|
||||
|
||||
response = Response(
|
||||
json_str,
|
||||
mimetype='application/json',
|
||||
headers={
|
||||
'Content-Disposition': f'attachment; filename=param-hub-categories-{datetime.now().strftime("%Y%m%d%H%M%S")}.json'
|
||||
}
|
||||
)
|
||||
return response
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@cat_bp.route('/api/categories/export/<category_id>', methods=['GET'])
|
||||
def api_export_single_category(category_id):
|
||||
"""导出单个分类配置"""
|
||||
try:
|
||||
categories = load_data(CATEGORIES_FILE)
|
||||
category = next((c for c in categories if c['id'] == category_id), None)
|
||||
if not category:
|
||||
return jsonify({'error': 'Category not found'}), 404
|
||||
|
||||
export_data = {
|
||||
'category': category,
|
||||
'export_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'version': '1.0'
|
||||
}
|
||||
|
||||
json_str = json.dumps(export_data, ensure_ascii=False, indent=2)
|
||||
|
||||
response = Response(
|
||||
json_str,
|
||||
mimetype='application/json',
|
||||
headers={
|
||||
'Content-Disposition': f'attachment; filename={category_id}-{datetime.now().strftime("%Y%m%d%H%M%S")}.json'
|
||||
}
|
||||
)
|
||||
return response
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
# ─── 导入分类数据 ───────────────────────────────────────────────────────
|
||||
|
||||
@cat_bp.route('/api/categories/import', methods=['GET', 'POST'])
|
||||
def api_import_categories():
|
||||
"""导入分类配置(仅分类本身,不含数据项)"""
|
||||
try:
|
||||
if request.method == 'GET':
|
||||
# 返回导入接口说明
|
||||
return jsonify({
|
||||
'endpoint': '/api/categories/import',
|
||||
'method': 'POST',
|
||||
'params': {
|
||||
'mode': 'merge(合并)或 replace(替换)'
|
||||
},
|
||||
'body': {
|
||||
'categories': '分类数组'
|
||||
}
|
||||
})
|
||||
|
||||
import_data = request.get_json()
|
||||
|
||||
if not import_data:
|
||||
return jsonify({'error': '无导入数据'}), 400
|
||||
|
||||
if 'categories' not in import_data:
|
||||
return jsonify({'error': '缺少 categories 字段'}), 400
|
||||
|
||||
mode = request.args.get('mode', 'merge')
|
||||
|
||||
result = {
|
||||
'success': True,
|
||||
'imported': 0,
|
||||
'updated': 0,
|
||||
'skipped': []
|
||||
}
|
||||
|
||||
existing_categories = load_data(CATEGORIES_FILE)
|
||||
imported_categories = import_data['categories']
|
||||
|
||||
for cat in imported_categories:
|
||||
existing = next((c for c in existing_categories if c['id'] == cat['id']), None)
|
||||
if existing:
|
||||
if mode == 'replace':
|
||||
existing.update(cat)
|
||||
result['updated'] += 1
|
||||
else:
|
||||
result['skipped'].append(cat['id'])
|
||||
else:
|
||||
existing_categories.append(cat)
|
||||
result['imported'] += 1
|
||||
|
||||
save_data(CATEGORIES_FILE, existing_categories)
|
||||
|
||||
return jsonify(result)
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@cat_bp.route('/api/categories/import/<category_id>', methods=['POST'])
|
||||
def api_import_single_category(category_id):
|
||||
"""导入单个分类配置"""
|
||||
try:
|
||||
import_data = request.get_json()
|
||||
|
||||
if not import_data:
|
||||
return jsonify({'error': '无导入数据'}), 400
|
||||
|
||||
if 'category' not in import_data:
|
||||
return jsonify({'error': '缺少 category 字段'}), 400
|
||||
|
||||
mode = request.args.get('mode', 'merge')
|
||||
|
||||
result = {
|
||||
'success': True,
|
||||
'imported': False,
|
||||
'updated': False
|
||||
}
|
||||
|
||||
categories = load_data(CATEGORIES_FILE)
|
||||
imported_cat = import_data['category']
|
||||
|
||||
existing = next((c for c in categories if c['id'] == category_id), None)
|
||||
if existing:
|
||||
if mode == 'replace':
|
||||
existing.update(imported_cat)
|
||||
result['updated'] = True
|
||||
else:
|
||||
categories.append(imported_cat)
|
||||
result['imported'] = True
|
||||
|
||||
save_data(CATEGORIES_FILE, categories)
|
||||
|
||||
return jsonify(result)
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
@@ -2,8 +2,9 @@
|
||||
CPU CRUD API
|
||||
"""
|
||||
import uuid
|
||||
import json
|
||||
from datetime import datetime
|
||||
from flask import Blueprint, request, jsonify
|
||||
from flask import Blueprint, request, jsonify, Response
|
||||
from config import CPUS_FILE
|
||||
from utils import load_data, save_data, parse_date_to_timestamp
|
||||
|
||||
@@ -95,3 +96,63 @@ def api_toggle_cpu_visible(cpu_id):
|
||||
cpu['visible'] = not cpu.get('visible', True)
|
||||
save_data(CPUS_FILE, cpus)
|
||||
return jsonify({'success': True, 'visible': cpu['visible']})
|
||||
|
||||
|
||||
# ─── 导出导入 ───────────────────────────────────────────────────────
|
||||
|
||||
@cpus_bp.route('/api/cpus/export', methods=['GET'])
|
||||
def api_export_cpus():
|
||||
"""导出所有CPU数据"""
|
||||
try:
|
||||
cpus = load_data(CPUS_FILE)
|
||||
export_data = {
|
||||
'type': 'cpus',
|
||||
'items': cpus,
|
||||
'count': len(cpus),
|
||||
'export_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'version': '1.0'
|
||||
}
|
||||
json_str = json.dumps(export_data, ensure_ascii=False, indent=2)
|
||||
response = Response(
|
||||
json_str,
|
||||
mimetype='application/json',
|
||||
headers={'Content-Disposition': f'attachment; filename=cpus-export-{datetime.now().strftime("%Y%m%d%H%M%S")}.json'}
|
||||
)
|
||||
return response
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@cpus_bp.route('/api/cpus/import', methods=['GET', 'POST'])
|
||||
def api_import_cpus():
|
||||
"""导入CPU数据"""
|
||||
try:
|
||||
if request.method == 'GET':
|
||||
return jsonify({'endpoint': '/api/cpus/import', 'method': 'POST', 'params': {'mode': 'merge 或 replace'}})
|
||||
|
||||
import_data = request.get_json()
|
||||
if not import_data or 'items' not in import_data:
|
||||
return jsonify({'error': '缺少 items 字段'}), 400
|
||||
|
||||
mode = request.args.get('mode', 'merge')
|
||||
cpus = load_data(CPUS_FILE)
|
||||
imported_items = import_data['items']
|
||||
|
||||
result = {'success': True, 'imported': 0, 'updated': 0, 'skipped': []}
|
||||
|
||||
for item in imported_items:
|
||||
existing = next((c for c in cpus if c['id'] == item['id']), None)
|
||||
if existing:
|
||||
if mode == 'replace':
|
||||
existing.update(item)
|
||||
result['updated'] += 1
|
||||
else:
|
||||
result['skipped'].append(item['id'])
|
||||
else:
|
||||
cpus.append(item)
|
||||
result['imported'] += 1
|
||||
|
||||
save_data(CPUS_FILE, cpus)
|
||||
return jsonify(result)
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
@@ -2,8 +2,9 @@
|
||||
GPU CRUD API
|
||||
"""
|
||||
import uuid
|
||||
import json
|
||||
from datetime import datetime
|
||||
from flask import Blueprint, request, jsonify
|
||||
from flask import Blueprint, request, jsonify, Response
|
||||
from config import GPUS_FILE
|
||||
from utils import load_data, save_data, parse_date_to_timestamp
|
||||
|
||||
@@ -95,3 +96,63 @@ def api_toggle_gpu_visible(gpu_id):
|
||||
gpu['visible'] = not gpu.get('visible', True)
|
||||
save_data(GPUS_FILE, gpus)
|
||||
return jsonify({'success': True, 'visible': gpu['visible']})
|
||||
|
||||
|
||||
# ─── 导出导入 ───────────────────────────────────────────────────────
|
||||
|
||||
@gpus_bp.route('/api/gpus/export', methods=['GET'])
|
||||
def api_export_gpus():
|
||||
"""导出所有GPU数据"""
|
||||
try:
|
||||
gpus = load_data(GPUS_FILE)
|
||||
export_data = {
|
||||
'type': 'gpus',
|
||||
'items': gpus,
|
||||
'count': len(gpus),
|
||||
'export_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'version': '1.0'
|
||||
}
|
||||
json_str = json.dumps(export_data, ensure_ascii=False, indent=2)
|
||||
response = Response(
|
||||
json_str,
|
||||
mimetype='application/json',
|
||||
headers={'Content-Disposition': f'attachment; filename=gpus-export-{datetime.now().strftime("%Y%m%d%H%M%S")}.json'}
|
||||
)
|
||||
return response
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@gpus_bp.route('/api/gpus/import', methods=['GET', 'POST'])
|
||||
def api_import_gpus():
|
||||
"""导入GPU数据"""
|
||||
try:
|
||||
if request.method == 'GET':
|
||||
return jsonify({'endpoint': '/api/gpus/import', 'method': 'POST', 'params': {'mode': 'merge 或 replace'}})
|
||||
|
||||
import_data = request.get_json()
|
||||
if not import_data or 'items' not in import_data:
|
||||
return jsonify({'error': '缺少 items 字段'}), 400
|
||||
|
||||
mode = request.args.get('mode', 'merge')
|
||||
gpus = load_data(GPUS_FILE)
|
||||
imported_items = import_data['items']
|
||||
|
||||
result = {'success': True, 'imported': 0, 'updated': 0, 'skipped': []}
|
||||
|
||||
for item in imported_items:
|
||||
existing = next((g for g in gpus if g['id'] == item['id']), None)
|
||||
if existing:
|
||||
if mode == 'replace':
|
||||
existing.update(item)
|
||||
result['updated'] += 1
|
||||
else:
|
||||
result['skipped'].append(item['id'])
|
||||
else:
|
||||
gpus.append(item)
|
||||
result['imported'] += 1
|
||||
|
||||
save_data(GPUS_FILE, gpus)
|
||||
return jsonify(result)
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
@@ -2,8 +2,9 @@
|
||||
动态分类数据 API
|
||||
"""
|
||||
import uuid
|
||||
import json
|
||||
from datetime import datetime
|
||||
from flask import Blueprint, request, jsonify
|
||||
from flask import Blueprint, request, jsonify, Response
|
||||
from config import DATA_DIR
|
||||
from utils import load_data, save_data, parse_date_to_timestamp
|
||||
|
||||
@@ -95,3 +96,66 @@ def api_toggle_item_visible(category_id, item_id):
|
||||
item['visible'] = not item.get('visible', True)
|
||||
save_data(items_file, items)
|
||||
return jsonify({'success': True, 'visible': item['visible']})
|
||||
|
||||
|
||||
# ─── 导出导入 ───────────────────────────────────────────────────────
|
||||
|
||||
@items_bp.route('/api/items/<category_id>/export', methods=['GET'])
|
||||
def api_export_items(category_id):
|
||||
"""导出指定分类的所有数据"""
|
||||
try:
|
||||
items_file = DATA_DIR / f'items_{category_id}.json'
|
||||
items = load_data(items_file)
|
||||
export_data = {
|
||||
'type': 'items',
|
||||
'category_id': category_id,
|
||||
'items': items,
|
||||
'count': len(items),
|
||||
'export_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'version': '1.0'
|
||||
}
|
||||
json_str = json.dumps(export_data, ensure_ascii=False, indent=2)
|
||||
response = Response(
|
||||
json_str,
|
||||
mimetype='application/json',
|
||||
headers={'Content-Disposition': f'attachment; filename={category_id}-export-{datetime.now().strftime("%Y%m%d%H%M%S")}.json'}
|
||||
)
|
||||
return response
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@items_bp.route('/api/items/<category_id>/import', methods=['GET', 'POST'])
|
||||
def api_import_items(category_id):
|
||||
"""导入指定分类的数据"""
|
||||
try:
|
||||
if request.method == 'GET':
|
||||
return jsonify({'endpoint': f'/api/items/{category_id}/import', 'method': 'POST', 'params': {'mode': 'merge 或 replace'}})
|
||||
|
||||
import_data = request.get_json()
|
||||
if not import_data or 'items' not in import_data:
|
||||
return jsonify({'error': '缺少 items 字段'}), 400
|
||||
|
||||
mode = request.args.get('mode', 'merge')
|
||||
items_file = DATA_DIR / f'items_{category_id}.json'
|
||||
items = load_data(items_file)
|
||||
imported_items = import_data['items']
|
||||
|
||||
result = {'success': True, 'imported': 0, 'updated': 0, 'skipped': []}
|
||||
|
||||
for item in imported_items:
|
||||
existing = next((i for i in items if i['id'] == item['id']), None)
|
||||
if existing:
|
||||
if mode == 'replace':
|
||||
existing.update(item)
|
||||
result['updated'] += 1
|
||||
else:
|
||||
result['skipped'].append(item['id'])
|
||||
else:
|
||||
items.append(item)
|
||||
result['imported'] += 1
|
||||
|
||||
save_data(items_file, items)
|
||||
return jsonify(result)
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
@@ -2,8 +2,9 @@
|
||||
AI模型 CRUD API
|
||||
"""
|
||||
import uuid
|
||||
import json
|
||||
from datetime import datetime
|
||||
from flask import Blueprint, request, jsonify
|
||||
from flask import Blueprint, request, jsonify, Response
|
||||
from config import MODELS_FILE
|
||||
from utils import load_data, save_data, parse_date_to_timestamp, safe_sort_key
|
||||
|
||||
@@ -88,3 +89,63 @@ def api_toggle_model_visible(model_id):
|
||||
model['visible'] = not model.get('visible', True)
|
||||
save_data(MODELS_FILE, models)
|
||||
return jsonify({'success': True, 'visible': model['visible']})
|
||||
|
||||
|
||||
# ─── 导出导入 ───────────────────────────────────────────────────────
|
||||
|
||||
@models_bp.route('/api/models/export', methods=['GET'])
|
||||
def api_export_models():
|
||||
"""导出所有模型数据"""
|
||||
try:
|
||||
models = load_data(MODELS_FILE)
|
||||
export_data = {
|
||||
'type': 'models',
|
||||
'items': models,
|
||||
'count': len(models),
|
||||
'export_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'version': '1.0'
|
||||
}
|
||||
json_str = json.dumps(export_data, ensure_ascii=False, indent=2)
|
||||
response = Response(
|
||||
json_str,
|
||||
mimetype='application/json',
|
||||
headers={'Content-Disposition': f'attachment; filename=models-export-{datetime.now().strftime("%Y%m%d%H%M%S")}.json'}
|
||||
)
|
||||
return response
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@models_bp.route('/api/models/import', methods=['GET', 'POST'])
|
||||
def api_import_models():
|
||||
"""导入模型数据"""
|
||||
try:
|
||||
if request.method == 'GET':
|
||||
return jsonify({'endpoint': '/api/models/import', 'method': 'POST', 'params': {'mode': 'merge 或 replace'}})
|
||||
|
||||
import_data = request.get_json()
|
||||
if not import_data or 'items' not in import_data:
|
||||
return jsonify({'error': '缺少 items 字段'}), 400
|
||||
|
||||
mode = request.args.get('mode', 'merge')
|
||||
models = load_data(MODELS_FILE)
|
||||
imported_items = import_data['items']
|
||||
|
||||
result = {'success': True, 'imported': 0, 'updated': 0, 'skipped': []}
|
||||
|
||||
for item in imported_items:
|
||||
existing = next((m for m in models if m['id'] == item['id']), None)
|
||||
if existing:
|
||||
if mode == 'replace':
|
||||
existing.update(item)
|
||||
result['updated'] += 1
|
||||
else:
|
||||
result['skipped'].append(item['id'])
|
||||
else:
|
||||
models.append(item)
|
||||
result['imported'] += 1
|
||||
|
||||
save_data(MODELS_FILE, models)
|
||||
return jsonify(result)
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
+316
-1
@@ -128,7 +128,11 @@
|
||||
<section id="section-categories" class="hidden">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold text-gray-800">分类管理</h1>
|
||||
<button onclick="openAddModal('category')" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"><i class="ri-add-line mr-2"></i>添加分类</button>
|
||||
<div class="flex gap-2">
|
||||
<button onclick="exportAllCategories()" class="px-4 py-2 bg-teal-600 text-white rounded-lg hover:bg-teal-700"><i class="ri-download-line mr-2"></i>导出全部</button>
|
||||
<button onclick="openImportModal()" class="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700"><i class="ri-upload-line mr-2"></i>导入数据</button>
|
||||
<button onclick="openAddModal('category')" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"><i class="ri-add-line mr-2"></i>添加分类</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-blue-50 rounded-lg p-4 mb-4">
|
||||
<p class="text-sm text-blue-700"><i class="ri-information-line mr-1"></i>内置分类(AI模型、GPU、CPU)的子类别配置可在此编辑,其数据管理入口在左侧导航栏的独立页面。</p>
|
||||
@@ -156,6 +160,8 @@
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold text-gray-800" id="dynamic-title">数据管理</h1>
|
||||
<div class="flex gap-2">
|
||||
<button onclick="exportCategoryData(dynamicCategoryId)" class="px-4 py-2 bg-teal-600 text-white rounded-lg hover:bg-teal-700"><i class="ri-download-line mr-2"></i>导出</button>
|
||||
<button onclick="openDataImportModal(dynamicCategoryId)" class="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700"><i class="ri-upload-line mr-2"></i>导入</button>
|
||||
<button onclick="openSmartAddModal('dynamic')" class="px-4 py-2 bg-orange-600 text-white rounded-lg hover:bg-orange-700"><i class="ri-magic-line mr-2"></i>智能添加</button>
|
||||
<button onclick="openAddModal('dynamic')" class="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700"><i class="ri-add-line mr-2"></i>手动添加</button>
|
||||
</div>
|
||||
@@ -175,6 +181,8 @@
|
||||
<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="exportModels()" class="px-4 py-2 bg-teal-600 text-white rounded-lg hover:bg-teal-700"><i class="ri-download-line mr-2"></i>导出</button>
|
||||
<button onclick="openDataImportModal('models')" class="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700"><i class="ri-upload-line mr-2"></i>导入</button>
|
||||
<button onclick="openSmartAddModal('model')" class="px-4 py-2 bg-orange-600 text-white rounded-lg hover:bg-orange-700"><i class="ri-magic-line mr-2"></i>智能添加</button>
|
||||
<button onclick="openAddModal('model')" class="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700"><i class="ri-add-line mr-2"></i>手动添加</button>
|
||||
</div>
|
||||
@@ -196,6 +204,8 @@
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold text-gray-800">GPU管理</h1>
|
||||
<div class="flex gap-2">
|
||||
<button onclick="exportGpus()" class="px-4 py-2 bg-teal-600 text-white rounded-lg hover:bg-teal-700"><i class="ri-download-line mr-2"></i>导出</button>
|
||||
<button onclick="openDataImportModal('gpus')" class="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700"><i class="ri-upload-line mr-2"></i>导入</button>
|
||||
<button onclick="openSmartAddModal('gpu')" class="px-4 py-2 bg-orange-600 text-white rounded-lg hover:bg-orange-700"><i class="ri-magic-line mr-2"></i>智能添加</button>
|
||||
<button onclick="openAddModal('gpu')" class="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700"><i class="ri-add-line mr-2"></i>手动添加</button>
|
||||
</div>
|
||||
@@ -217,6 +227,8 @@
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold text-gray-800">CPU管理</h1>
|
||||
<div class="flex gap-2">
|
||||
<button onclick="exportCpus()" class="px-4 py-2 bg-teal-600 text-white rounded-lg hover:bg-teal-700"><i class="ri-download-line mr-2"></i>导出</button>
|
||||
<button onclick="openDataImportModal('cpus')" class="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700"><i class="ri-upload-line mr-2"></i>导入</button>
|
||||
<button onclick="openSmartAddModal('cpu')" class="px-4 py-2 bg-orange-600 text-white rounded-lg hover:bg-orange-700"><i class="ri-magic-line mr-2"></i>智能添加</button>
|
||||
<button onclick="openAddModal('cpu')" class="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700"><i class="ri-add-line mr-2"></i>手动添加</button>
|
||||
</div>
|
||||
@@ -493,6 +505,78 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分类导入弹窗 -->
|
||||
<div id="importModal" class="fixed inset-0 bg-black/50 z-50 hidden flex items-center justify-center">
|
||||
<div class="bg-white rounded-xl max-w-2xl w-full mx-4">
|
||||
<div class="p-6 border-b flex justify-between items-center">
|
||||
<h2 class="text-xl font-bold text-gray-800"><i class="ri-upload-line mr-2 text-purple-600"></i>导入分类配置</h2>
|
||||
<button onclick="closeImportModal()" class="text-gray-400 hover:text-gray-600"><i class="ri-close-line text-2xl"></i></button>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div class="bg-purple-50 rounded-lg p-4 mb-4">
|
||||
<p class="text-sm text-purple-700"><i class="ri-information-line mr-1"></i>导入分类配置。可选择<strong>合并模式</strong>(保留现有数据)或<strong>替换模式</strong>(覆盖现有数据)。</p>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="text-sm text-gray-600 mb-2 block">选择导入文件(JSON格式)</label>
|
||||
<input type="file" id="importFileInput" accept=".json" class="w-full px-4 py-2 border rounded-lg" onchange="handleImportFile(event)">
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-sm text-gray-600 mb-2 block">导入模式</label>
|
||||
<select id="importMode" class="w-full px-4 py-2 border rounded-lg">
|
||||
<option value="merge">合并模式 - 保留现有数据,只添加新数据</option>
|
||||
<option value="replace">替换模式 - 覆盖已存在的数据</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="importPreview" class="hidden border rounded-lg p-4 bg-gray-50">
|
||||
<h3 class="text-sm font-semibold text-gray-700 mb-3"><i class="ri-eye-line mr-1"></i>导入预览</h3>
|
||||
<div id="importPreviewContent"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-6 border-t flex justify-end gap-4">
|
||||
<button onclick="closeImportModal()" class="px-4 py-2 bg-gray-200 text-gray-600 rounded-lg hover:bg-gray-300">取消</button>
|
||||
<button onclick="doImport()" class="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700"><i class="ri-upload-line mr-1"></i>确认导入</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 数据导入弹窗 -->
|
||||
<div id="dataImportModal" class="fixed inset-0 bg-black/50 z-50 hidden flex items-center justify-center">
|
||||
<div class="bg-white rounded-xl max-w-2xl w-full mx-4">
|
||||
<div class="p-6 border-b flex justify-between items-center">
|
||||
<h2 class="text-xl font-bold text-gray-800"><i class="ri-upload-line mr-2 text-teal-600"></i>导入数据</h2>
|
||||
<button onclick="closeDataImportModal()" class="text-gray-400 hover:text-gray-600"><i class="ri-close-line text-2xl"></i></button>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div class="bg-teal-50 rounded-lg p-4 mb-4">
|
||||
<p class="text-sm text-teal-700"><i class="ri-information-line mr-1"></i>导入数据条目。可选择<strong>合并模式</strong>(保留现有数据)或<strong>替换模式</strong>(覆盖现有数据)。</p>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="text-sm text-gray-600 mb-2 block">选择导入文件(JSON格式)</label>
|
||||
<input type="file" id="dataImportFileInput" accept=".json" class="w-full px-4 py-2 border rounded-lg" onchange="handleDataImportFile(event)">
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-sm text-gray-600 mb-2 block">导入模式</label>
|
||||
<select id="dataImportMode" class="w-full px-4 py-2 border rounded-lg">
|
||||
<option value="merge">合并模式 - 保留现有数据,只添加新数据</option>
|
||||
<option value="replace">替换模式 - 覆盖已存在的数据</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="dataImportPreview" class="hidden border rounded-lg p-4 bg-gray-50">
|
||||
<h3 class="text-sm font-semibold text-gray-700 mb-3"><i class="ri-eye-line mr-1"></i>导入预览</h3>
|
||||
<div id="dataImportPreviewContent"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-6 border-t flex justify-end gap-4">
|
||||
<button onclick="closeDataImportModal()" class="px-4 py-2 bg-gray-200 text-gray-600 rounded-lg hover:bg-gray-300">取消</button>
|
||||
<button onclick="doDataImport()" class="px-4 py-2 bg-teal-600 text-white rounded-lg hover:bg-teal-700"><i class="ri-upload-line mr-1"></i>确认导入</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentType = '';
|
||||
let currentId = '';
|
||||
@@ -940,6 +1024,7 @@
|
||||
${subcatCount > 0 ? `<span class="px-2 py-1 bg-green-100 text-green-600 rounded text-xs">${subcatCount} 个</span>` : '<span class="text-gray-400">无</span>'}
|
||||
</td>
|
||||
<td class="px-4 py-3 text-center">
|
||||
<button onclick="exportSingleCategory('${c.id}')" class="text-teal-600 hover:text-teal-800 mr-2" title="导出"><i class="ri-download-line"></i></button>
|
||||
<button onclick="editItem('category', '${c.id}')" class="text-blue-600 hover:text-blue-800 mr-2" title="编辑"><i class="ri-edit-line"></i></button>
|
||||
${!isBuiltin ? `<button onclick="deleteItem('category', '${c.id}')" class="text-red-600 hover:text-red-800" title="删除"><i class="ri-delete-bin-line"></i></button>` : '<span class="text-gray-300 cursor-not-allowed"><i class="ri-delete-bin-line"></i></span>'}
|
||||
</td>
|
||||
@@ -2678,6 +2763,236 @@
|
||||
// 监听编辑弹框打开,显示智能补充按钮
|
||||
document.getElementById('editModal').addEventListener('showSmartUpdate', showSmartUpdateButton);
|
||||
|
||||
// ─── 分类导出导入功能 ─────────────────────────────────────────────────
|
||||
|
||||
// 导出所有分类
|
||||
async function exportAllCategories() {
|
||||
try {
|
||||
const res = await fetch('/api/categories/export');
|
||||
if (!res.ok) { const err = await res.json(); alert('导出失败: ' + err.error); return; }
|
||||
const blob = await res.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `categories-${new Date().toISOString().slice(0,19).replace(/[:-]/g,'')}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
a.remove();
|
||||
} catch (e) { alert('导出失败: ' + e.message); }
|
||||
}
|
||||
|
||||
// 导出单个分类配置
|
||||
async function exportSingleCategory(categoryId) {
|
||||
try {
|
||||
const res = await fetch(`/api/categories/export/${categoryId}`);
|
||||
if (!res.ok) { const err = await res.json(); alert('导出失败: ' + err.error); return; }
|
||||
const blob = await res.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${categoryId}-${new Date().toISOString().slice(0,19).replace(/[:-]/g,'')}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
a.remove();
|
||||
} catch (e) { alert('导出失败: ' + e.message); }
|
||||
}
|
||||
|
||||
// 打开分类导入弹窗
|
||||
function openImportModal() {
|
||||
document.getElementById('importModal').classList.remove('hidden');
|
||||
document.getElementById('importPreview').classList.add('hidden');
|
||||
document.getElementById('importFileInput').value = '';
|
||||
importData = null;
|
||||
}
|
||||
|
||||
function closeImportModal() { document.getElementById('importModal').classList.add('hidden'); }
|
||||
|
||||
let importData = null;
|
||||
function handleImportFile(event) {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => { try { importData = JSON.parse(e.target.result); showImportPreview(); } catch (err) { alert('JSON 解析失败: ' + err.message); } };
|
||||
reader.readAsText(file);
|
||||
}
|
||||
|
||||
function showImportPreview() {
|
||||
if (!importData) return;
|
||||
const preview = document.getElementById('importPreview');
|
||||
const content = document.getElementById('importPreviewContent');
|
||||
preview.classList.remove('hidden');
|
||||
let html = '<div class="space-y-2">';
|
||||
if (importData.categories) {
|
||||
html += `<div class="font-medium text-gray-800"><i class="ri-folder-line mr-1"></i>包含 ${importData.categories.length} 个分类</div>`;
|
||||
html += '<div class="flex flex-wrap gap-2 ml-5">';
|
||||
importData.categories.forEach(cat => { html += `<span class="px-2 py-1 bg-blue-100 text-blue-700 rounded text-xs">${cat.name}</span>`; });
|
||||
html += '</div>';
|
||||
} else if (importData.category) {
|
||||
html += `<div class="font-medium text-gray-800"><i class="ri-folder-line mr-1"></i>单个分类: ${importData.category.name}</div>`;
|
||||
}
|
||||
html += `<div class="mt-3 text-sm text-gray-500">导出时间: ${importData.export_time || '未知'}</div>`;
|
||||
html += '</div>';content.innerHTML = html;
|
||||
}
|
||||
|
||||
async function doImport() {
|
||||
if (!importData) { alert('请先选择导入文件'); return; }
|
||||
const mode = document.getElementById('importMode').value;
|
||||
try {
|
||||
let endpoint = importData.category ? `/api/categories/import/${importData.category.id}?mode=${mode}` : `/api/categories/import?mode=${mode}`;
|
||||
const res = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(importData) });
|
||||
const result = await res.json();
|
||||
if (result.error) { alert('导入失败: ' + result.error); } else {
|
||||
let msg = '导入成功!\n新增: ' + (result.imported||0) + '\n更新: ' + (result.updated||0);
|
||||
if (result.skipped?.length) msg += '\n跳过: ' + result.skipped.join(', ');
|
||||
alert(msg);
|
||||
closeImportModal();
|
||||
await loadCategories(); renderSidebar(); loadAdminCategories();
|
||||
}
|
||||
} catch (e) { alert('导入失败: ' + e.message); }
|
||||
}
|
||||
|
||||
// ─── 数据导出导入功能 ─────────────────────────────────────────────────
|
||||
|
||||
let dataImportType = null;
|
||||
let dataImportCategoryId = null;
|
||||
let dataImportData = null;
|
||||
|
||||
// 导出模型
|
||||
async function exportModels() {
|
||||
try {
|
||||
const res = await fetch('/api/models/export');
|
||||
if (!res.ok) { const err = await res.json(); alert('导出失败: ' + err.error); return; }
|
||||
const blob = await res.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `models-${new Date().toISOString().slice(0,19).replace(/[:-]/g,'')}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
a.remove();
|
||||
} catch (e) { alert('导出失败: ' + e.message); }
|
||||
}
|
||||
|
||||
// 导出GPU
|
||||
async function exportGpus() {
|
||||
try {
|
||||
const res = await fetch('/api/gpus/export');
|
||||
if (!res.ok) { const err = await res.json(); alert('导出失败: ' + err.error); return; }
|
||||
const blob = await res.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `gpus-${new Date().toISOString().slice(0,19).replace(/[:-]/g,'')}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
a.remove();
|
||||
} catch (e) { alert('导出失败: ' + e.message); }
|
||||
}
|
||||
|
||||
// 导出CPU
|
||||
async function exportCpus() {
|
||||
try {
|
||||
const res = await fetch('/api/cpus/export');
|
||||
if (!res.ok) { const err = await res.json(); alert('导出失败: ' + err.error); return; }
|
||||
const blob = await res.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `cpus-${new Date().toISOString().slice(0,19).replace(/[:-]/g,'')}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
a.remove();
|
||||
} catch (e) { alert('导出失败: ' + e.message); }
|
||||
}
|
||||
|
||||
// 导出动态分类数据
|
||||
async function exportCategoryData(categoryId) {
|
||||
if (!categoryId) { alert('未选择分类'); return; }
|
||||
try {
|
||||
const res = await fetch(`/api/items/${categoryId}/export`);
|
||||
if (!res.ok) { const err = await res.json(); alert('导出失败: ' + err.error); return; }
|
||||
const blob = await res.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${categoryId}-${new Date().toISOString().slice(0,19).replace(/[:-]/g,'')}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
a.remove();
|
||||
} catch (e) { alert('导出失败: ' + e.message); }
|
||||
}
|
||||
|
||||
// 打开数据导入弹窗
|
||||
function openDataImportModal(typeOrCategoryId) {
|
||||
dataImportType = typeOrCategoryId;
|
||||
dataImportCategoryId = null;
|
||||
// 如果是动态分类,需要区分类型
|
||||
if (typeOrCategoryId && !['models', 'gpus', 'cpus'].includes(typeOrCategoryId)) {
|
||||
dataImportCategoryId = typeOrCategoryId;
|
||||
dataImportType = 'items';
|
||||
}
|
||||
document.getElementById('dataImportModal').classList.remove('hidden');
|
||||
document.getElementById('dataImportPreview').classList.add('hidden');
|
||||
document.getElementById('dataImportFileInput').value = '';
|
||||
dataImportData = null;
|
||||
}
|
||||
|
||||
function closeDataImportModal() { document.getElementById('dataImportModal').classList.add('hidden'); }
|
||||
|
||||
function handleDataImportFile(event) {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => { try { dataImportData = JSON.parse(e.target.result); showDataImportPreview(); } catch (err) { alert('JSON 解析失败: ' + err.message); } };
|
||||
reader.readAsText(file);
|
||||
}
|
||||
|
||||
function showDataImportPreview() {
|
||||
if (!dataImportData) return;
|
||||
const preview = document.getElementById('dataImportPreview');
|
||||
const content = document.getElementById('dataImportPreviewContent');
|
||||
preview.classList.remove('hidden');
|
||||
let html = '<div class="space-y-2">';
|
||||
html += `<div class="font-medium text-gray-800"><i class="ri-database-line mr-1"></i>数据类型: ${dataImportData.type || '未知'}</div>`;
|
||||
html += `<div class="text-sm text-gray-600 ml-5">包含 ${dataImportData.count || dataImportData.items?.length || 0} 条数据</div>`;
|
||||
html += `<div class="mt-3 text-sm text-gray-500">导出时间: ${dataImportData.export_time || '未知'}</div>`;
|
||||
html += '</div>';content.innerHTML = html;
|
||||
}
|
||||
|
||||
async function doDataImport() {
|
||||
if (!dataImportData) { alert('请先选择导入文件'); return; }
|
||||
const mode = document.getElementById('dataImportMode').value;
|
||||
try {
|
||||
let endpoint;
|
||||
if (dataImportType === 'models') endpoint = '/api/models/import?mode=' + mode;
|
||||
else if (dataImportType === 'gpus') endpoint = '/api/gpus/import?mode=' + mode;
|
||||
else if (dataImportType === 'cpus') endpoint = '/api/cpus/import?mode=' + mode;
|
||||
else if (dataImportCategoryId) endpoint = `/api/items/${dataImportCategoryId}/import?mode=` + mode;
|
||||
else { alert('未知数据类型'); return; }
|
||||
|
||||
const res = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(dataImportData) });
|
||||
const result = await res.json();
|
||||
if (result.error) { alert('导入失败: ' + result.error); } else {
|
||||
let msg = '导入成功!\n新增: ' + (result.imported||0) + '\n更新: ' + (result.updated||0);
|
||||
if (result.skipped?.length) msg += '\n跳过: ' + result.skipped.length + ' 条';
|
||||
alert(msg);
|
||||
closeDataImportModal();
|
||||
// 刷新当前页面数据
|
||||
if (dataImportType === 'models') loadAdminModels();
|
||||
else if (dataImportType === 'gpus') loadAdminGpus();
|
||||
else if (dataImportType === 'cpus') loadAdminCpus();
|
||||
else if (dataImportCategoryId) loadDynamicCategoryData(dataImportCategoryId);
|
||||
loadOverview();
|
||||
}
|
||||
} catch (e) { alert('导入失败: ' + e.message); }
|
||||
}
|
||||
|
||||
init();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user