3 Commits
21 changed files with 373 additions and 2 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+15
View File
@@ -0,0 +1,15 @@
==================================================
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
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.
+156 -1
View File
@@ -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
+201
View File
@@ -128,8 +128,12 @@
<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>
<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>
</div>
@@ -493,6 +497,46 @@
</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>
<script>
let currentType = '';
let currentId = '';
@@ -940,6 +984,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 +2723,162 @@
// 监听编辑弹框打开,显示智能补充按钮
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 = `param-hub-categories-export-${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}-export-${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 = '';
}
// 关闭导入弹窗
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;
if (importData.category) {
// 导入单个分类
endpoint = `/api/categories/import/${importData.category.id}?mode=${mode}`;
} else {
// 导入全部分类
endpoint = `/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';
msg += `新增分类: ${result.imported || 0}\n`;msg += `更新分类: ${result.updated || 0}`;
if (result.skipped && result.skipped.length > 0) {
msg += `\n跳过已存在分类: ${result.skipped.join(', ')}`;
}
alert(msg);
closeImportModal();
await loadCategories();
renderSidebar();
loadAdminCategories();
}
} catch (e) {
alert('导入失败: ' + e.message);
}
}
init();
</script>
</body>