为各类别添加数据导入导出功能

This commit is contained in:
2026-07-11 00:34:47 +08:00
parent f83bf11669
commit 01abe7f86f
10 changed files with 457 additions and 95 deletions
+205 -91
View File
@@ -160,6 +160,8 @@
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-800" id="dynamic-title">数据管理</h1>
<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="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>
@@ -179,6 +181,8 @@
<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="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="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>
@@ -200,6 +204,8 @@
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-800">GPU管理</h1>
<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="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>
@@ -221,6 +227,8 @@
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-800">CPU管理</h1>
<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="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>
@@ -501,20 +509,18 @@
<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>
<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>
<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">
@@ -522,8 +528,6 @@
<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>
@@ -537,6 +541,42 @@
</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>
let currentType = '';
let currentId = '';
@@ -2729,154 +2769,228 @@
async function exportAllCategories() {
try {
const res = await fetch('/api/categories/export');
if (!res.ok) {
const err = await res.json();
alert('导出失败: ' + err.error);
return;
}
// 获取文件并下载
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`;
a.download = `categories-${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);
}
} 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;
}
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`;
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);
}
} catch (e) { alert('导出失败: ' + e.message); }
}
// 打开导入弹窗
// 打开分类导入弹窗
function openImportModal() {
document.getElementById('importModal').classList.remove('hidden');
document.getElementById('importPreview').classList.add('hidden');
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;
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.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>`;
});
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;
}
if (!importData) { alert('请先选择导入文件'); return; }
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 {
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)
});
if (dataImportType === 'models') endpoint = '/api/models/import?mode=' + mode;
else if (dataImportType === 'gpus') endpoint = '/api/gpus/import?mode=' + mode;
else if (dataImportType === 'cpus') endpoint = '/api/cpus/import?mode=' + mode;
else if (dataImportCategoryId) endpoint = `/api/items/${dataImportCategoryId}/import?mode=` + mode;
else { alert('未知数据类型'); return; }
const res = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(dataImportData) });
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(', ')}`;
}
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.length + ' 条';
alert(msg);
closeImportModal();
await loadCategories();
renderSidebar();
loadAdminCategories();
closeDataImportModal();
// 刷新当前页面数据
if (dataImportType === 'models') loadAdminModels();
else if (dataImportType === 'gpus') loadAdminGpus();
else if (dataImportType === 'cpus') loadAdminCpus();
else if (dataImportCategoryId) loadDynamicCategoryData(dataImportCategoryId);
loadOverview();
}
} catch (e) {
alert('导入失败: ' + e.message);
}
} catch (e) { alert('导入失败: ' + e.message); }
}
init();