754 lines
34 KiB
JavaScript
754 lines
34 KiB
JavaScript
/**
|
||
* 表格组件
|
||
* 处理表格渲染、数据操作等
|
||
*/
|
||
|
||
// 当前页面状态
|
||
window.currentPage = 'fine-tune';
|
||
window.currentParentPage = null;
|
||
window.selectedItems = new Set(); // 存储选中的项ID
|
||
window.currentPageData = []; // 存储当前页面数据
|
||
window.modelListCache = []; // 模型列表缓存
|
||
window.currentModelTab = 'config'; // 模型管理页面当前tab: 'config'=配置模型, 'trained'=训练模型
|
||
|
||
// 获取 API 数据
|
||
async function fetchData(url) {
|
||
const response = await fetch(url);
|
||
const result = await response.json();
|
||
if (result.code !== 0) {
|
||
throw new Error(result.message || '获取数据失败');
|
||
}
|
||
return result.data || [];
|
||
}
|
||
|
||
// 删除数据
|
||
async function deleteItem(api, id) {
|
||
// 如果是我的模型,提示删除合并模型
|
||
const confirmMessage = api === 'model-manage/trained-models'
|
||
? '确定要删除合并模型吗?权重文件不会删除。'
|
||
: '确定要删除这条记录吗?';
|
||
|
||
window.showConfirm('确认删除', confirmMessage, async () => {
|
||
try {
|
||
// 如果是我的模型,调用删除合并模型的API
|
||
if (api === 'model-manage/trained-models') {
|
||
const response = await fetch(`${window.API_BASE}/model-manage/trained-models/${id}?type=merged`, {
|
||
method: 'DELETE'
|
||
});
|
||
const result = await response.json();
|
||
if (result.code === 0) {
|
||
window.showMessage('成功', '删除成功', 'success');
|
||
// 清除合并状态缓存
|
||
sessionStorage.removeItem('merge_status_' + id);
|
||
// 刷新当前页面
|
||
clearSelection();
|
||
const activeLink = document.querySelector('.nav-link.sidebar-item-active');
|
||
if (activeLink && typeof window.loadPage === 'function') {
|
||
window.loadPage(activeLink.dataset.page);
|
||
}
|
||
} else {
|
||
window.showMessage('错误', result.message || '删除失败', 'error');
|
||
}
|
||
} else {
|
||
const response = await fetch(`${window.API_BASE}/${api}/${id}`, {
|
||
method: 'DELETE'
|
||
});
|
||
const result = await response.json();
|
||
if (result.code === 0) {
|
||
// 刷新当前页面
|
||
clearSelection(); // 清除选中状态
|
||
const activeLink = document.querySelector('.nav-link.sidebar-item-active');
|
||
if (activeLink && typeof window.loadPage === 'function') {
|
||
window.loadPage(activeLink.dataset.page);
|
||
}
|
||
} else {
|
||
window.showMessage('错误', result.message || '删除失败', 'error');
|
||
}
|
||
}
|
||
} catch (error) {
|
||
window.showMessage('错误', '删除失败: ' + error.message, 'error');
|
||
}
|
||
});
|
||
}
|
||
|
||
// 更新模型用途
|
||
async function updateModelPurpose(id, purpose) {
|
||
try {
|
||
const response = await fetch(`${window.API_BASE}/model-manage/${id}/purpose`, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ purpose })
|
||
});
|
||
const result = await response.json();
|
||
if (result.code === 0) {
|
||
// 刷新当前页面
|
||
const activeLink = document.querySelector('.nav-link.sidebar-item-active');
|
||
if (activeLink && typeof window.loadPage === 'function') {
|
||
window.loadPage(activeLink.dataset.page);
|
||
}
|
||
} else {
|
||
window.showMessage('错误', result.message || '更新失败', 'error');
|
||
}
|
||
} catch (error) {
|
||
window.showMessage('错误', '更新失败: ' + error.message, 'error');
|
||
}
|
||
}
|
||
|
||
// 切换单个项的选中状态
|
||
function toggleItemSelection(id, api) {
|
||
if (window.selectedItems.has(id)) {
|
||
window.selectedItems.delete(id);
|
||
} else {
|
||
window.selectedItems.add(id);
|
||
}
|
||
// 重新渲染当前页面以更新UI
|
||
refreshCurrentPage();
|
||
}
|
||
|
||
// 切换全选/取消全选
|
||
function toggleSelectAll(checkbox, api) {
|
||
// 使用保存的当前页面数据
|
||
if (checkbox.checked) {
|
||
// 全选当前页面的所有数据(支持 name 或 id)
|
||
window.currentPageData.forEach(item => window.selectedItems.add(item.name || item.id));
|
||
} else {
|
||
// 取消全选,移除当前页面所有数据的选中状态
|
||
window.currentPageData.forEach(item => window.selectedItems.delete(item.name || item.id));
|
||
}
|
||
refreshCurrentPage();
|
||
}
|
||
|
||
// 清除所有选中项
|
||
function clearSelection() {
|
||
window.selectedItems.clear();
|
||
refreshCurrentPage();
|
||
}
|
||
|
||
// 批量删除选中的项
|
||
function batchDeleteItems(api) {
|
||
if (window.selectedItems.size === 0) {
|
||
window.showMessage('提示', '请先选择要删除的项', 'warning');
|
||
return;
|
||
}
|
||
|
||
window.showConfirm('批量删除', `确定要删除选中的 ${window.selectedItems.size} 条记录吗?`, async () => {
|
||
const ids = Array.from(window.selectedItems);
|
||
let successCount = 0;
|
||
let failCount = 0;
|
||
|
||
for (const id of ids) {
|
||
try {
|
||
const response = await fetch(`${window.API_BASE}/${api}/${id}`, {
|
||
method: 'DELETE'
|
||
});
|
||
const result = await response.json();
|
||
if (result.code === 0) {
|
||
successCount++;
|
||
} else {
|
||
failCount++;
|
||
}
|
||
} catch (error) {
|
||
failCount++;
|
||
}
|
||
}
|
||
|
||
clearSelection();
|
||
|
||
// 刷新当前页面
|
||
const activeLink = document.querySelector('.nav-link.sidebar-item-active');
|
||
if (activeLink && typeof window.loadPage === 'function') {
|
||
window.loadPage(activeLink.dataset.page);
|
||
}
|
||
|
||
if (failCount === 0) {
|
||
window.showMessage('成功', `成功删除 ${successCount} 条记录`, 'success');
|
||
} else {
|
||
window.showMessage('部分失败', `成功删除 ${successCount} 条,${failCount} 条删除失败`, 'warning');
|
||
}
|
||
});
|
||
}
|
||
|
||
// 刷新当前页面(重新渲染)
|
||
function refreshCurrentPage() {
|
||
const activeLink = document.querySelector('.nav-link.sidebar-item-active');
|
||
if (activeLink) {
|
||
const pageName = activeLink.dataset.page;
|
||
const config = window.tableConfigs[pageName];
|
||
|
||
if (config && (config.hasModelTabs || config.api === 'model-manage' || config.api === 'dataset-manage')) {
|
||
const container = document.getElementById('page-content');
|
||
if (container && typeof renderTablePage === 'function') {
|
||
container.innerHTML = renderTablePage(config, window.currentPageData);
|
||
// 恢复复选框状态
|
||
updateCheckboxStates();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 更新复选框状态(保持选中状态)
|
||
function updateCheckboxStates() {
|
||
const checkboxes = document.querySelectorAll('tbody input[type="checkbox"]');
|
||
checkboxes.forEach(cb => {
|
||
const match = cb.getAttribute('onchange')?.match(/toggleItemSelection\((\d+)/) || cb.getAttribute('onchange')?.match(/toggleItemSelection\(([^,]+)/);
|
||
const id = match ? parseInt(match[1]) || match[1] : null;
|
||
if (id !== null && window.selectedItems.has(id)) {
|
||
cb.checked = true;
|
||
cb.closest('tr')?.classList.add('bg-blue-50');
|
||
} else {
|
||
cb.checked = false;
|
||
cb.closest('tr')?.classList.remove('bg-blue-50');
|
||
}
|
||
});
|
||
|
||
// 更新批量操作栏的显示状态
|
||
const batchActions = document.getElementById('batchActions');
|
||
if (batchActions) {
|
||
if (window.selectedItems.size > 0) {
|
||
batchActions.classList.remove('hidden');
|
||
batchActions.querySelector('strong').textContent = window.selectedItems.size;
|
||
} else {
|
||
batchActions.classList.add('hidden');
|
||
}
|
||
}
|
||
|
||
// 更新批量删除按钮
|
||
const batchDeleteBtn = document.querySelector('#batchActions button[onclick^="batchDeleteItems"]');
|
||
if (batchDeleteBtn) {
|
||
if (window.selectedItems.size > 0) {
|
||
batchDeleteBtn.innerHTML = `<i class="fa fa-trash mr-1"></i>批量删除 (${window.selectedItems.size})`;
|
||
} else {
|
||
batchDeleteBtn.innerHTML = `<i class="fa fa-trash mr-1"></i>批量删除 (0)`;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 编辑数据集
|
||
function editItem(api, id) {
|
||
if (api === 'dataset-manage') {
|
||
// 跳转到数据集创建页面进行编辑
|
||
window.location.href = `dataset-create.html?id=${id}`;
|
||
} else if (api === 'model-manage') {
|
||
// 跳转到模型创建页面进行编辑
|
||
window.location.href = `model-manage-create.html?id=${id}`;
|
||
} else {
|
||
window.showMessage('提示', '编辑功能开发中...', 'info');
|
||
}
|
||
}
|
||
|
||
// 下载数据集(打包下载)
|
||
function downloadDataset(datasetId) {
|
||
const protocol = window.location.protocol;
|
||
const hostname = window.location.hostname;
|
||
window.open(`${protocol}//${hostname}:7861/api/dataset-manage/download/${datasetId}`, '_blank');
|
||
}
|
||
|
||
// 开始模型对比
|
||
async function startCompare(id) {
|
||
// 跳转到模型对比聊天页面(通过主框架加载)
|
||
window.location.href = `main.html?page=model-compare-chat&id=${id}`;
|
||
}
|
||
|
||
// 筛选表格
|
||
function filterTable() {
|
||
const searchInput = document.getElementById('tableSearchInput');
|
||
if (!searchInput) return;
|
||
|
||
const keyword = searchInput.value.toLowerCase().trim();
|
||
const tbody = document.querySelector('#page-content table tbody');
|
||
if (!tbody) return;
|
||
|
||
const rows = tbody.querySelectorAll('tr');
|
||
rows.forEach(row => {
|
||
const text = row.querySelector('td')?.textContent?.toLowerCase() || '';
|
||
if (keyword === '' || text.includes(keyword)) {
|
||
row.style.display = '';
|
||
} else {
|
||
row.style.display = 'none';
|
||
}
|
||
});
|
||
}
|
||
|
||
// 刷新表格数据 - 重新加载当前页面
|
||
window.loadTableData = function() {
|
||
const activeLink = document.querySelector('.nav-link.sidebar-item-active');
|
||
if (activeLink && typeof window.loadPage === 'function') {
|
||
window.loadPage(activeLink.dataset.page);
|
||
}
|
||
};
|
||
|
||
// 合并模型权重(保留兼容)
|
||
window.viewTrainedModel = function(name, method, path) {
|
||
if (typeof window.startMerge === 'function') {
|
||
window.startMerge(name, method, path);
|
||
}
|
||
};
|
||
|
||
// 编辑模型
|
||
window.editModel = function(modelId) {
|
||
window.location.href = `model-manage-create.html?id=${modelId}`;
|
||
};
|
||
|
||
// 预览数据集
|
||
window.previewDataset = function(datasetId) {
|
||
window.location.href = `dataset-preview.html?id=${datasetId}`;
|
||
};
|
||
|
||
// 下载数据集
|
||
window.downloadDataset = function(datasetId) {
|
||
window.open(`${window.API_BASE}/dataset-manage/download/${datasetId}`, '_blank');
|
||
};
|
||
|
||
// 导出模型权重
|
||
function exportModel(modelName) {
|
||
window.open(`${window.API_BASE}/model-manage/trained-models/${encodeURIComponent(modelName)}/export`, '_blank');
|
||
}
|
||
|
||
// 删除已训练模型的权重
|
||
async function deleteTrainedWeight(modelName) {
|
||
window.showConfirm('确认删除', `确定要删除模型 "${modelName}" 的权重文件吗?合并模型不受影响。`, async () => {
|
||
try {
|
||
const response = await fetch(`${window.API_BASE}/model-manage/trained-models/${encodeURIComponent(modelName)}?type=lora`, {
|
||
method: 'DELETE'
|
||
});
|
||
const result = await response.json();
|
||
if (result.code === 0) {
|
||
// 只显示成功消息,不刷新表格(因为后端可能因为没有权重文件而不返回这条记录)
|
||
window.showMessage('成功', '权重已删除', 'success');
|
||
// 清除该模型的合并状态缓存,让前端重新从后端获取状态
|
||
sessionStorage.removeItem('merge_status_' + modelName);
|
||
sessionStorage.removeItem('merge_status_' + modelName + '_time');
|
||
} else {
|
||
window.showMessage('错误', result.message || '删除失败', 'error');
|
||
}
|
||
} catch (error) {
|
||
console.error('删除权重失败:', error);
|
||
window.showMessage('错误', '删除失败: ' + error.message, 'error');
|
||
}
|
||
});
|
||
}
|
||
|
||
// 切换模型管理tab
|
||
function switchModelTab(tab) {
|
||
window.currentModelTab = tab;
|
||
// 清除选中状态
|
||
clearSelection();
|
||
// 重新加载模型管理页面
|
||
window.loadPage('model-manage');
|
||
}
|
||
|
||
// ========== 渲染函数 ==========
|
||
|
||
// 渲染表格页面
|
||
function renderTablePage(config, data) {
|
||
// 如果是模型管理页面,根据tab动态决定列和配置
|
||
let columns = config.columns || [];
|
||
let createButton = '';
|
||
let supportsMultiSelect = false;
|
||
let currentApi = config.api;
|
||
|
||
if (config.hasModelTabs) {
|
||
if (window.currentModelTab === 'config') {
|
||
// 配置模型tab
|
||
columns = [
|
||
{ title: '模型名称', key: 'name' },
|
||
{ title: '模型类型', key: 'type', render: (val) => {
|
||
const textMap = { 'LLM': '大语言模型', 'CV': '计算机视觉', 'NLP': '自然语言处理', 'Embedding': '向量模型', 'Other': '其他' };
|
||
const displayText = textMap[val] || val || '-';
|
||
return '<span class="px-2 py-1 rounded text-xs bg-blue-100 text-blue-700">' + displayText + '</span>';
|
||
}},
|
||
{ title: '用途', key: 'purpose', render: (val) => {
|
||
const purposeMap = { 'training': { text: '训练', class: 'bg-blue-100 text-blue-700' }, 'inference': { text: '推理', class: 'bg-green-100 text-green-700' }, 'evaluation': { text: '评测', class: 'bg-purple-100 text-purple-700' } };
|
||
const display = purposeMap[val] || { text: val || '-', class: 'bg-gray-100 text-gray-700' };
|
||
return '<span class="px-2 py-1 rounded text-xs ' + display.class + '">' + display.text + '</span>';
|
||
}},
|
||
{ title: '模型来源', key: 'model_source', render: (val) => {
|
||
const textMap = { 'local': '本地模型', 'api': '在线模型', 'online': '在线模型' };
|
||
const displayText = textMap[val] || val || '-';
|
||
return '<span class="px-2 py-1 rounded text-xs bg-gray-100 text-gray-700">' + displayText + '</span>';
|
||
}},
|
||
{ title: '描述', key: 'description', render: (val) => val || '-' },
|
||
{ title: '创建时间', key: 'create_time', render: (val) => val ? new Date(val).toLocaleString('zh-CN') : '-' }
|
||
];
|
||
createButton = `
|
||
<button onclick="showCreateModal('model-manage')" class="bg-primary text-white px-3 py-1.5 rounded text-sm hover:bg-primary/90 transition-colors">
|
||
<i class="fa fa-plus mr-1"></i>新建模型
|
||
</button>
|
||
`;
|
||
supportsMultiSelect = true;
|
||
currentApi = 'model-manage';
|
||
} else {
|
||
// 训练模型tab
|
||
columns = [
|
||
{ title: '模型名称', key: 'name' },
|
||
{ title: '训练方法', key: 'train_methods', render: (val) => val && val[0] ? val[0].name : '-' },
|
||
{ title: '基座模型', key: 'base_model_path', render: (val) => `<span class="text-xs text-gray-500 truncate block" title="${val}">${val || '-'}</span>` },
|
||
{ title: '创建时间', key: 'create_time', render: (val) => val ? new Date(val).toLocaleString('zh-CN') : '-' }
|
||
];
|
||
supportsMultiSelect = true;
|
||
currentApi = 'model-manage/trained-models';
|
||
}
|
||
} else {
|
||
// 非模型管理页面,使用原始配置
|
||
columns = config.columns || columns;
|
||
createButton = config.api === 'dataset-manage' ? `
|
||
<button onclick="showCreateModal('${config.api}')" class="bg-primary text-white px-3 py-1.5 rounded text-sm hover:bg-primary/90 transition-colors">
|
||
<i class="fa fa-plus mr-1"></i>创建数据集
|
||
</button>
|
||
` : (config.api === 'fine-tune' ? `
|
||
<button onclick="navigateToPage('fine-tune-create')" class="bg-primary text-white px-3 py-1.5 rounded text-sm hover:bg-primary/90 transition-colors">
|
||
<i class="fa fa-plus mr-1"></i>新建调优任务
|
||
</button>
|
||
` : (config.api === 'model-compare' ? `
|
||
<button onclick="navigateToPage('model-compare-create')" class="bg-primary text-white px-3 py-1.5 rounded text-sm hover:bg-primary/90 transition-colors">
|
||
<i class="fa fa-plus mr-1"></i>新建对比
|
||
</button>
|
||
` : ''));
|
||
supportsMultiSelect = config.api === 'model-manage' || config.api === 'model-manage/trained-models' || config.api === 'dataset-manage' || config.api === 'fine-tune';
|
||
}
|
||
|
||
// 搜索框
|
||
const searchBox = (config.api === 'model-manage' || config.api === 'model-manage/trained-models' || config.api === 'dataset-manage' || config.api === 'fine-tune' || config.hasModelTabs) ? `
|
||
<div class="relative">
|
||
<input type="text" id="tableSearchInput" placeholder="搜索${config.title}..."
|
||
class="w-72 pl-9 pr-3 py-1.5 rounded border border-gray-300 text-sm focus:outline-none focus:border-primary focus:ring-1 focus:border-primary"
|
||
oninput="filterTable()">
|
||
<i class="fa fa-search absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"></i>
|
||
</div>
|
||
` : '';
|
||
|
||
// 批量删除按钮(仅当有选中项时显示)
|
||
const batchDeleteButton = supportsMultiSelect && window.selectedItems.size > 0 ? `
|
||
<button onclick="batchDeleteItems('${currentApi}')" class="bg-red-500 text-white px-4 py-2 rounded text-sm hover:bg-red-600 transition-colors font-medium shadow-sm">
|
||
<i class="fa fa-trash mr-1"></i>批量删除 (${window.selectedItems.size})
|
||
</button>
|
||
` : '';
|
||
|
||
const hasData = data && data.length > 0;
|
||
|
||
// 多选列头
|
||
const selectAllHeader = supportsMultiSelect ? `
|
||
<th class="px-4 py-3 text-center font-medium w-10">
|
||
<input type="checkbox" class="w-4 h-4 text-primary rounded border-gray-300 cursor-pointer"
|
||
onchange="toggleSelectAll(this, '${currentApi}')">
|
||
</th>
|
||
` : '';
|
||
|
||
return `
|
||
<div class="bg-white rounded-lg shadow-sm mb-6">
|
||
<div class="flex items-center justify-between p-4 border-b border-gray-100">
|
||
<h2 class="text-lg font-medium">${config.title}</h2>
|
||
<div class="flex items-center space-x-3">
|
||
${searchBox}
|
||
${createButton}
|
||
</div>
|
||
</div>
|
||
${config.hasModelTabs ? `
|
||
<div class="px-4 border-b border-gray-100">
|
||
<div class="flex space-x-1" id="modelTabs">
|
||
<button onclick="switchModelTab('config')" class="tab-btn px-4 py-2 text-sm font-medium rounded-t-lg transition-colors ${window.currentModelTab === 'config' ? 'bg-primary text-white' : 'text-gray-500 hover:text-gray-700 hover:bg-gray-100'}">
|
||
<i class="fa fa-cog mr-1"></i>配置模型
|
||
</button>
|
||
<button onclick="switchModelTab('trained')" class="tab-btn px-4 py-2 text-sm font-medium rounded-t-lg transition-colors ${window.currentModelTab === 'trained' ? 'bg-primary text-white' : 'text-gray-500 hover:text-gray-700 hover:bg-gray-100'}">
|
||
<i class="fa fa-rocket mr-1"></i>训练模型
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<style>
|
||
.tab-btn { position: relative; transition: all 0.2s; }
|
||
</style>
|
||
` : ''}
|
||
${supportsMultiSelect ? `
|
||
<div id="batchActions" class="px-4 py-2 bg-blue-50 border-b border-blue-100 flex items-center justify-between ${window.selectedItems.size > 0 ? '' : 'hidden'}">
|
||
<div class="flex items-center text-sm text-blue-700">
|
||
<span>已选择 <strong>${window.selectedItems.size}</strong> 项</span>
|
||
<button onclick="clearSelection()" class="ml-3 text-blue-500 hover:text-blue-700 underline">取消选择</button>
|
||
</div>
|
||
<div class="flex items-center space-x-2">
|
||
${batchDeleteButton}
|
||
</div>
|
||
</div>
|
||
` : ''}
|
||
<div class="overflow-x-auto">
|
||
<table class="w-full">
|
||
<thead>
|
||
<tr class="table-header-bg text-gray-500 text-sm">
|
||
${selectAllHeader}
|
||
${columns.map(col => `<th class="px-4 py-3 text-center font-medium">${col.title}</th>`).join('')}
|
||
<th class="px-4 py-3 text-center font-medium">操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${hasData ? data.map(item => `
|
||
<tr class="border-b border-gray-100 table-row-hover ${window.selectedItems.has(item.name || item.id) ? 'bg-blue-50' : ''}">
|
||
${supportsMultiSelect ? `
|
||
<td class="px-4 py-4 text-sm text-center">
|
||
<input type="checkbox" class="w-4 h-4 text-primary rounded border-gray-300 cursor-pointer"
|
||
${window.selectedItems.has(item.name || item.id) ? 'checked' : ''}
|
||
onchange="toggleItemSelection('${item.name || item.id}', '${currentApi}')">
|
||
</td>
|
||
` : ''}
|
||
${columns.map(col => `
|
||
<td class="px-4 py-4 text-sm text-center">
|
||
${col.render ? col.render(item[col.key], item) : (item[col.key] || '-')}
|
||
</td>
|
||
`).join('')}
|
||
<td class="px-4 py-4 text-sm text-center">
|
||
<div class="flex justify-center space-x-2">
|
||
${config.api === 'fine-tune' ? `
|
||
<button onclick="viewFineTuneLogs('${item.id}', '${item.name}')" class="bg-blue-500 text-white px-3 py-1 rounded text-xs hover:bg-blue-600">查看日志</button>
|
||
<button onclick="deleteItem('${config.api}', '${item.id}')" class="bg-red-500 text-white px-3 py-1 rounded text-xs hover:bg-red-600">删除任务</button>
|
||
` : (currentApi === 'model-manage/trained-models' ? `
|
||
${getMergeButtonHtml(item.name, item.train_methods?.[0]?.name || 'lora', item.base_model_path || '', item.merged, item.merging)}
|
||
<button onclick="deleteTrainedWeight('${item.name}')" class="bg-orange-500 text-white px-3 py-1 rounded text-xs hover:bg-orange-600" title="删除权重文件">删除权重</button>
|
||
${item.merged ? `
|
||
<button onclick="deleteItem('${currentApi}', '${item.name || item.id}')" class="bg-red-500 text-white px-3 py-1 rounded text-xs hover:bg-red-600" title="删除合并模型">删除模型</button>
|
||
` : ''}
|
||
${(item.merged && !item.merging) ? `
|
||
<button onclick="exportModel('${item.name}')" class="bg-green-500 text-white px-3 py-1 rounded text-xs hover:bg-green-600">导出</button>
|
||
` : ''}
|
||
` : (currentApi === 'model-manage' ? `
|
||
<button onclick="editModel('${item.id}')" class="bg-blue-500 text-white px-3 py-1 rounded text-xs hover:bg-blue-600">编辑</button>
|
||
<button onclick="deleteItem('${currentApi}', '${item.id}')" class="bg-red-500 text-white px-3 py-1 rounded text-xs hover:bg-red-600">删除</button>
|
||
` : (config.api === 'dataset-manage' ? `
|
||
<button onclick="previewDataset('${item.id}')" class="bg-blue-500 text-white px-3 py-1 rounded text-xs hover:bg-blue-600">预览</button>
|
||
<button onclick="downloadDataset('${item.id}')" class="bg-green-500 text-white px-3 py-1 rounded text-xs hover:bg-green-600">下载</button>
|
||
<button onclick="deleteItem('${config.api}', '${item.id}')" class="bg-red-500 text-white px-3 py-1 rounded text-xs hover:bg-red-600">删除</button>
|
||
` : (config.api === 'model-compare' ? `
|
||
${getCompareButtonHtml(item.id, item.status)}
|
||
` : ''))))}
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
`).join('') : ''}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
${!hasData ? `
|
||
<div class="p-8 text-center text-gray-400">
|
||
<div class="py-12">
|
||
<i class="fa fa-inbox text-5xl mb-4 text-gray-300"></i>
|
||
<p class="text-gray-500">暂无数据</p>
|
||
</div>
|
||
</div>
|
||
` : ''}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// 获取合并按钮HTML
|
||
function getMergeButtonHtml(name, method, path, merged, merging) {
|
||
const storageKey = 'merge_status_' + name;
|
||
const tempStatus = sessionStorage.getItem(storageKey);
|
||
const tempStatusTime = sessionStorage.getItem(storageKey + '_time');
|
||
console.log('[DEBUG] getMergeButtonHtml:', name, 'tempStatus:', tempStatus, 'merged:', merged, 'merging:', merging);
|
||
|
||
// 检查临时状态是否过期(超过5分钟视为过期)
|
||
const now = Date.now();
|
||
const statusExpired = tempStatusTime && (now - parseInt(tempStatusTime)) > 5 * 60 * 1000;
|
||
|
||
// 如果状态过期或无效,清除并视为无状态
|
||
if (statusExpired || (tempStatus && !tempStatusTime)) {
|
||
sessionStorage.removeItem(storageKey);
|
||
sessionStorage.removeItem(storageKey + '_time');
|
||
// 继续检查后端状态
|
||
} else if (tempStatus === 'merging') {
|
||
// 如果后端已经完成合并但前端状态未更新,清除临时状态
|
||
if (merged) {
|
||
sessionStorage.removeItem(storageKey);
|
||
sessionStorage.removeItem(storageKey + '_time');
|
||
} else {
|
||
return `<button class="bg-gray-300 text-gray-500 px-3 py-1 rounded text-xs cursor-not-allowed flex items-center" disabled>
|
||
<i class="fa fa-spinner fa-spin mr-1"></i>合并中...
|
||
</button>`;
|
||
}
|
||
}
|
||
// 如果后端返回正在合并中(锁文件存在)
|
||
if (merging) {
|
||
return `<button class="bg-gray-300 text-gray-500 px-3 py-1 rounded text-xs cursor-not-allowed flex items-center" disabled>
|
||
<i class="fa fa-spinner fa-spin mr-1"></i>合并中...
|
||
</button>`;
|
||
}
|
||
if (tempStatus === 'success' && merged) {
|
||
sessionStorage.removeItem(storageKey);
|
||
sessionStorage.removeItem(storageKey + '_time');
|
||
return `<button class="bg-gray-300 text-gray-500 px-3 py-1 rounded text-xs cursor-not-allowed" disabled>合并成功</button>`;
|
||
}
|
||
if (tempStatus === 'success' && !merged) {
|
||
sessionStorage.removeItem(storageKey);
|
||
sessionStorage.removeItem(storageKey + '_time');
|
||
}
|
||
if (merged) {
|
||
return `<button class="bg-gray-300 text-gray-500 px-3 py-1 rounded text-xs cursor-not-allowed" disabled>合并成功</button>`;
|
||
}
|
||
return `<button onclick="startMerge('${name}', '${method}', '${path}')" class="bg-primary text-white px-3 py-1 rounded text-xs hover:bg-primary/90">合并权重</button>`;
|
||
}
|
||
|
||
// 获取模型对比操作按钮HTML
|
||
function getCompareButtonHtml(id, status) {
|
||
// status: pending(未加载), loading(加载中), loaded(已加载)
|
||
if (status === 'loading') {
|
||
return `<button class="bg-yellow-500 text-white px-3 py-1 rounded text-xs cursor-not-allowed flex items-center" disabled>
|
||
<i class="fa fa-spinner fa-spin mr-1"></i>加载中...
|
||
</button>`;
|
||
}
|
||
if (status === 'loaded') {
|
||
return `
|
||
<button onclick="startCompare(${id})" class="bg-green-500 text-white px-3 py-1 rounded text-xs hover:bg-green-600 flex items-center">
|
||
<i class="fa fa-play mr-1"></i>开始对比
|
||
</button>
|
||
<button onclick="unloadCompare(${id})" class="bg-gray-500 text-white px-3 py-1 rounded text-xs hover:bg-gray-600">
|
||
停止
|
||
</button>
|
||
`;
|
||
}
|
||
// pending - 显示"准备加载"按钮
|
||
return `<button onclick="loadCompare(${id})" class="bg-primary text-white px-3 py-1 rounded text-xs hover:bg-primary/90">
|
||
<i class="fa fa-cloud-download mr-1"></i>准备加载
|
||
</button>`;
|
||
}
|
||
|
||
// 加载模型对比任务
|
||
async function loadCompare(id) {
|
||
try {
|
||
const response = await fetch(`${window.API_BASE}/model-compare/${id}/load`, {
|
||
method: 'POST'
|
||
});
|
||
const result = await response.json();
|
||
|
||
if (result.code === 0) {
|
||
window.showMessage('提示', '正在加载模型,请稍候...', 'info');
|
||
// 轮询加载状态
|
||
pollLoadStatus(id);
|
||
} else {
|
||
window.showMessage('错误', result.message || '加载失败', 'error');
|
||
}
|
||
} catch (error) {
|
||
console.error('加载模型失败:', error);
|
||
window.showMessage('错误', '加载失败: ' + error.message, 'error');
|
||
}
|
||
}
|
||
|
||
// 轮询加载状态
|
||
async function pollLoadStatus(id, maxAttempts = 60) {
|
||
let attempts = 0;
|
||
const checkInterval = 3000; // 3秒检查一次
|
||
|
||
const poll = async () => {
|
||
attempts++;
|
||
try {
|
||
const response = await fetch(`${window.API_BASE}/model-compare/${id}/load-status`);
|
||
const result = await response.json();
|
||
|
||
if (result.code === 0) {
|
||
const data = result.data;
|
||
if (data.all_ready) {
|
||
window.showMessage('成功', '模型加载完成!可以开始对比了。', 'success');
|
||
window.loadTableData(); // 刷新表格显示"开始对比"按钮
|
||
} else if (attempts < maxAttempts) {
|
||
setTimeout(poll, checkInterval);
|
||
} else {
|
||
window.showMessage('警告', '模型加载超时,请重试', 'warning');
|
||
}
|
||
} else if (attempts < maxAttempts) {
|
||
setTimeout(poll, checkInterval);
|
||
} else {
|
||
window.showMessage('错误', result.message || '加载状态检查失败', 'error');
|
||
}
|
||
} catch (error) {
|
||
console.error('检查加载状态失败:', error);
|
||
if (attempts < maxAttempts) {
|
||
setTimeout(poll, checkInterval);
|
||
}
|
||
}
|
||
};
|
||
|
||
setTimeout(poll, checkInterval);
|
||
}
|
||
|
||
// 停止模型对比任务
|
||
async function unloadCompare(id) {
|
||
window.showConfirm('确认停止', '确定要停止模型服务吗?', async () => {
|
||
try {
|
||
const response = await fetch(`${window.API_BASE}/model-compare/${id}/unload`, {
|
||
method: 'POST'
|
||
});
|
||
const result = await response.json();
|
||
|
||
if (result.code === 0) {
|
||
window.showMessage('成功', '已停止模型服务', 'success');
|
||
window.loadTableData();
|
||
} else {
|
||
window.showMessage('错误', result.message || '停止失败', 'error');
|
||
}
|
||
} catch (error) {
|
||
console.error('停止模型服务失败:', error);
|
||
window.showMessage('错误', '停止失败: ' + error.message, 'error');
|
||
}
|
||
});
|
||
}
|
||
|
||
// 启动合并任务
|
||
async function startMerge(name, method, path) {
|
||
const storageKey = 'merge_status_' + name;
|
||
sessionStorage.setItem(storageKey, 'merging');
|
||
sessionStorage.setItem(storageKey + '_time', Date.now().toString());
|
||
window.loadTableData();
|
||
|
||
try {
|
||
const response = await fetch(`${window.API_BASE}/model-manage/merge`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
model_name: name,
|
||
train_method: method || 'lora',
|
||
base_model_path: path
|
||
})
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.code === 0) {
|
||
sessionStorage.setItem(storageKey, 'success');
|
||
setTimeout(() => window.loadTableData(), 1500);
|
||
} else {
|
||
sessionStorage.removeItem(storageKey);
|
||
sessionStorage.removeItem(storageKey + '_time');
|
||
window.showMessage('失败', result.message || '合并失败', 'error');
|
||
window.loadTableData();
|
||
}
|
||
} catch (error) {
|
||
console.error('[DEBUG] 合并失败:', error);
|
||
sessionStorage.removeItem(storageKey);
|
||
sessionStorage.removeItem(storageKey + '_time');
|
||
window.showMessage('错误', '合并失败: ' + error.message, 'error');
|
||
window.loadTableData();
|
||
}
|
||
}
|
||
|
||
// 导出表格组件
|
||
window.TableComponent = {
|
||
fetchData,
|
||
deleteItem,
|
||
updateModelPurpose,
|
||
toggleItemSelection,
|
||
toggleSelectAll,
|
||
clearSelection,
|
||
batchDeleteItems,
|
||
refreshCurrentPage,
|
||
updateCheckboxStates,
|
||
editItem,
|
||
downloadDataset,
|
||
startCompare,
|
||
filterTable,
|
||
exportModel,
|
||
switchModelTab,
|
||
renderTablePage,
|
||
getMergeButtonHtml,
|
||
startMerge,
|
||
deleteTrainedWeight,
|
||
getCompareButtonHtml,
|
||
loadCompare,
|
||
unloadCompare
|
||
};
|