Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f83bf11669 | ||
|
|
bc2720257d |
@@ -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 -
|
|
||||||
Binary file not shown.
@@ -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
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user