2 Commits
4 changed files with 39 additions and 176 deletions
-1
View File
@@ -13,4 +13,3 @@ WARNING: This is a development server. Do not use it in a production deployment.
* Running on http://127.0.0.1:16041 * Running on http://127.0.0.1:16041
* Running on http://192.168.0.101:16041 * Running on http://192.168.0.101:16041
Press CTRL+C to quit Press CTRL+C to quit
127.0.0.1 - - [10/Jul/2026 23:59:48] "GET /api/categories/export HTTP/1.1" 200 -
+33 -147
View File
@@ -5,8 +5,8 @@ import uuid
import json import json
from datetime import datetime from datetime import datetime
from flask import Blueprint, request, jsonify, Response from flask import Blueprint, request, jsonify, Response
from config import CATEGORIES_FILE, DATA_DIR from config import CATEGORIES_FILE
from utils import load_data, save_data, load_config from utils import load_data, save_data
cat_bp = Blueprint('api_categories', __name__) cat_bp = Blueprint('api_categories', __name__)
@@ -73,54 +73,27 @@ def api_toggle_category_visible(category_id):
return jsonify({'success': True, 'visible': category['visible']}) return jsonify({'success': True, 'visible': category['visible']})
# ─── 导出分类相关数据 ─────────────────────────────────────────────────────── # ─── 导出分类数据 ───────────────────────────────────────────────────────
@cat_bp.route('/api/categories/export', methods=['GET']) @cat_bp.route('/api/categories/export', methods=['GET'])
def api_export_categories(): def api_export_categories():
"""导出所有分类及其相关数据(子类别、参数字段、关联数据项)""" """导出所有分类配置(仅分类本身,不含数据项)"""
try: try:
# 获取所有分类
categories = load_data(CATEGORIES_FILE) categories = load_data(CATEGORIES_FILE)
# 获取各类别下的数据
export_data = { export_data = {
'categories': categories, 'categories': categories,
'export_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), 'export_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
'version': '1.0' 'version': '1.0'
} }
# 加载各类别数据文件
# 内置分类:AI模型、GPU、CPU
models_file = DATA_DIR / 'models.json'
gpus_file = DATA_DIR / 'gpus.json'
cpus_file = DATA_DIR / 'cpus.json'
if models_file.exists():
export_data['models'] = load_data(models_file)
if gpus_file.exists():
export_data['gpus'] = load_data(gpus_file)
if cpus_file.exists():
export_data['cpus'] = load_data(cpus_file)
# 动态分类数据(每个自定义分类有独立的数据文件)
for cat in categories:
cat_id = cat['id']
# 排除内置分类
if cat_id in ['ai-models', 'gpus', 'cpus']:
continue
# 查找该分类的数据文件
cat_data_file = DATA_DIR / f'{cat_id}.json'
if cat_data_file.exists():
export_data[cat_id] = load_data(cat_data_file)
# 导出为 JSON 文件
json_str = json.dumps(export_data, ensure_ascii=False, indent=2) json_str = json.dumps(export_data, ensure_ascii=False, indent=2)
response = Response( response = Response(
json_str, json_str,
mimetype='application/json', mimetype='application/json',
headers={ headers={
'Content-Disposition': f'attachment; filename=param-hub-categories-export-{datetime.now().strftime("%Y%m%d%H%M%S")}.json' 'Content-Disposition': f'attachment; filename=param-hub-categories-{datetime.now().strftime("%Y%m%d%H%M%S")}.json'
} }
) )
return response return response
@@ -130,7 +103,7 @@ def api_export_categories():
@cat_bp.route('/api/categories/export/<category_id>', methods=['GET']) @cat_bp.route('/api/categories/export/<category_id>', methods=['GET'])
def api_export_single_category(category_id): def api_export_single_category(category_id):
"""导出单个分类及其相关数据""" """导出单个分类配置"""
try: try:
categories = load_data(CATEGORIES_FILE) categories = load_data(CATEGORIES_FILE)
category = next((c for c in categories if c['id'] == category_id), None) category = next((c for c in categories if c['id'] == category_id), None)
@@ -143,32 +116,13 @@ def api_export_single_category(category_id):
'version': '1.0' 'version': '1.0'
} }
# 加载该分类的数据
if category_id == 'ai-models':
models_file = DATA_DIR / 'models.json'
if models_file.exists():
export_data['items'] = load_data(models_file)
elif category_id == 'gpus':
gpus_file = DATA_DIR / 'gpus.json'
if gpus_file.exists():
export_data['items'] = load_data(gpus_file)
elif category_id == 'cpus':
cpus_file = DATA_DIR / 'cpus.json'
if cpus_file.exists():
export_data['items'] = load_data(cpus_file)
else:
# 动态分类
cat_data_file = DATA_DIR / f'{category_id}.json'
if cat_data_file.exists():
export_data['items'] = load_data(cat_data_file)
json_str = json.dumps(export_data, ensure_ascii=False, indent=2) json_str = json.dumps(export_data, ensure_ascii=False, indent=2)
response = Response( response = Response(
json_str, json_str,
mimetype='application/json', mimetype='application/json',
headers={ headers={
'Content-Disposition': f'attachment; filename={category_id}-export-{datetime.now().strftime("%Y%m%d%H%M%S")}.json' 'Content-Disposition': f'attachment; filename={category_id}-{datetime.now().strftime("%Y%m%d%H%M%S")}.json'
} }
) )
return response return response
@@ -176,33 +130,42 @@ def api_export_single_category(category_id):
return jsonify({'error': str(e)}), 500 return jsonify({'error': str(e)}), 500
# ─── 导入分类相关数据 ─────────────────────────────────────────────────────── # ─── 导入分类数据 ───────────────────────────────────────────────────────
@cat_bp.route('/api/categories/import', methods=['POST']) @cat_bp.route('/api/categories/import', methods=['GET', 'POST'])
def api_import_categories(): def api_import_categories():
"""导入分类及其相关数据""" """导入分类配置(仅分类本身,不含数据项)"""
try: try:
if request.method == 'GET':
# 返回导入接口说明
return jsonify({
'endpoint': '/api/categories/import',
'method': 'POST',
'params': {
'mode': 'merge(合并)或 replace(替换)'
},
'body': {
'categories': '分类数组'
}
})
import_data = request.get_json() import_data = request.get_json()
if not import_data: if not import_data:
return jsonify({'error': '无导入数据'}), 400 return jsonify({'error': '无导入数据'}), 400
# 验证数据结构
if 'categories' not in import_data: if 'categories' not in import_data:
return jsonify({'error': '缺少 categories 字段'}), 400 return jsonify({'error': '缺少 categories 字段'}), 400
# 导入模式:merge(合并)或 replace(替换)
mode = request.args.get('mode', 'merge') mode = request.args.get('mode', 'merge')
result = { result = {
'success': True, 'success': True,
'imported_categories': 0, 'imported': 0,
'imported_items': 0, 'updated': 0,
'skipped': [], 'skipped': []
'updated': []
} }
# 处理分类
existing_categories = load_data(CATEGORIES_FILE) existing_categories = load_data(CATEGORIES_FILE)
imported_categories = import_data['categories'] imported_categories = import_data['categories']
@@ -210,68 +173,16 @@ def api_import_categories():
existing = next((c for c in existing_categories if c['id'] == cat['id']), None) existing = next((c for c in existing_categories if c['id'] == cat['id']), None)
if existing: if existing:
if mode == 'replace': if mode == 'replace':
# 替换模式:更新现有分类
existing.update(cat) existing.update(cat)
result['updated'].append(cat['id']) result['updated'] += 1
else: else:
# 合并模式:跳过已存在的分类
result['skipped'].append(cat['id']) result['skipped'].append(cat['id'])
else: else:
# 新分类:添加
existing_categories.append(cat) existing_categories.append(cat)
result['imported_categories'] += 1 result['imported'] += 1
save_data(CATEGORIES_FILE, existing_categories) save_data(CATEGORIES_FILE, existing_categories)
# 导入各类别数据
data_mappings = {
'models': 'models.json',
'gpus': 'gpus.json',
'cpus': 'cpus.json'
}
for key, filename in data_mappings.items():
if key in import_data:
data_file = DATA_DIR / filename
existing_items = load_data(data_file) if data_file.exists() else []
imported_items = import_data[key]
for item in imported_items:
existing = next((i for i in existing_items if i['id'] == item['id']), None)
if existing:
if mode == 'replace':
existing.update(item)
else:
continue # 合并模式跳过
else:
existing_items.append(item)
result['imported_items'] += 1
save_data(data_file, existing_items)
# 导入动态分类数据
for cat in imported_categories:
cat_id = cat['id']
if cat_id in ['ai-models', 'gpus', 'cpus']:
continue
if cat_id in import_data:
data_file = DATA_DIR / f'{cat_id}.json'
existing_items = load_data(data_file) if data_file.exists() else []
imported_items = import_data[cat_id]
for item in imported_items:
existing = next((i for i in existing_items if i['id'] == item['id']), None)
if existing:
if mode == 'replace':
existing.update(item)
else:
continue
else:
existing_items.append(item)
result['imported_items'] += 1
save_data(data_file, existing_items)
return jsonify(result) return jsonify(result)
except Exception as e: except Exception as e:
return jsonify({'error': str(e)}), 500 return jsonify({'error': str(e)}), 500
@@ -279,7 +190,7 @@ def api_import_categories():
@cat_bp.route('/api/categories/import/<category_id>', methods=['POST']) @cat_bp.route('/api/categories/import/<category_id>', methods=['POST'])
def api_import_single_category(category_id): def api_import_single_category(category_id):
"""导入单个分类数据""" """导入单个分类配置"""
try: try:
import_data = request.get_json() import_data = request.get_json()
@@ -293,11 +204,10 @@ def api_import_single_category(category_id):
result = { result = {
'success': True, 'success': True,
'imported_items': 0, 'imported': False,
'updated_items': 0 'updated': False
} }
# 处理分类
categories = load_data(CATEGORIES_FILE) categories = load_data(CATEGORIES_FILE)
imported_cat = import_data['category'] imported_cat = import_data['category']
@@ -305,37 +215,13 @@ def api_import_single_category(category_id):
if existing: if existing:
if mode == 'replace': if mode == 'replace':
existing.update(imported_cat) existing.update(imported_cat)
result['updated'] = True
else: else:
categories.append(imported_cat) categories.append(imported_cat)
result['imported'] = True
save_data(CATEGORIES_FILE, categories) save_data(CATEGORIES_FILE, categories)
# 处理数据项
if 'items' in import_data:
if category_id == 'ai-models':
data_file = DATA_DIR / 'models.json'
elif category_id == 'gpus':
data_file = DATA_DIR / 'gpus.json'
elif category_id == 'cpus':
data_file = DATA_DIR / 'cpus.json'
else:
data_file = DATA_DIR / f'{category_id}.json'
existing_items = load_data(data_file) if data_file.exists() else []
imported_items = import_data['items']
for item in imported_items:
existing = next((i for i in existing_items if i['id'] == item['id']), None)
if existing:
if mode == 'replace':
existing.update(item)
result['updated_items'] += 1
else:
existing_items.append(item)
result['imported_items'] += 1
save_data(data_file, existing_items)
return jsonify(result) return jsonify(result)
except Exception as e: except Exception as e:
return jsonify({'error': str(e)}), 500 return jsonify({'error': str(e)}), 500
+5 -27
View File
@@ -2823,31 +2823,11 @@
importData.categories.forEach(cat => { importData.categories.forEach(cat => {
html += `<span class="px-2 py-1 bg-blue-100 text-blue-700 rounded text-xs">${cat.name}</span>`; html += `<span class="px-2 py-1 bg-blue-100 text-blue-700 rounded text-xs">${cat.name}</span>`;
}); });
html += '</div>';} html += '</div>';
} else if (importData.category) { } 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="font-medium text-gray-800"><i class="ri-folder-line mr-1"></i>单个分类: ${importData.category.name}</div>`;
} }
// 数据项
const itemTypes = ['models', 'gpus', 'cpus', 'items'];
let totalItems = 0;
itemTypes.forEach(type => {
if (importData[type] && importData[type].length > 0) {
totalItems += importData[type].length;
html += `<div class="text-sm text-gray-600 ml-5"><i class="ri-database-line mr-1"></i>${type}: ${importData[type].length} 条</div>`;
}
});
// 动态分类数据
const dynamicCats = Object.keys(importData).filter(k =>
k !== 'categories' && k !== 'category' && k !== 'export_time' && k !== 'version' &&
!itemTypes.includes(k) && Array.isArray(importData[k])
);
dynamicCats.forEach(catId => {
totalItems += importData[catId].length;
html += `<div class="text-sm text-gray-600 ml-5"><i class="ri-database-line mr-1"></i>${catId}: ${importData[catId].length} 条</div>`;
});
html += `<div class="mt-3 text-sm text-gray-500">导出时间: ${importData.export_time || '未知'}</div>`; html += `<div class="mt-3 text-sm text-gray-500">导出时间: ${importData.export_time || '未知'}</div>`;
html += '</div>';content.innerHTML = html; html += '</div>';content.innerHTML = html;
} }
@@ -2860,15 +2840,14 @@
} }
const mode = document.getElementById('importMode').value; const mode = document.getElementById('importMode').value;
const categoryId = document.getElementById('importCategoryId').value;
try { try {
let endpoint; let endpoint;
if (categoryId && importData.category) { if (importData.category) {
// 导入单个分类 // 导入单个分类
endpoint = `/api/categories/import/${categoryId}?mode=${mode}`; endpoint = `/api/categories/import/${importData.category.id}?mode=${mode}`;
} else { } else {
// 导入全部 // 导入全部分类
endpoint = `/api/categories/import?mode=${mode}`; endpoint = `/api/categories/import?mode=${mode}`;
} }
@@ -2884,7 +2863,7 @@
alert('导入失败: ' + result.error); alert('导入失败: ' + result.error);
} else { } else {
let msg = '导入成功!\n'; let msg = '导入成功!\n';
msg += `新增分类: ${result.imported_categories || 0}\n`;msg += `新增数据: ${result.imported_items || 0}\n`;msg += `更新数据: ${result.updated_items || 0}`; msg += `新增分类: ${result.imported || 0}\n`;msg += `更新分类: ${result.updated || 0}`;
if (result.skipped && result.skipped.length > 0) { if (result.skipped && result.skipped.length > 0) {
msg += `\n跳过已存在分类: ${result.skipped.join(', ')}`; msg += `\n跳过已存在分类: ${result.skipped.join(', ')}`;
} }
@@ -2894,7 +2873,6 @@
await loadCategories(); await loadCategories();
renderSidebar(); renderSidebar();
loadAdminCategories(); loadAdminCategories();
loadOverview();
} }
} catch (e) { } catch (e) {
alert('导入失败: ' + e.message); alert('导入失败: ' + e.message);