重构了main.html的主函数

重构了大量的页面的sidebar
优化了代码结构
This commit is contained in:
2026-02-02 09:22:52 +08:00
33 changed files with 5566 additions and 2383 deletions

View File

@@ -25,6 +25,8 @@
<script src="../js/components/table.js"></script>
<script src="../js/pages/render.js"></script>
<script src="../js/main.js"></script>
<!-- 常量配置 -->
<script src="../js/config/constants.js"></script>
<link href="../lib/font-awesome/css/font-awesome.min.css" rel="stylesheet">
<link href="../css/main.css" rel="stylesheet">
</head>
@@ -50,12 +52,6 @@
<span class="ml-2">模型调优</span>
</a>
</div>
<div class="nav-item-wrapper">
<a href="main.html?page=my-models" data-page="my-models" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
<i class="fa fa-database w-5 text-center"></i>
<span class="ml-2">我的模型</span>
</a>
</div>
<div class="nav-item-wrapper">
<a href="#" data-page="model-eval" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
<i class="fa fa-line-chart w-5 text-center"></i>
@@ -85,7 +81,7 @@
</div>
<div class="nav-item-wrapper">
<a href="tools.html" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
<i class="fa fa-database w-5 text-center"></i>
<i class="fa fa-wrench w-5 text-center"></i>
<span class="ml-2">其他工具</span>
</a>
</div>
@@ -171,12 +167,19 @@
</div>
<script>
// API 基础地址 - 使用 config.yaml 中的 app.port (7861)
// 加载常量配置
if (typeof window.CONSTANTS === 'undefined') {
// 如果constants.js还未加载使用默认值
window.CONSTANTS = { API_CONFIG: { PORT: 7861, METRICS_INTERVAL: 30000 } };
}
const CONFIG = window.CONSTANTS.API_CONFIG;
// API 基础地址
if (typeof window.getApiBase !== 'function') {
window.getApiBase = () => {
const protocol = window.location.protocol;
const hostname = window.location.hostname;
return `${protocol}//${hostname}:7861/api`;
return `${protocol}//${hostname}:${CONFIG.PORT}/api`;
};
}
if (typeof window.API_BASE === 'undefined') {
@@ -215,9 +218,9 @@
}
}
// 页面加载时获取监控数据,并每30秒刷新
// 页面加载时获取监控数据,并定期刷新
fetchSystemMetrics();
setInterval(fetchSystemMetrics, 30000);
setInterval(fetchSystemMetrics, CONFIG.METRICS_INTERVAL);
// 各功能模块的表格配置
const tableConfigs = {
@@ -274,7 +277,7 @@
}},
{ title: '创建时间', key: 'create_time', render: (val) => val ? new Date(val).toLocaleString('zh-CN') : '-' }
],
actions: ['compare', 'delete']
actions: ['startCompare', 'delete']
},
'dataset-manage': {
title: '数据集管理',
@@ -356,7 +359,15 @@
{ title: '描述', key: 'description', render: (val) => val || '-' },
{ title: '创建时间', key: 'create_time', render: (val) => val ? new Date(val).toLocaleString('zh-CN') : '-' }
],
actions: ['edit', 'delete']
actions: ['edit', 'delete'],
// 训练模型 tab 的列配置
trainedColumns: [
{ 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') : '-' }
],
trainedActions: ['view', 'delete']
},
'logs': {
title: '查看日志',
@@ -563,7 +574,7 @@
progressRefreshTimer = setInterval(() => {
refreshTrainingProgress();
checkAndUpdateTaskStatus();
}, 5000);
}, CONFIG.TRAINING_REFRESH_INTERVAL);
// 更新侧边栏高亮状态
document.querySelectorAll('.nav-link').forEach(link => {
@@ -761,7 +772,10 @@
}
let data = await fetchData(apiUrl);
// 如果配置了 dataPath从返回数据中提取指定字段
if (config.dataPath && typeof data === 'object' && data !== null) {
// 训练模型 tab 需要从 {models: [...]} 中提取数据
if (config.hasModelTabs && currentModelTab === 'trained' && typeof data === 'object' && data !== null) {
data = data.models || [];
} else if (config.dataPath && typeof data === 'object' && data !== null) {
data = data[config.dataPath] || [];
}
currentPageData = data; // 保存当前页面数据
@@ -1075,15 +1089,13 @@
// 下载数据集(打包下载)
function downloadDataset(datasetId) {
const protocol = window.location.protocol;
const hostname = window.location.hostname;
window.open(`${protocol}//${hostname}:7861/api/dataset-manage/download/${datasetId}`, '_blank');
window.open(`${API_BASE}/dataset-manage/download/${datasetId}`, '_blank');
}
// 开始模型对比
async function startCompare(id) {
// 跳转到模型对比聊天页面(通过主框架加载
window.location.href = `main.html?page=model-compare-chat&id=${id}`;
// 跳转到模型对比聊天页面(独立页面
window.location.href = `model-compare-chat.html?id=${id}`;
}
// 筛选表格
@@ -1108,16 +1120,23 @@
// 渲染表格页面
function renderTablePage(config, data) {
// 使用配置中的列定义,如果没有则使用默认列
const columns = config.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') : '-' }
];
// 根据 tab 选择列配置和当前 API
let columns;
let currentApi = config.api;
if (config.hasModelTabs && currentModelTab === 'trained') {
columns = config.trainedColumns || config.columns;
currentApi = 'model-manage/trained-models';
} else {
columns = config.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') : '-' }
];
}
// 搜索框
const searchBox = (config.api === 'model-manage' || config.api === 'model-manage/trained-models' || config.api === 'dataset-manage' || config.api === 'fine-tune') ? `
const searchBox = (currentApi === 'model-manage' || currentApi === 'model-manage/trained-models' || config.api === 'dataset-manage' || config.api === 'fine-tune') ? `
<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"
@@ -1126,27 +1145,50 @@
</div>
` : '';
// 是否支持多选(模型管理和数据集管理)
const supportsMultiSelect = config.api === 'model-manage' || config.api === 'model-manage/trained-models' || config.api === 'dataset-manage' || config.api === 'fine-tune';
// 是否支持多选
const supportsMultiSelect = currentApi === 'model-manage' || currentApi === 'model-manage/trained-models' || config.api === 'dataset-manage' || config.api === 'fine-tune';
// 创建按钮(根据API类型决定是否显示)
const createButton = config.api === 'model-manage' ? `
// 创建按钮(根据 tab 类型决定是否显示)
let createButton = '';
if (config.hasModelTabs && currentModelTab === 'trained') {
// 训练模型 tab 不显示创建按钮
createButton = '';
} else if (config.hasModelTabs && currentModelTab === 'config') {
// 配置模型 tab 显示创建按钮
createButton = `
<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 === 'dataset-manage' ? `
`;
} else if (config.api === 'model-manage') {
createButton = `
<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>
`;
} else if (config.api === 'dataset-manage') {
createButton = `
<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' ? `
`;
} else if (config.api === 'fine-tune') {
createButton = `
<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>
` : ''));
`;
} else if (config.api === 'model-compare') {
createButton = `
<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>
`;
}
// 批量删除按钮(仅当有选中项时显示)
const batchDeleteButton = supportsMultiSelect && selectedItems.size > 0 ? `
<button onclick="batchDeleteItems('${config.api}')" class="bg-red-500 text-white px-4 py-2 rounded text-sm hover:bg-red-600 transition-colors font-medium shadow-sm">
<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>批量删除 (${selectedItems.size})
</button>
` : '';
@@ -1157,7 +1199,7 @@
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, '${config.api}')">
onchange="toggleSelectAll(this, '${currentApi}')">
</th>
` : '';
@@ -1212,7 +1254,7 @@
<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"
${selectedItems.has(item.name || item.id) ? 'checked' : ''}
onchange="toggleItemSelection('${item.name || item.id}', '${config.api}')">
onchange="toggleItemSelection('${item.name || item.id}', '${currentApi}')">
</td>
` : ''}
${columns.map(col => `
@@ -1222,23 +1264,27 @@
`).join('')}
<td class="px-4 py-4 text-sm text-center">
<div class="flex justify-center space-x-2">
${config.api === 'fine-tune' ? `
${currentApi === '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>
` : (config.api === 'model-manage/trained-models' ? `
<button onclick="deleteItem('${currentApi}', '${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">删除权重</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>
<button onclick="deleteItem('${config.api}', '${item.name || item.id}')" class="bg-red-500 text-white px-3 py-1 rounded text-xs hover:bg-red-600">删除</button>
<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">删除模型</button>
` : ''}
` : (config.api === 'model-manage' ? `
` : (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('${config.api}', '${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="deleteItem('${currentApi}', '${item.id}')" class="bg-red-500 text-white px-3 py-1 rounded text-xs hover:bg-red-600">删除</button>
` : (currentApi === '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>
` : '')))}
<button onclick="deleteItem('${currentApi}', '${item.id}')" class="bg-red-500 text-white px-3 py-1 rounded text-xs hover:bg-red-600">删除</button>
` : (currentApi === 'model-compare' ? `
<button onclick="startCompare('${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>
` : ''))))}
</div>
</td>
</tr>
@@ -1463,14 +1509,30 @@
// 获取合并按钮HTML根据合并状态显示不同按钮
function getMergeButtonHtml(name, method, path, merged, merging) {
// 优先检查 sessionStorage 中的临时状态(用于前端实时显示)
const tempStatus = sessionStorage.getItem('merge_status_' + name);
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);
// 如果前端正在合并中,显示合并中
if (tempStatus === '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>`;
// 检查临时状态是否过期超过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) {
@@ -1481,12 +1543,14 @@
// 如果前端成功状态且后端也返回已合并,显示成功
if (tempStatus === 'success' && merged) {
// 清除临时成功状态,让后端状态接管
sessionStorage.removeItem('merge_status_' + name);
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('merge_status_' + name);
sessionStorage.removeItem(storageKey);
sessionStorage.removeItem(storageKey + '_time');
}
// 如果后端返回已合并,显示成功
if (merged) {
@@ -1498,7 +1562,9 @@
// 启动合并任务
async function startMerge(name, method, path) {
// 先设置状态为"合并中"(存储到 sessionStorage
sessionStorage.setItem('merge_status_' + name, 'merging');
const storageKey = 'merge_status_' + name;
sessionStorage.setItem(storageKey, 'merging');
sessionStorage.setItem(storageKey + '_time', Date.now().toString());
// 刷新表格显示合并中状态
loadTableData();
@@ -1517,19 +1583,21 @@
if (result.code === 0) {
// 设置为成功状态,确保即使后端还没更新也能显示成功
sessionStorage.setItem('merge_status_' + name, 'success');
sessionStorage.setItem(storageKey, 'success');
// 延迟刷新表格,用后端真实状态替换前端状态
setTimeout(() => loadTableData(), 1500);
} else {
// 清除合并状态
sessionStorage.removeItem('merge_status_' + name);
sessionStorage.removeItem(storageKey);
sessionStorage.removeItem(storageKey + '_time');
showMessage('失败', result.message || '合并失败', 'error');
loadTableData();
}
} catch (error) {
console.error('[DEBUG] 合并失败:', error);
// 清除合并状态
sessionStorage.removeItem('merge_status_' + name);
sessionStorage.removeItem(storageKey);
sessionStorage.removeItem(storageKey + '_time');
showMessage('错误', '合并失败: ' + error.message, 'error');
loadTableData();
}
@@ -1546,6 +1614,29 @@
window.open(`${API_BASE}/model-manage/trained-models/${encodeURIComponent(modelName)}/export`, '_blank');
}
// 删除已训练模型的权重
window.deleteTrainedWeight = function(modelName) {
showConfirm('确认删除', `确定要删除模型 "${modelName}" 的权重文件吗?合并模型不受影响。`, async () => {
try {
const response = await fetch(`${API_BASE}/model-manage/trained-models/${encodeURIComponent(modelName)}?type=lora`, {
method: 'DELETE'
});
const result = await response.json();
if (result.code === 0) {
showMessage('成功', '权重已删除', 'success');
// 清除该模型的合并状态缓存,让前端重新从后端获取状态
sessionStorage.removeItem('merge_status_' + modelName);
sessionStorage.removeItem('merge_status_' + modelName + '_time');
} else {
showMessage('错误', result.message || '删除失败', 'error');
}
} catch (error) {
console.error('删除权重失败:', error);
showMessage('错误', '删除失败: ' + error.message, 'error');
}
});
};
// 编辑模型 - 全局
window.editModel = function(modelId) {
window.location.href = `model-manage-create.html?id=${modelId}`;
@@ -1754,64 +1845,6 @@
}
}
}
// ============ Web日志系统 ============
const webLogger = {
_currentPage: 'main',
// 初始化当前页面名称
init: function(pageName) {
this._currentPage = pageName || 'unknown';
},
// 发送日志到服务器
_sendLog: async function(level, message) {
try {
await fetch(`${API_BASE}/web-log`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
level: level,
message: message,
page: this._currentPage,
timestamp: new Date().toISOString()
})
});
} catch (e) {
// 发送失败时只记录到控制台
console.warn('日志发送失败:', e);
}
},
info: function(message) {
console.log(`[INFO] ${message}`);
this._sendLog('info', message);
},
error: function(message) {
console.error(`[ERROR] ${message}`);
this._sendLog('error', message);
},
warning: function(message) {
console.warn(`[WARNING] ${message}`);
this._sendLog('warning', message);
},
debug: function(message) {
console.debug(`[DEBUG] ${message}`);
this._sendLog('debug', message);
}
};
// 页面加载完成后初始化日志
document.addEventListener('DOMContentLoaded', function() {
// 获取当前页面名称
const path = window.location.pathname;
const pageName = path.split('/').pop().replace('.html', '') || 'main';
webLogger.init(pageName);
webLogger.info('页面加载完成');
});
</script>
<!-- 自定义消息弹窗 -->