1 Commits
Author SHA1 Message Date
hz4th_coder 01abe7f86f 为各类别添加数据导入导出功能 2026-07-11 00:34:47 +08:00
10 changed files with 457 additions and 95 deletions
+1
View File
@@ -13,3 +13,4 @@ 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 - - [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.
+62 -1
View File
@@ -2,8 +2,9 @@
CPU CRUD API CPU CRUD API
""" """
import uuid import uuid
import json
from datetime import datetime from datetime import datetime
from flask import Blueprint, request, jsonify from flask import Blueprint, request, jsonify, Response
from config import CPUS_FILE from config import CPUS_FILE
from utils import load_data, save_data, parse_date_to_timestamp 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) cpu['visible'] = not cpu.get('visible', True)
save_data(CPUS_FILE, cpus) save_data(CPUS_FILE, cpus)
return jsonify({'success': True, 'visible': cpu['visible']}) 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
+62 -1
View File
@@ -2,8 +2,9 @@
GPU CRUD API GPU CRUD API
""" """
import uuid import uuid
import json
from datetime import datetime from datetime import datetime
from flask import Blueprint, request, jsonify from flask import Blueprint, request, jsonify, Response
from config import GPUS_FILE from config import GPUS_FILE
from utils import load_data, save_data, parse_date_to_timestamp 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) gpu['visible'] = not gpu.get('visible', True)
save_data(GPUS_FILE, gpus) save_data(GPUS_FILE, gpus)
return jsonify({'success': True, 'visible': gpu['visible']}) 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
+65 -1
View File
@@ -2,8 +2,9 @@
动态分类数据 API 动态分类数据 API
""" """
import uuid import uuid
import json
from datetime import datetime from datetime import datetime
from flask import Blueprint, request, jsonify from flask import Blueprint, request, jsonify, Response
from config import DATA_DIR from config import DATA_DIR
from utils import load_data, save_data, parse_date_to_timestamp 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) item['visible'] = not item.get('visible', True)
save_data(items_file, items) save_data(items_file, items)
return jsonify({'success': True, 'visible': item['visible']}) 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
+62 -1
View File
@@ -2,8 +2,9 @@
AI模型 CRUD API AI模型 CRUD API
""" """
import uuid import uuid
import json
from datetime import datetime from datetime import datetime
from flask import Blueprint, request, jsonify from flask import Blueprint, request, jsonify, Response
from config import MODELS_FILE from config import MODELS_FILE
from utils import load_data, save_data, parse_date_to_timestamp, safe_sort_key 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) model['visible'] = not model.get('visible', True)
save_data(MODELS_FILE, models) save_data(MODELS_FILE, models)
return jsonify({'success': True, 'visible': model['visible']}) 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
+205 -91
View File
@@ -160,6 +160,8 @@
<div class="flex justify-between items-center mb-6"> <div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-800" id="dynamic-title">数据管理</h1> <h1 class="text-2xl font-bold text-gray-800" id="dynamic-title">数据管理</h1>
<div class="flex gap-2"> <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="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> <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> </div>
@@ -179,6 +181,8 @@
<div class="flex justify-between items-center mb-6"> <div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-800">大模型管理</h1> <h1 class="text-2xl font-bold text-gray-800">大模型管理</h1>
<div class="flex gap-2"> <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="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> <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> </div>
@@ -200,6 +204,8 @@
<div class="flex justify-between items-center mb-6"> <div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-800">GPU管理</h1> <h1 class="text-2xl font-bold text-gray-800">GPU管理</h1>
<div class="flex gap-2"> <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="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> <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> </div>
@@ -221,6 +227,8 @@
<div class="flex justify-between items-center mb-6"> <div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-800">CPU管理</h1> <h1 class="text-2xl font-bold text-gray-800">CPU管理</h1>
<div class="flex gap-2"> <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="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> <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> </div>
@@ -501,20 +509,18 @@
<div id="importModal" class="fixed inset-0 bg-black/50 z-50 hidden flex items-center justify-center"> <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="bg-white rounded-xl max-w-2xl w-full mx-4">
<div class="p-6 border-b flex justify-between items-center"> <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> <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> <button onclick="closeImportModal()" class="text-gray-400 hover:text-gray-600"><i class="ri-close-line text-2xl"></i></button>
</div> </div>
<div class="p-6"> <div class="p-6">
<div class="bg-purple-50 rounded-lg p-4 mb-4"> <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> <p class="text-sm text-purple-700"><i class="ri-information-line mr-1"></i>导入分类配置。可选择<strong>合并模式</strong>(保留现有数据)或<strong>替换模式</strong>(覆盖现有数据)。</p>
</div> </div>
<div class="space-y-4"> <div class="space-y-4">
<div> <div>
<label class="text-sm text-gray-600 mb-2 block">选择导入文件(JSON格式)</label> <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)"> <input type="file" id="importFileInput" accept=".json" class="w-full px-4 py-2 border rounded-lg" onchange="handleImportFile(event)">
</div> </div>
<div> <div>
<label class="text-sm text-gray-600 mb-2 block">导入模式</label> <label class="text-sm text-gray-600 mb-2 block">导入模式</label>
<select id="importMode" class="w-full px-4 py-2 border rounded-lg"> <select id="importMode" class="w-full px-4 py-2 border rounded-lg">
@@ -522,8 +528,6 @@
<option value="replace">替换模式 - 覆盖已存在的数据</option> <option value="replace">替换模式 - 覆盖已存在的数据</option>
</select> </select>
</div> </div>
<!-- 预览区域 -->
<div id="importPreview" class="hidden border rounded-lg p-4 bg-gray-50"> <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> <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 id="importPreviewContent"></div>
@@ -537,6 +541,42 @@
</div> </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> <script>
let currentType = ''; let currentType = '';
let currentId = ''; let currentId = '';
@@ -2729,154 +2769,228 @@
async function exportAllCategories() { async function exportAllCategories() {
try { try {
const res = await fetch('/api/categories/export'); const res = await fetch('/api/categories/export');
if (!res.ok) { if (!res.ok) { const err = await res.json(); alert('导出失败: ' + err.error); return; }
const err = await res.json();
alert('导出失败: ' + err.error);
return;
}
// 获取文件并下载
const blob = await res.blob(); const blob = await res.blob();
const url = window.URL.createObjectURL(blob); const url = window.URL.createObjectURL(blob);
const a = document.createElement('a'); const a = document.createElement('a');
a.href = url; a.href = url;
a.download = `param-hub-categories-export-${new Date().toISOString().slice(0,19).replace(/[:-]/g,'')}.json`; a.download = `categories-${new Date().toISOString().slice(0,19).replace(/[:-]/g,'')}.json`;
document.body.appendChild(a); document.body.appendChild(a);
a.click(); a.click();
window.URL.revokeObjectURL(url); window.URL.revokeObjectURL(url);
a.remove(); a.remove();
} catch (e) { } catch (e) { alert('导出失败: ' + e.message); }
alert('导出失败: ' + e.message);
}
} }
// 导出单个分类 // 导出单个分类配置
async function exportSingleCategory(categoryId) { async function exportSingleCategory(categoryId) {
try { try {
const res = await fetch(`/api/categories/export/${categoryId}`); const res = await fetch(`/api/categories/export/${categoryId}`);
if (!res.ok) { if (!res.ok) { const err = await res.json(); alert('导出失败: ' + err.error); return; }
const err = await res.json();
alert('导出失败: ' + err.error);
return;
}
const blob = await res.blob(); const blob = await res.blob();
const url = window.URL.createObjectURL(blob); const url = window.URL.createObjectURL(blob);
const a = document.createElement('a'); const a = document.createElement('a');
a.href = url; a.href = url;
a.download = `${categoryId}-export-${new Date().toISOString().slice(0,19).replace(/[:-]/g,'')}.json`; a.download = `${categoryId}-${new Date().toISOString().slice(0,19).replace(/[:-]/g,'')}.json`;
document.body.appendChild(a); document.body.appendChild(a);
a.click(); a.click();
window.URL.revokeObjectURL(url); window.URL.revokeObjectURL(url);
a.remove(); a.remove();
} catch (e) { } catch (e) { alert('导出失败: ' + e.message); }
alert('导出失败: ' + e.message);
}
} }
// 打开导入弹窗 // 打开分类导入弹窗
function openImportModal() { function openImportModal() {
document.getElementById('importModal').classList.remove('hidden'); document.getElementById('importModal').classList.remove('hidden');
document.getElementById('importPreview').classList.add('hidden'); document.getElementById('importPreview').classList.add('hidden');
document.getElementById('importFileInput').value = ''; document.getElementById('importFileInput').value = '';
importData = null;
} }
// 关闭导入弹窗 function closeImportModal() { document.getElementById('importModal').classList.add('hidden'); }
function closeImportModal() {
document.getElementById('importModal').classList.add('hidden');
}
// 处理导入文件选择
let importData = null; let importData = null;
function handleImportFile(event) { function handleImportFile(event) {
const file = event.target.files[0]; const file = event.target.files[0];
if (!file) return; if (!file) return;
const reader = new FileReader(); const reader = new FileReader();
reader.onload = (e) => { reader.onload = (e) => { try { importData = JSON.parse(e.target.result); showImportPreview(); } catch (err) { alert('JSON 解析失败: ' + err.message); } };
try {
importData = JSON.parse(e.target.result);
showImportPreview();
} catch (err) {
alert('JSON 解析失败: ' + err.message);
}
};
reader.readAsText(file); reader.readAsText(file);
} }
// 显示导入预览
function showImportPreview() { function showImportPreview() {
if (!importData) return; if (!importData) return;
const preview = document.getElementById('importPreview'); const preview = document.getElementById('importPreview');
const content = document.getElementById('importPreviewContent'); const content = document.getElementById('importPreviewContent');
preview.classList.remove('hidden'); preview.classList.remove('hidden');
let html = '<div class="space-y-2">'; let html = '<div class="space-y-2">';
// 分类信息
if (importData.categories) { 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="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">'; html += '<div class="flex flex-wrap gap-2 ml-5">';
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>`;
} }
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;
} }
// 执行导入
async function doImport() { async function doImport() {
if (!importData) { if (!importData) { alert('请先选择导入文件'); return; }
alert('请先选择导入文件');
return;
}
const mode = document.getElementById('importMode').value; 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 { try {
let endpoint; let endpoint;
if (importData.category) { if (dataImportType === 'models') endpoint = '/api/models/import?mode=' + mode;
// 导入单个分类 else if (dataImportType === 'gpus') endpoint = '/api/gpus/import?mode=' + mode;
endpoint = `/api/categories/import/${importData.category.id}?mode=${mode}`; else if (dataImportType === 'cpus') endpoint = '/api/cpus/import?mode=' + mode;
} else { else if (dataImportCategoryId) endpoint = `/api/items/${dataImportCategoryId}/import?mode=` + mode;
// 导入全部分类 else { alert('未知数据类型'); return; }
endpoint = `/api/categories/import?mode=${mode}`;
}
const res = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(importData)
});
const res = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(dataImportData) });
const result = await res.json(); const result = await res.json();
if (result.error) { alert('导入失败: ' + result.error); } else {
if (result.error) { let msg = '导入成功!\n新增: ' + (result.imported||0) + '\n更新: ' + (result.updated||0);
alert('导入失败: ' + result.error); if (result.skipped?.length) msg += '\n跳过: ' + result.skipped.length + ' 条';
} else {
let msg = '导入成功!\n';
msg += `新增分类: ${result.imported || 0}\n`;msg += `更新分类: ${result.updated || 0}`;
if (result.skipped && result.skipped.length > 0) {
msg += `\n跳过已存在分类: ${result.skipped.join(', ')}`;
}
alert(msg); alert(msg);
closeDataImportModal();
closeImportModal(); // 刷新当前页面数据
await loadCategories(); if (dataImportType === 'models') loadAdminModels();
renderSidebar(); else if (dataImportType === 'gpus') loadAdminGpus();
loadAdminCategories(); else if (dataImportType === 'cpus') loadAdminCpus();
else if (dataImportCategoryId) loadDynamicCategoryData(dataImportCategoryId);
loadOverview();
} }
} catch (e) { } catch (e) { alert('导入失败: ' + e.message); }
alert('导入失败: ' + e.message);
}
} }
init(); init();