/** * 表格组件 * 处理表格渲染、数据操作等 */ // 当前页面状态 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 = `批量删除 (${window.selectedItems.size})`; } else { batchDeleteBtn.innerHTML = `批量删除 (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 '' + displayText + ''; }}, { 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 '' + display.text + ''; }}, { title: '模型来源', key: 'model_source', render: (val) => { const textMap = { 'local': '本地模型', 'api': '在线模型', 'online': '在线模型' }; const displayText = textMap[val] || val || '-'; return '' + displayText + ''; }}, { title: '描述', key: 'description', render: (val) => val || '-' }, { title: '创建时间', key: 'create_time', render: (val) => val ? new Date(val).toLocaleString('zh-CN') : '-' } ]; createButton = ` `; 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) => `${val || '-'}` }, { 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' ? ` ` : (config.api === 'fine-tune' ? ` ` : (config.api === 'model-compare' ? ` ` : '')); 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) ? `
` : ''; // 批量删除按钮(仅当有选中项时显示) const batchDeleteButton = supportsMultiSelect && window.selectedItems.size > 0 ? ` ` : ''; const hasData = data && data.length > 0; // 多选列头 const selectAllHeader = supportsMultiSelect ? ` ` : ''; return `

${config.title}

${searchBox} ${createButton}
${config.hasModelTabs ? `
` : ''} ${supportsMultiSelect ? `
已选择 ${window.selectedItems.size}
${batchDeleteButton}
` : ''}
${selectAllHeader} ${columns.map(col => ``).join('')} ${hasData ? data.map(item => ` ${supportsMultiSelect ? ` ` : ''} ${columns.map(col => ` `).join('')} `).join('') : ''}
${col.title}操作
${col.render ? col.render(item[col.key], item) : (item[col.key] || '-')}
${config.api === 'fine-tune' ? ` ` : (currentApi === 'model-manage/trained-models' ? ` ${getMergeButtonHtml(item.name, item.train_methods?.[0]?.name || 'lora', item.base_model_path || '', item.merged, item.merging)} ${item.merged ? ` ` : ''} ${(item.merged && !item.merging) ? ` ` : ''} ` : (currentApi === 'model-manage' ? ` ` : (config.api === 'dataset-manage' ? ` ` : (config.api === 'model-compare' ? ` ${getCompareButtonHtml(item.id, item.status)} ` : ''))))}
${!hasData ? `

暂无数据

` : ''}
`; } // 获取合并按钮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 ``; } } // 如果后端返回正在合并中(锁文件存在) if (merging) { return ``; } if (tempStatus === 'success' && merged) { sessionStorage.removeItem(storageKey); sessionStorage.removeItem(storageKey + '_time'); return ``; } if (tempStatus === 'success' && !merged) { sessionStorage.removeItem(storageKey); sessionStorage.removeItem(storageKey + '_time'); } if (merged) { return ``; } return ``; } // 获取模型对比操作按钮HTML function getCompareButtonHtml(id, status) { // status: pending(未加载), loading(加载中), loaded(已加载) if (status === 'loading') { return ``; } if (status === 'loaded') { return ` `; } // pending - 显示"准备加载"按钮 return ``; } // 加载模型对比任务 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 };