添加分类管理导出导入功能

This commit is contained in:
2026-07-10 23:59:53 +08:00
parent 7d376533f3
commit cfe463ae8b
21 changed files with 512 additions and 4 deletions
+224 -1
View File
@@ -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>
@@ -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,184 @@
// 监听编辑弹框打开,显示智能补充按钮
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>`;
}
// 数据项
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>';content.innerHTML = html;
}
// 执行导入
async function doImport() {
if (!importData) {
alert('请先选择导入文件');
return;
}
const mode = document.getElementById('importMode').value;
const categoryId = document.getElementById('importCategoryId').value;
try {
let endpoint;
if (categoryId && importData.category) {
// 导入单个分类
endpoint = `/api/categories/import/${categoryId}?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_categories || 0}\n`;msg += `新增数据: ${result.imported_items || 0}\n`;msg += `更新数据: ${result.updated_items || 0}`;
if (result.skipped && result.skipped.length > 0) {
msg += `\n跳过已存在分类: ${result.skipped.join(', ')}`;
}
alert(msg);
closeImportModal();
await loadCategories();
renderSidebar();
loadAdminCategories();
loadOverview();
}
} catch (e) {
alert('导入失败: ' + e.message);
}
}
init();
</script>
</body>