重构了main.html的主函数
重构了大量的页面的sidebar 优化了代码结构
This commit is contained in:
21
web/js/api.js
Normal file
21
web/js/api.js
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* API 配置模块
|
||||
* 提供 API 基础地址和常用配置
|
||||
*/
|
||||
|
||||
// API 基础地址配置
|
||||
(function() {
|
||||
if (typeof window.getApiBase !== 'function') {
|
||||
window.getApiBase = () => {
|
||||
const protocol = window.location.protocol;
|
||||
const hostname = window.location.hostname;
|
||||
return `${protocol}//${hostname}:7861/api`;
|
||||
};
|
||||
}
|
||||
if (typeof window.API_BASE === 'undefined') {
|
||||
window.API_BASE = window.getApiBase();
|
||||
}
|
||||
})();
|
||||
|
||||
// 导出 API_BASE 供其他模块使用
|
||||
window.API_BASE = window.getApiBase();
|
||||
236
web/js/components/sidebar-loader.js
Normal file
236
web/js/components/sidebar-loader.js
Normal file
@@ -0,0 +1,236 @@
|
||||
/**
|
||||
* 共享侧边栏加载器
|
||||
* 动态加载侧边栏组件,支持高亮当前页面
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// 侧边栏容器占位样式(防止加载时闪烁)
|
||||
const containerStyles = `
|
||||
#sidebar-container {
|
||||
width: 16rem;
|
||||
min-width: 16rem;
|
||||
height: 100vh;
|
||||
background-color: #001529;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
#sidebar-container {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// 侧边栏样式
|
||||
const sidebarStyles = `
|
||||
.sidebar-section-title {
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.75rem;
|
||||
color: rgba(191, 203, 217, 0.7);
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
.nav-link:hover {
|
||||
background-color: rgba(0, 21, 41, 0.2);
|
||||
}
|
||||
.sidebar-item-active {
|
||||
background-color: rgba(24, 144, 255, 0.1) !important;
|
||||
color: #1890ff !important;
|
||||
border-left: 4px solid #1890ff;
|
||||
}
|
||||
.sidebar-slider {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 4px;
|
||||
height: 0;
|
||||
background-color: #1890ff;
|
||||
border-radius: 0 2px 2px 0;
|
||||
transition: top 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
height 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
opacity 0.3s ease;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
opacity: 0;
|
||||
}
|
||||
.nav-item-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
.nav-link {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
// 立即注入容器样式(防止闪烁)
|
||||
(function injectContainerStyles() {
|
||||
const styleEl = document.createElement('style');
|
||||
styleEl.id = 'sidebar-container-styles';
|
||||
styleEl.textContent = containerStyles;
|
||||
document.head.appendChild(styleEl);
|
||||
})();
|
||||
|
||||
// 根据当前页面路径确定活动页面
|
||||
function getCurrentPage() {
|
||||
const path = window.location.pathname;
|
||||
const search = window.location.search;
|
||||
const fileName = path.split('/').pop().replace('.html', '');
|
||||
|
||||
// 检查 URL 参数
|
||||
const urlParams = new URLSearchParams(search);
|
||||
const pageParam = urlParams.get('page');
|
||||
if (pageParam) {
|
||||
return pageParam;
|
||||
}
|
||||
|
||||
// 根据文件名映射
|
||||
const pageMap = {
|
||||
'main': 'fine-tune',
|
||||
'hardware': 'hardware',
|
||||
'logs': 'logs',
|
||||
'tools': 'tools',
|
||||
'fine-tune-create': 'fine-tune',
|
||||
'training-log': 'fine-tune',
|
||||
'dataset-create': 'dataset-manage',
|
||||
'dataset-preview': 'dataset-manage',
|
||||
'model-manage': 'model-manage',
|
||||
'model-manage-create': 'model-manage',
|
||||
'model-eval': 'model-eval',
|
||||
'model-eval-create': 'model-eval',
|
||||
'model-compare-create': 'model-compare',
|
||||
'model-compare-chat': 'model-compare',
|
||||
'model-compare-result': 'model-compare',
|
||||
'model-dimension-create': 'model-eval',
|
||||
'model-inference': 'model-manage',
|
||||
'custom-tool-create': 'tools'
|
||||
};
|
||||
|
||||
return pageMap[fileName] || 'fine-tune';
|
||||
}
|
||||
|
||||
// 加载侧边栏
|
||||
async function loadSidebar() {
|
||||
const container = document.getElementById('sidebar-container');
|
||||
if (!container) {
|
||||
console.warn('未找到 sidebar-container 元素');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 计算组件路径
|
||||
const currentPath = window.location.pathname;
|
||||
let basePath = '';
|
||||
if (currentPath.includes('/pages/')) {
|
||||
basePath = 'components/';
|
||||
} else {
|
||||
basePath = 'pages/components/';
|
||||
}
|
||||
|
||||
const response = await fetch(basePath + 'sidebar.html');
|
||||
if (!response.ok) {
|
||||
throw new Error('加载侧边栏失败: ' + response.status);
|
||||
}
|
||||
|
||||
const html = await response.text();
|
||||
container.innerHTML = html;
|
||||
|
||||
// 注入样式
|
||||
if (!document.getElementById('sidebar-styles')) {
|
||||
const styleEl = document.createElement('style');
|
||||
styleEl.id = 'sidebar-styles';
|
||||
styleEl.textContent = sidebarStyles;
|
||||
document.head.appendChild(styleEl);
|
||||
}
|
||||
|
||||
// 修正 logo 路径
|
||||
const logo = container.querySelector('#sidebar-logo');
|
||||
if (logo) {
|
||||
const depth = currentPath.split('/pages/').length > 1 ? '../' : '';
|
||||
logo.src = depth + 'assets/logo/logo.png';
|
||||
}
|
||||
|
||||
// 高亮当前页面
|
||||
const currentPage = window.sidebarCurrentPage || getCurrentPage();
|
||||
highlightCurrentPage(currentPage);
|
||||
|
||||
// 初始化滑块
|
||||
initSlider();
|
||||
|
||||
} catch (error) {
|
||||
console.error('加载侧边栏出错:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 高亮当前页面
|
||||
function highlightCurrentPage(currentPage) {
|
||||
document.querySelectorAll('.nav-link').forEach(link => {
|
||||
const page = link.dataset.page;
|
||||
if (page === currentPage) {
|
||||
link.classList.add('sidebar-item-active');
|
||||
link.classList.remove('hover:bg-[#001529]/20', 'transition-colors');
|
||||
} else {
|
||||
link.classList.remove('sidebar-item-active');
|
||||
link.classList.add('hover:bg-[#001529]/20', 'transition-colors');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 初始化滑块
|
||||
function initSlider() {
|
||||
const slider = document.getElementById('sidebar-slider');
|
||||
if (!slider) return;
|
||||
|
||||
// 找到当前活动项
|
||||
const activeLink = document.querySelector('.nav-link.sidebar-item-active');
|
||||
if (activeLink) {
|
||||
const wrapper = activeLink.closest('.nav-item-wrapper');
|
||||
if (wrapper) {
|
||||
updateSliderPosition(wrapper);
|
||||
}
|
||||
}
|
||||
|
||||
// 绑定导航点击事件
|
||||
document.querySelectorAll('.nav-link').forEach(link => {
|
||||
link.addEventListener('click', function(e) {
|
||||
const wrapper = this.closest('.nav-item-wrapper');
|
||||
if (wrapper) {
|
||||
updateSliderPosition(wrapper);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 更新滑块位置
|
||||
function updateSliderPosition(targetWrapper) {
|
||||
const slider = document.getElementById('sidebar-slider');
|
||||
if (!slider || !targetWrapper) return;
|
||||
|
||||
const nav = document.querySelector('nav');
|
||||
if (!nav) return;
|
||||
|
||||
const navRect = nav.getBoundingClientRect();
|
||||
const wrapperRect = targetWrapper.getBoundingClientRect();
|
||||
|
||||
const top = wrapperRect.top - navRect.top + nav.scrollTop;
|
||||
const height = wrapperRect.height;
|
||||
|
||||
slider.style.top = top + 'px';
|
||||
slider.style.height = height + 'px';
|
||||
slider.style.opacity = '1';
|
||||
}
|
||||
|
||||
// 导出到全局
|
||||
window.SidebarLoader = {
|
||||
load: loadSidebar,
|
||||
highlight: highlightCurrentPage,
|
||||
getCurrentPage: getCurrentPage
|
||||
};
|
||||
|
||||
// DOM 加载完成后自动加载侧边栏
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', loadSidebar);
|
||||
} else {
|
||||
loadSidebar();
|
||||
}
|
||||
})();
|
||||
753
web/js/components/table.js
Normal file
753
web/js/components/table.js
Normal file
@@ -0,0 +1,753 @@
|
||||
/**
|
||||
* 表格组件
|
||||
* 处理表格渲染、数据操作等
|
||||
*/
|
||||
|
||||
// 当前页面状态
|
||||
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
|
||||
};
|
||||
66
web/js/config/constants.js
Normal file
66
web/js/config/constants.js
Normal file
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* 常量定义
|
||||
* 集中管理所有硬编码的配置值
|
||||
*/
|
||||
|
||||
// API 配置
|
||||
window.API_CONFIG = {
|
||||
PORT: 7861,
|
||||
TIMEOUT: 30000, // 请求超时时间
|
||||
|
||||
// 系统监控
|
||||
METRICS_INTERVAL: 30000, // 系统指标刷新间隔(ms)
|
||||
|
||||
// 训练进度
|
||||
TRAINING_REFRESH_INTERVAL: 5000, // 训练进度刷新间隔(ms)
|
||||
PROGRESS_BAR_WIDTH: 200, // 进度条宽度
|
||||
|
||||
// 合并操作
|
||||
MERGE_TIMEOUT: 5 * 60 * 1000, // 合并状态超时时间(ms)
|
||||
|
||||
// 分页
|
||||
PAGE_SIZE: 10,
|
||||
|
||||
// 消息提示
|
||||
TOAST_DURATION: 3000, // toast提示显示时间(ms)
|
||||
|
||||
// 模型类型
|
||||
MODEL_TYPES: {
|
||||
'LLM': '大语言模型',
|
||||
'CV': '计算机视觉',
|
||||
'NLP': '自然语言处理',
|
||||
'Embedding': '向量模型',
|
||||
'Other': '其他'
|
||||
},
|
||||
|
||||
// 用途映射
|
||||
PURPOSE_MAP: {
|
||||
'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' }
|
||||
},
|
||||
|
||||
// 模型来源
|
||||
SOURCE_MAP: {
|
||||
'local': '本地模型',
|
||||
'api': '在线模型',
|
||||
'online': '在线模型'
|
||||
},
|
||||
|
||||
// 训练方法显示名称
|
||||
TRAIN_METHOD_MAP: {
|
||||
'lora': 'LoRA',
|
||||
'qlora': 'QLoRA',
|
||||
'full': '全量微调',
|
||||
'prefix': 'Prefix Tuning',
|
||||
'adapter': 'Adapter',
|
||||
'peft': 'PEFT',
|
||||
'adalora': 'AdaLoRA',
|
||||
'longlora': 'LongLoRA',
|
||||
'dpo': 'DPO',
|
||||
'cpt': 'CPT'
|
||||
}
|
||||
};
|
||||
|
||||
// 导出为常量引用方便使用
|
||||
window.CONSTANTS = window.API_CONFIG;
|
||||
597
web/js/main.js
Normal file
597
web/js/main.js
Normal file
@@ -0,0 +1,597 @@
|
||||
/**
|
||||
* 主入口模块
|
||||
* 页面初始化和导航控制
|
||||
*/
|
||||
|
||||
// 各功能模块的表格配置
|
||||
window.tableConfigs = {
|
||||
'fine-tune': {
|
||||
title: '模型调优',
|
||||
api: 'fine-tune',
|
||||
hasCreate: true,
|
||||
createText: '创建训练任务',
|
||||
columns: [
|
||||
{ title: '任务名称', key: 'name' },
|
||||
{ title: '任务状态', key: 'status', render: (val) => `<span class="px-2 py-1 rounded text-xs ${val === 'running' ? 'bg-green-100 text-green-700' : val === 'failed' ? 'bg-red-100 text-red-700' : 'bg-gray-100 text-gray-700'}">${val}</span>` },
|
||||
{ title: '训练方式', key: 'train_type', render: (val) => val === 'SFT' ? 'SFT 微调训练' : (val === 'DPO' ? 'DPO 偏好训练' : (val === 'CPT' ? 'CPT 继续预训练' : '-')) },
|
||||
{ title: '训练模板', key: 'template', render: (val) => val || '-' },
|
||||
{ title: '基座模型', key: 'base_model', render: (val, row) => `<span class="model-name-cell" data-model-id="${val}">加载中...</span>` }
|
||||
],
|
||||
actions: ['stop', 'logs', 'delete']
|
||||
},
|
||||
'my-models': {
|
||||
title: '我的模型',
|
||||
api: 'model-manage/trained-models',
|
||||
dataPath: 'models',
|
||||
hasCreate: false,
|
||||
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') : '-' }
|
||||
],
|
||||
actions: ['view', 'delete']
|
||||
},
|
||||
'model-eval': {
|
||||
title: '模型评测',
|
||||
isExternalPage: true,
|
||||
createConfig: {
|
||||
page: 'model-eval-create',
|
||||
hasCreate: true,
|
||||
createText: '新建评测'
|
||||
}
|
||||
},
|
||||
'model-compare': {
|
||||
title: '模型对比',
|
||||
api: 'model-compare',
|
||||
hasCreate: true,
|
||||
createText: '新建对比',
|
||||
columns: [
|
||||
{ title: '对比名称', key: 'model_name' },
|
||||
{ title: '描述', key: 'description', render: (val) => val || '-' },
|
||||
{ title: '相关模型', key: 'models', render: (val) => {
|
||||
if (!val) return '-';
|
||||
try {
|
||||
// 如果是字符串,尝试解析 JSON
|
||||
let models = val;
|
||||
if (typeof val === 'string') {
|
||||
try {
|
||||
models = JSON.parse(val);
|
||||
} catch {
|
||||
models = val.split(',').map(id => ({ model_name: id.trim() }));
|
||||
}
|
||||
}
|
||||
// 如果是数组,提取模型名称
|
||||
if (Array.isArray(models) && models.length > 0) {
|
||||
return models.map(function(m) {
|
||||
if (typeof m === 'object' && m !== null) {
|
||||
return m.model_name || m.name || '未知模型';
|
||||
}
|
||||
return String(m);
|
||||
}).join(', ');
|
||||
}
|
||||
// 如果是单个对象
|
||||
if (typeof models === 'object' && models !== null) {
|
||||
return models.model_name || models.name || '未知模型';
|
||||
}
|
||||
return String(models);
|
||||
} catch (e) {
|
||||
return '解析错误';
|
||||
}
|
||||
}},
|
||||
{ title: '创建时间', key: 'create_time', render: (val) => val ? new Date(val).toLocaleString('zh-CN') : '-' }
|
||||
],
|
||||
actions: ['compare', 'delete']
|
||||
},
|
||||
'dataset-manage': {
|
||||
title: '数据集管理',
|
||||
api: 'dataset-manage',
|
||||
hasCreate: true,
|
||||
createText: '上传数据集',
|
||||
columns: [
|
||||
{ title: '数据集名称', key: 'name' },
|
||||
{ title: '数据类型', key: 'type', render: (val) => {
|
||||
const textMap = {
|
||||
'train': '训练数据',
|
||||
'test': '测试数据',
|
||||
'eval': '评测数据',
|
||||
'val': '验证数据',
|
||||
'other': '其他'
|
||||
};
|
||||
const displayText = textMap[val?.toLowerCase()] || val || '-';
|
||||
return '<span class="px-2 py-1 rounded text-xs bg-blue-100 text-blue-700">' + displayText + '</span>';
|
||||
}},
|
||||
{ title: '存储位置', key: 'storage_type', render: (val) => {
|
||||
const textMap = {
|
||||
'local': '本地存储',
|
||||
'minio': 'MinIO',
|
||||
'cloud': '云存储'
|
||||
};
|
||||
const displayText = textMap[val] || val || '-';
|
||||
return '<span class="px-2 py-1 rounded text-xs bg-green-100 text-green-700">' + displayText + '</span>';
|
||||
}},
|
||||
{ title: '大小', key: 'size', render: (val) => (val && val !== '0 B' && val !== '0') ? val : '-' },
|
||||
{ title: '数据条数', key: 'count', render: (val) => val || 0 },
|
||||
{ title: '描述', key: 'description', render: (val) => val || '-' },
|
||||
{ title: '创建时间', key: 'create_time', render: (val) => val ? new Date(val).toLocaleString('zh-CN') : '-' }
|
||||
],
|
||||
actions: ['preview', 'download', 'delete']
|
||||
},
|
||||
'data-generate': {
|
||||
title: '其他工具',
|
||||
isTools: true,
|
||||
defaultTools: [
|
||||
{ id: 'data-generate', name: '数据生成', icon: 'fa-database', description: '基于LLM生成微调数据集' },
|
||||
{ id: 'json2jsonl', name: 'JSON转JSONL', icon: 'fa-code', description: '将JSON文件转换为JSONL格式' },
|
||||
{ id: 'md-convert', name: '转换Markdown', icon: 'fa-file-text', description: '将Markdown文件转换为训练数据' }
|
||||
],
|
||||
customTools: []
|
||||
},
|
||||
'model-manage': {
|
||||
title: '模型管理',
|
||||
api: 'model-manage',
|
||||
hasCreate: true,
|
||||
hasModelTabs: true,
|
||||
createText: '添加模型',
|
||||
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') : '-' }
|
||||
],
|
||||
actions: ['edit', 'delete']
|
||||
},
|
||||
'config': {
|
||||
title: '平台性能',
|
||||
skipFetch: true,
|
||||
hasCreate: false,
|
||||
isHardwareMonitor: true
|
||||
},
|
||||
'logs': {
|
||||
title: '查看日志',
|
||||
skipFetch: true,
|
||||
hasCreate: false,
|
||||
isLogViewer: true
|
||||
},
|
||||
'model-compare-chat': {
|
||||
title: '模型对比',
|
||||
skipFetch: true,
|
||||
hasCreate: false,
|
||||
isExternalPage: true
|
||||
},
|
||||
'model-compare-result': {
|
||||
title: '对比结果',
|
||||
skipFetch: true,
|
||||
hasCreate: false,
|
||||
isExternalPage: true
|
||||
},
|
||||
'training-log': {
|
||||
title: '训练日志',
|
||||
skipFetch: true,
|
||||
hasCreate: false,
|
||||
isExternalPage: true
|
||||
}
|
||||
};
|
||||
|
||||
// 操作按钮映射
|
||||
window.actionLabels = {
|
||||
'stop': '停止',
|
||||
'logs': '查看日志',
|
||||
'delete': '删除',
|
||||
'deploy': '部署',
|
||||
'eval': '评测',
|
||||
'report': '查看报告',
|
||||
'scale': '扩容',
|
||||
'preview': '预览',
|
||||
'download': '下载',
|
||||
'detail': '详情',
|
||||
'edit': '编辑',
|
||||
'compare': '开始对话',
|
||||
'chat': '对话',
|
||||
'view': '合并权重'
|
||||
};
|
||||
|
||||
// 加载模型列表缓存
|
||||
async function loadModelListCache() {
|
||||
try {
|
||||
const response = await fetch(`${window.API_BASE}/model-manage`);
|
||||
const result = await response.json();
|
||||
if (result.code === 0) {
|
||||
window.modelListCache = result.data || [];
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载模型列表失败:', e);
|
||||
window.modelListCache = [];
|
||||
}
|
||||
}
|
||||
|
||||
// 根据模型ID获取模型名称(同步版本)
|
||||
function getModelName(modelId) {
|
||||
if (!modelId) return '-';
|
||||
const model = window.modelListCache.find(m =>
|
||||
m.id == modelId ||
|
||||
m.id === String(modelId) ||
|
||||
m.id === Number(modelId)
|
||||
);
|
||||
if (model) {
|
||||
return model.name;
|
||||
}
|
||||
return `模型${modelId}`;
|
||||
}
|
||||
|
||||
// 异步获取模型名称并更新 DOM
|
||||
async function fetchAndUpdateModelName(modelId, cellElement) {
|
||||
if (!modelId) {
|
||||
cellElement.textContent = '-';
|
||||
return;
|
||||
}
|
||||
|
||||
let model = window.modelListCache.find(m =>
|
||||
m.id == modelId ||
|
||||
m.id === String(modelId) ||
|
||||
m.id === Number(modelId)
|
||||
);
|
||||
|
||||
if (!model) {
|
||||
try {
|
||||
const response = await fetch(`${window.API_BASE}/model-manage`);
|
||||
const result = await response.json();
|
||||
if (result.code === 0) {
|
||||
window.modelListCache = result.data || [];
|
||||
model = window.modelListCache.find(m =>
|
||||
m.id == modelId ||
|
||||
m.id === String(modelId) ||
|
||||
m.id === Number(modelId)
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取模型列表失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
if (model) {
|
||||
cellElement.textContent = model.name;
|
||||
} else {
|
||||
cellElement.textContent = `模型${modelId}`;
|
||||
}
|
||||
}
|
||||
|
||||
// 根据模型ID列表获取模型名称列表
|
||||
function getModelNames(modelIds) {
|
||||
if (!modelIds || !Array.isArray(modelIds)) return '-';
|
||||
return modelIds.map(id => getModelName(id)).join(', ');
|
||||
}
|
||||
|
||||
// 显示创建表单页面
|
||||
window.showCreateModal = function(apiType) {
|
||||
if (apiType === 'fine-tune') {
|
||||
window.location.href = 'fine-tune-create.html';
|
||||
} else if (apiType === 'model-manage') {
|
||||
window.location.href = 'model-manage-create.html';
|
||||
} else if (apiType === 'model-eval') {
|
||||
window.location.href = 'model-eval-create.html';
|
||||
} else if (apiType === 'dataset-manage') {
|
||||
window.location.href = 'dataset-create.html';
|
||||
} else if (apiType === 'model-compare') {
|
||||
window.location.href = 'model-compare-create.html';
|
||||
} else {
|
||||
window.showMessage('提示', '该功能开发中...', 'info');
|
||||
}
|
||||
};
|
||||
|
||||
// 返回列表页
|
||||
window.goBack = function() {
|
||||
if (window.currentParentPage) {
|
||||
window.currentPage = window.currentParentPage;
|
||||
window.currentParentPage = null;
|
||||
loadPage(window.currentPage);
|
||||
} else {
|
||||
loadPage('fine-tune');
|
||||
}
|
||||
};
|
||||
|
||||
// 跳转到页面
|
||||
window.navigateToPage = function(pageName) {
|
||||
if (pageName.endsWith('-create')) {
|
||||
window.location.href = `${pageName}.html`;
|
||||
} else {
|
||||
window.location.href = `main.html?page=${pageName}`;
|
||||
}
|
||||
};
|
||||
|
||||
// 返回到列表页
|
||||
window.goBackToList = function() {
|
||||
navigateToPage('fine-tune');
|
||||
};
|
||||
|
||||
// 加载页面内容
|
||||
async function loadPage(pageName) {
|
||||
// 切换页面时清除选中状态
|
||||
TableComponent.clearSelection();
|
||||
|
||||
// 离开日志页面时停止自动刷新
|
||||
SystemService.stopLogAutoRefresh();
|
||||
|
||||
// 离开模型调优页面时停止进度刷新
|
||||
if (window.currentPage === 'fine-tune' && pageName !== 'fine-tune') {
|
||||
TrainingService.stopProgressRefresh();
|
||||
}
|
||||
|
||||
const container = document.getElementById('page-content');
|
||||
const config = window.tableConfigs[pageName];
|
||||
|
||||
if (!config) return;
|
||||
|
||||
// 更新当前页面
|
||||
window.currentPage = pageName;
|
||||
|
||||
// 显示加载中
|
||||
container.innerHTML = `
|
||||
<div class="bg-white rounded-lg shadow-sm mb-6 p-8 text-center">
|
||||
<i class="fa fa-spinner fa-spin text-3xl text-primary"></i>
|
||||
<p class="mt-2 text-gray-500">加载中...</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 显示/隐藏返回按钮
|
||||
const backBtn = document.getElementById('pageBackBtn');
|
||||
if (config.isExternalPage) {
|
||||
backBtn.classList.remove('hidden');
|
||||
} else {
|
||||
backBtn.classList.add('hidden');
|
||||
}
|
||||
|
||||
try {
|
||||
if (config.isExternalPage) {
|
||||
// 外部页面
|
||||
const response = await fetch(`${pageName}.html?t=${Date.now()}`);
|
||||
if (response.ok) {
|
||||
const html = await response.text();
|
||||
const scriptRegex = /<script\b(?![^>]*\bsrc)[^>]*>([\s\S]*?)<\/script>/g;
|
||||
const scriptContents = [];
|
||||
let match;
|
||||
while ((match = scriptRegex.exec(html)) !== null) {
|
||||
scriptContents.push(match[1]);
|
||||
}
|
||||
const scriptContent = scriptContents.join('\n');
|
||||
const htmlWithoutScript = html.replace(/<script\b[^>]*>[\s\S]*?<\/script>/g, '');
|
||||
|
||||
let headerHtml = '';
|
||||
if (config.createConfig && config.createConfig.hasCreate) {
|
||||
headerHtml = `
|
||||
<div class="bg-white rounded-lg shadow-sm mb-6 p-4 border-b border-gray-100 flex items-center justify-between">
|
||||
<div class="flex items-center space-x-8">
|
||||
<button class="tab-btn active pb-3 text-sm font-medium flex items-center text-primary" data-tab="tasks" onclick="switchTab(this, 'tasks')">
|
||||
<i class="fa fa-tasks mr-2"></i>评测任务
|
||||
</button>
|
||||
<button class="tab-btn pb-3 text-sm font-medium flex items-center text-gray-500" data-tab="leaderboard" onclick="switchTab(this, 'leaderboard')">
|
||||
<i class="fa fa-trophy mr-2"></i>排行榜
|
||||
</button>
|
||||
<button class="tab-btn pb-3 text-sm font-medium flex items-center text-gray-500" data-tab="dimensions" onclick="switchTab(this, 'dimensions')">
|
||||
<i class="fa fa-sliders mr-2"></i>评测维度
|
||||
</button>
|
||||
</div>
|
||||
<div id="headerActionButtons" style="min-height: 36px;">
|
||||
<button onclick="navigateToPage('${config.createConfig.page}')" class="bg-primary text-white px-4 py-2 rounded-lg text-sm hover:bg-primary/90 transition-colors flex items-center">
|
||||
<i class="fa fa-plus mr-2"></i>${config.createConfig.createText}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
.tab-btn { position: relative; transition: all 0.2s; }
|
||||
.tab-btn.active { color: #1890ff; }
|
||||
.tab-btn.active::after { content: ''; position: absolute; bottom: -16px; left: 0; right: 0; height: 2px; background-color: #1890ff; }
|
||||
.tab-btn:hover:not(.active) { color: #1890ff; }
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
|
||||
container.innerHTML = headerHtml + htmlWithoutScript;
|
||||
|
||||
if (scriptContent && scriptContent.trim()) {
|
||||
try {
|
||||
const oldScript = document.getElementById('externalPageScript');
|
||||
if (oldScript) oldScript.remove();
|
||||
const scriptEl = document.createElement('script');
|
||||
scriptEl.id = 'externalPageScript';
|
||||
scriptEl.textContent = scriptContent;
|
||||
document.body.appendChild(scriptEl);
|
||||
} catch (e) {
|
||||
console.error('执行脚本失败:', e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new Error('页面加载失败');
|
||||
}
|
||||
} else if (config.isHardwareMonitor) {
|
||||
container.innerHTML = PageRenderer.renderConfigPage(config, null);
|
||||
PageRenderer.initGPUList();
|
||||
PageRenderer.startRefreshTimer();
|
||||
} else if (config.isLogViewer) {
|
||||
container.innerHTML = PageRenderer.renderLogViewerPage(config);
|
||||
SystemService.initLogViewer();
|
||||
} else if (config.isForm) {
|
||||
const data = await TableComponent.fetchData(`${window.API_BASE}/${config.api}`);
|
||||
container.innerHTML = PageRenderer.renderConfigPage(config, data);
|
||||
} else if (config.isTools) {
|
||||
container.innerHTML = PageRenderer.renderToolsPage(config);
|
||||
} else {
|
||||
// 模型管理页面根据tab选择不同的API
|
||||
let apiUrl = `${window.API_BASE}/${config.api}`;
|
||||
if (config.hasModelTabs) {
|
||||
if (window.currentModelTab === 'trained') {
|
||||
apiUrl = `${window.API_BASE}/model-manage/trained-models`;
|
||||
}
|
||||
}
|
||||
let data = await TableComponent.fetchData(apiUrl);
|
||||
let dataPath = config.dataPath || null;
|
||||
if (config.hasModelTabs && window.currentModelTab === 'trained') {
|
||||
dataPath = 'models';
|
||||
}
|
||||
if (dataPath && typeof data === 'object' && data !== null) {
|
||||
data = data[dataPath] || [];
|
||||
}
|
||||
window.currentPageData = data;
|
||||
container.innerHTML = TableComponent.renderTablePage(config, data);
|
||||
|
||||
setTimeout(() => {
|
||||
const modelCells = container.querySelectorAll('.model-name-cell');
|
||||
modelCells.forEach(cell => {
|
||||
const modelId = cell.getAttribute('data-model-id');
|
||||
if (modelId) {
|
||||
fetchAndUpdateModelName(modelId, cell);
|
||||
}
|
||||
});
|
||||
}, 0);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error);
|
||||
container.innerHTML = `
|
||||
<div class="bg-white rounded-lg shadow-sm mb-6 p-8 text-center">
|
||||
<i class="fa fa-exclamation-circle text-3xl text-danger"></i>
|
||||
<p class="mt-2 text-gray-500">加载数据失败,请检查后端服务是否启动</p>
|
||||
<p class="text-sm text-gray-400 mt-1">${error.message}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// 添加评测维度
|
||||
window.addDimension = function() {
|
||||
window.location.href = 'model-dimension-create.html';
|
||||
};
|
||||
|
||||
// 删除评测维度
|
||||
window.deleteDimension = async function(id) {
|
||||
window.showConfirm('确认删除', '确定要删除此评测维度吗?', async () => {
|
||||
try {
|
||||
const response = await fetch(`${window.API_BASE}/dimension/${id}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
const result = await response.json();
|
||||
if (result.code === 0) {
|
||||
window.showMessage('成功', '删除成功', 'success', () => {
|
||||
switchTab(document.querySelector('[data-tab="dimensions"]'), 'dimensions');
|
||||
});
|
||||
} else {
|
||||
window.showMessage('错误', result.message || '删除失败', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除维度失败:', error);
|
||||
window.showMessage('错误', '删除失败: ' + error.message, 'error');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 切换 Tab
|
||||
window.switchTab = function(btn, tabId) {
|
||||
const parent = btn.parentElement;
|
||||
parent.querySelectorAll('.tab-btn').forEach(b => {
|
||||
b.classList.remove('active', 'text-primary');
|
||||
b.classList.add('text-gray-500');
|
||||
});
|
||||
btn.classList.add('active');
|
||||
btn.classList.remove('text-gray-500');
|
||||
|
||||
if (typeof window.switchTabContent === 'function') {
|
||||
window.switchTabContent(tabId);
|
||||
}
|
||||
|
||||
const btnContainer = document.getElementById('headerActionButtons');
|
||||
const currentConfig = window.tableConfigs[window.currentPage];
|
||||
if (btnContainer) {
|
||||
if (tabId === 'tasks') {
|
||||
const page = currentConfig?.createConfig?.page || 'model-eval-create';
|
||||
const text = currentConfig?.createConfig?.createText || '新建评测';
|
||||
btnContainer.innerHTML = `
|
||||
<button onclick="navigateToPage('${page}')" class="bg-primary text-white px-4 py-2 rounded-lg text-sm hover:bg-primary/90 transition-colors flex items-center">
|
||||
<i class="fa fa-plus mr-2"></i>${text}
|
||||
</button>
|
||||
`;
|
||||
} else if (tabId === 'leaderboard') {
|
||||
btnContainer.innerHTML = '<span class="invisible px-4 py-2 rounded-lg">占位</span>';
|
||||
} else if (tabId === 'dimensions') {
|
||||
btnContainer.innerHTML = `
|
||||
<button onclick="addDimension()" class="bg-primary text-white px-4 py-2 rounded-lg text-sm hover:bg-primary/90 transition-colors flex items-center">
|
||||
<i class="fa fa-plus mr-2"></i>添加维度
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ============ 初始化 ============
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 从 localStorage 加载自定义工具
|
||||
const savedCustomTools = localStorage.getItem('customTools');
|
||||
if (savedCustomTools) {
|
||||
window.tableConfigs['data-generate'].customTools = JSON.parse(savedCustomTools);
|
||||
}
|
||||
|
||||
// 加载模型列表缓存
|
||||
loadModelListCache();
|
||||
|
||||
// 检查URL参数
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const pageParam = urlParams.get('page');
|
||||
|
||||
let defaultPage = 'fine-tune';
|
||||
|
||||
if (pageParam) {
|
||||
defaultPage = pageParam;
|
||||
} else {
|
||||
const sessionPage = sessionStorage.getItem('lastPage');
|
||||
const localPage = localStorage.getItem('lastPage');
|
||||
const savedPage = sessionPage || localPage;
|
||||
|
||||
if (savedPage && window.tableConfigs[savedPage]) {
|
||||
defaultPage = savedPage;
|
||||
}
|
||||
}
|
||||
|
||||
sessionStorage.setItem('lastPage', defaultPage);
|
||||
|
||||
// 加载页面
|
||||
loadPage(defaultPage);
|
||||
|
||||
// 启动系统监控定时器
|
||||
SystemService.fetchSystemMetrics();
|
||||
setInterval(SystemService.fetchSystemMetrics, 30000);
|
||||
|
||||
// 启动训练进度自动刷新
|
||||
TrainingService.startProgressRefresh();
|
||||
|
||||
// 初始化日志
|
||||
const path = window.location.pathname;
|
||||
const pageName = path.split('/').pop().replace('.html', '') || 'main';
|
||||
webLogger.init(pageName);
|
||||
webLogger.info('页面加载完成');
|
||||
});
|
||||
723
web/js/pages/render.js
Normal file
723
web/js/pages/render.js
Normal file
@@ -0,0 +1,723 @@
|
||||
/**
|
||||
* 页面渲染模块
|
||||
* 包含各类型页面的渲染函数
|
||||
*/
|
||||
|
||||
// 渲染日志查看页面
|
||||
function renderLogViewerPage(config) {
|
||||
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-2">
|
||||
<button onclick="SystemService.refreshLogs()" class="px-3 py-1.5 text-sm bg-primary text-white rounded hover:bg-primary/90 transition-colors">
|
||||
<i class="fa fa-refresh mr-1"></i>刷新
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<!-- 日志类型切换 -->
|
||||
<div class="flex items-center mb-4">
|
||||
<div class="flex bg-gray-100 rounded-lg p-1">
|
||||
<button id="logTabSystem" onclick="SystemService.switchLogTab('system')" class="px-4 py-1.5 text-sm rounded-md transition-colors bg-white shadow-sm text-primary">
|
||||
系统日志
|
||||
</button>
|
||||
<button id="logTabTraining" onclick="SystemService.switchLogTab('training')" class="px-4 py-1.5 text-sm rounded-md transition-colors text-gray-600 hover:text-gray-800">
|
||||
训练日志
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 系统日志选项 -->
|
||||
<div id="systemLogOptions">
|
||||
<!-- 日期选择 -->
|
||||
<div class="flex items-center flex-wrap gap-4 mb-4">
|
||||
<div class="flex items-center">
|
||||
<label class="text-sm text-gray-600 mr-3">选择日期:</label>
|
||||
<input type="date" id="logDatePicker" class="px-3 py-1.5 border border-gray-300 rounded text-sm focus:border-primary focus:outline-none" onchange="SystemService.loadLogFiles()">
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<label class="text-sm text-gray-600 mr-3">自动刷新:</label>
|
||||
<select id="logRefreshInterval" onchange="SystemService.setRefreshInterval()" class="px-3 py-1.5 border border-gray-300 rounded text-sm focus:border-primary focus:outline-none">
|
||||
<option value="0">关闭</option>
|
||||
<option value="5">5秒</option>
|
||||
<option value="10" selected>10秒</option>
|
||||
<option value="30">30秒</option>
|
||||
<option value="60">60秒</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="logRefreshCountdown" class="text-sm text-gray-500 hidden">
|
||||
<i class="fa fa-clock-o mr-1"></i><span>下次刷新: <span id="countdownNumber">10</span>秒</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 日志类型选择 -->
|
||||
<div class="flex items-center mb-4">
|
||||
<label class="text-sm text-gray-600 mr-3">日志类型:</label>
|
||||
<select id="logTypeSelect" onchange="SystemService.loadSelectedLog()" class="px-3 py-1.5 border border-gray-300 rounded text-sm focus:border-primary focus:outline-none">
|
||||
<option value="">请选择日志文件</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 训练日志选项(初始隐藏) -->
|
||||
<div id="trainingLogOptions" class="hidden">
|
||||
<div class="flex items-center mb-4">
|
||||
<label class="text-sm text-gray-600 mr-3">训练日志:</label>
|
||||
<select id="trainingLogSelect" onchange="SystemService.loadSelectedTrainingLog()" class="px-3 py-1.5 border border-gray-300 rounded text-sm focus:border-primary focus:outline-none flex-1">
|
||||
<option value="">请选择训练日志</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 日志内容显示 -->
|
||||
<div class="border border-gray-200 rounded-lg">
|
||||
<div class="flex items-center justify-between px-4 py-2 bg-gray-50 border-b border-gray-200">
|
||||
<span class="text-sm text-gray-600" id="logFileInfo">请选择日志文件</span>
|
||||
<div class="flex items-center space-x-3">
|
||||
<input type="text" id="logSearchInput" placeholder="搜索日志..." oninput="SystemService.filterLogContent()" class="px-2 py-1 text-xs border border-gray-300 rounded focus:border-primary focus:outline-none">
|
||||
<span id="logMatchCount" class="text-xs text-gray-500"></span>
|
||||
<button onclick="SystemService.clearLogContent()" class="text-xs text-gray-500 hover:text-primary">清空</button>
|
||||
</div>
|
||||
</div>
|
||||
<pre id="logContent" class="p-4 text-xs font-mono bg-gray-900 text-gray-100 overflow-auto max-h-[600px]" style="white-space: pre-wrap; word-wrap: break-word;">日志内容将在这里显示...</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 渲染工具卡片页面
|
||||
function renderToolsPage(config) {
|
||||
const renderToolCard = (tool, canDelete = false, isCustom = false) => `
|
||||
<div class="relative group border border-gray-200 rounded-lg p-6 cursor-pointer hover:border-primary hover:shadow-md transition-all" onclick="navigateToTool('${tool.id}', '${tool.url || ''}', ${isCustom})">
|
||||
<div class="w-12 h-12 rounded-lg bg-primary/10 flex items-center justify-center mb-4 group-hover:bg-primary/20 transition-colors">
|
||||
<i class="fa ${tool.icon} text-xl text-primary"></i>
|
||||
</div>
|
||||
<h3 class="text-base font-medium text-gray-800 mb-2">${tool.name}</h3>
|
||||
<p class="text-sm text-gray-500">${tool.description}</p>
|
||||
${canDelete ? `
|
||||
<button onclick="event.stopPropagation(); editCustomTool('${tool.id}')" class="absolute top-2 right-10 w-6 h-6 rounded-full bg-gray-100 hover:bg-blue-100 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity z-10" title="修改">
|
||||
<i class="fa fa-pencil text-gray-400 hover:text-blue-500 text-xs"></i>
|
||||
</button>
|
||||
<button onclick="event.stopPropagation(); deleteCustomTool('${tool.id}')" class="absolute top-2 right-2 w-6 h-6 rounded-full bg-gray-100 hover:bg-red-100 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity z-10" title="删除">
|
||||
<i class="fa fa-times text-gray-400 hover:text-red-500 text-xs"></i>
|
||||
</button>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
|
||||
const defaultCards = config.defaultTools.map(t => renderToolCard(t, false, false)).join('');
|
||||
const customCards = config.customTools.map(t => renderToolCard(t, true, true)).join('');
|
||||
|
||||
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>
|
||||
<button onclick="showCreateToolModal()" 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>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<!-- 默认工具 -->
|
||||
<h3 class="text-sm font-semibold text-gray-700 mb-4 pb-2 border-b border-gray-100">默认工具</h3>
|
||||
<div class="grid grid-cols-3 gap-6 mb-8">
|
||||
${defaultCards || '<p class="text-gray-400 text-sm col-span-3">暂无默认工具</p>'}
|
||||
</div>
|
||||
|
||||
<!-- 自定义工具 -->
|
||||
<h3 class="text-sm font-semibold text-gray-700 mb-4 pb-2 border-b border-gray-100">自定义工具</h3>
|
||||
<div class="grid grid-cols-3 gap-6">
|
||||
${customCards || '<p class="text-gray-400 text-sm col-span-3">暂无自定义工具,点击右上角添加</p>'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 删除自定义工具
|
||||
function deleteCustomTool(toolId) {
|
||||
window.showConfirm('确认删除', '确定要删除这个自定义工具吗?', () => {
|
||||
const config = window.tableConfigs['data-generate'];
|
||||
config.customTools = config.customTools.filter(t => t.id !== toolId);
|
||||
localStorage.setItem('customTools', JSON.stringify(config.customTools));
|
||||
document.getElementById('page-content').innerHTML = renderToolsPage(config);
|
||||
});
|
||||
}
|
||||
|
||||
// 修改自定义工具
|
||||
function editCustomTool(toolId) {
|
||||
const config = window.tableConfigs['data-generate'];
|
||||
const tool = config.customTools.find(t => t.id === toolId);
|
||||
if (tool) {
|
||||
localStorage.setItem('editTool', JSON.stringify(tool));
|
||||
window.location.href = 'custom-tool-create.html?edit=true';
|
||||
}
|
||||
}
|
||||
|
||||
// 显示创建工具弹窗
|
||||
function showCreateToolModal() {
|
||||
window.location.href = 'custom-tool-create.html';
|
||||
}
|
||||
|
||||
// 跳转到工具页面
|
||||
function navigateToTool(toolId, url, isCustom = false) {
|
||||
if (isCustom && url) {
|
||||
if (url.startsWith('http')) {
|
||||
window.open(url, '_blank');
|
||||
} else {
|
||||
window.location.href = url;
|
||||
}
|
||||
} else if (toolId === 'data-generate') {
|
||||
window.location.href = 'data-generate.html';
|
||||
} else if (toolId === 'json2jsonl') {
|
||||
window.showMessage('提示', 'JSON转JSONL 工具开发中...', 'info');
|
||||
} else if (toolId === 'md-convert') {
|
||||
window.showMessage('提示', '转换Markdown 工具开发中...', 'info');
|
||||
} else {
|
||||
window.showMessage('提示', `${toolId} 功能开发中...`, 'info');
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染配置页面(硬件监控)
|
||||
function renderConfigPage(config, data) {
|
||||
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 text-sm text-gray-500">
|
||||
<i class="fa fa-refresh mr-2"></i>
|
||||
<span class="mr-2">刷新频率:</span>
|
||||
<select id="refreshInterval" onchange="changeRefreshRate()" class="px-2 py-1 border border-gray-300 rounded text-sm focus:border-primary focus:outline-none">
|
||||
<option value="1000">1秒</option>
|
||||
<option value="3000">3秒</option>
|
||||
<option value="5000" selected>5秒</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<!-- 第一行:CPU、内存、磁盘 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
|
||||
<!-- CPU监控 -->
|
||||
<div class="border border-gray-200 rounded-lg p-5">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center">
|
||||
<div class="w-10 h-10 rounded-lg bg-blue-100 flex items-center justify-center mr-3">
|
||||
<i class="fa fa-microchip text-blue-600 text-lg"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-gray-800">CPU 使用率</h3>
|
||||
<p class="text-xs text-gray-500" id="cpuCores">4 核心</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<span class="text-2xl font-medium text-gray-800" id="cpuPercent">0%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative h-4 bg-gray-100 rounded-full overflow-hidden">
|
||||
<div id="cpuBar" class="absolute left-0 top-0 h-full bg-gradient-to-r from-green-400 to-blue-500 transition-all duration-500" style="width: 0%"></div>
|
||||
</div>
|
||||
<div class="mt-4 grid grid-cols-4 gap-2" id="cpuCoresList">
|
||||
<div class="text-center p-2 bg-gray-50 rounded">
|
||||
<div class="text-xs text-gray-500">核心1</div>
|
||||
<div class="text-sm font-medium text-gray-800" id="core1">0%</div>
|
||||
</div>
|
||||
<div class="text-center p-2 bg-gray-50 rounded">
|
||||
<div class="text-xs text-gray-500">核心2</div>
|
||||
<div class="text-sm font-medium text-gray-800" id="core2">0%</div>
|
||||
</div>
|
||||
<div class="text-center p-2 bg-gray-50 rounded">
|
||||
<div class="text-xs text-gray-500">核心3</div>
|
||||
<div class="text-sm font-medium text-gray-800" id="core3">0%</div>
|
||||
</div>
|
||||
<div class="text-center p-2 bg-gray-50 rounded">
|
||||
<div class="text-xs text-gray-500">核心4</div>
|
||||
<div class="text-sm font-medium text-gray-800" id="core4">0%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 内存监控 -->
|
||||
<div class="border border-gray-200 rounded-lg p-5">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center">
|
||||
<div class="w-10 h-10 rounded-lg bg-purple-100 flex items-center justify-center mr-3">
|
||||
<i class="fa fa-database text-purple-600 text-lg"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-gray-800">内存使用</h3>
|
||||
<p class="text-xs text-gray-500" id="memoryTotal">总计: 16 GB</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<span class="text-2xl font-medium text-gray-800" id="memoryPercent">0%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative h-4 bg-gray-100 rounded-full overflow-hidden">
|
||||
<div id="memoryBar" class="absolute left-0 top-0 h-full bg-gradient-to-r from-yellow-400 to-orange-500 transition-all duration-500" style="width: 0%"></div>
|
||||
</div>
|
||||
<div class="mt-4 flex justify-between text-sm">
|
||||
<div class="text-center">
|
||||
<div class="text-xs text-gray-500">已用</div>
|
||||
<div class="text-sm font-medium text-gray-800" id="memoryUsed">0 GB</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-xs text-gray-500">可用</div>
|
||||
<div class="text-sm font-medium text-gray-800" id="memoryAvailable">0 GB</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-xs text-gray-500">缓存</div>
|
||||
<div class="text-sm font-medium text-gray-800" id="memoryCached">0 GB</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 磁盘监控 -->
|
||||
<div class="border border-gray-200 rounded-lg p-5">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center">
|
||||
<div class="w-10 h-10 rounded-lg bg-green-100 flex items-center justify-center mr-3">
|
||||
<i class="fa fa-hdd-o text-green-600 text-lg"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-gray-800">磁盘使用</h3>
|
||||
<p class="text-xs text-gray-500" id="diskTotal">SSD 512 GB</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<span class="text-2xl font-medium text-gray-800" id="diskPercent">0%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative h-4 bg-gray-100 rounded-full overflow-hidden">
|
||||
<div id="diskBar" class="absolute left-0 top-0 h-full bg-gradient-to-r from-emerald-400 to-teal-500 transition-all duration-500" style="width: 0%"></div>
|
||||
</div>
|
||||
<div class="mt-4 grid grid-cols-3 gap-2 text-center">
|
||||
<div class="p-2 bg-gray-50 rounded">
|
||||
<div class="text-xs text-gray-500">已用空间</div>
|
||||
<div class="text-sm font-medium text-gray-800" id="diskUsed">0 GB</div>
|
||||
</div>
|
||||
<div class="p-2 bg-gray-50 rounded">
|
||||
<div class="text-xs text-gray-500">可用空间</div>
|
||||
<div class="text-sm font-medium text-gray-800" id="diskAvailable">0 GB</div>
|
||||
</div>
|
||||
<div class="p-2 bg-gray-50 rounded">
|
||||
<div class="text-xs text-gray-500">读写速度</div>
|
||||
<div class="text-sm font-medium text-gray-800" id="diskSpeed">0 MB/s</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 第二行:GPU监控 -->
|
||||
<div class="mb-6">
|
||||
<div class="flex items-center mb-4">
|
||||
<div class="w-10 h-10 rounded-lg bg-red-100 flex items-center justify-center mr-3">
|
||||
<i class="fa fa-microchip text-red-600 text-lg"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-gray-800">GPU监控</h3>
|
||||
<p class="text-xs text-gray-500" id="gpuCount">多GPU并行监控</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4" id="gpuList" style="max-height: 400px; overflow-y: auto;">
|
||||
<!-- GPU卡片由initGPUList()动态生成 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 第三行:网络和系统 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- 网络流量 -->
|
||||
<div class="border border-gray-200 rounded-lg p-5">
|
||||
<div class="flex items-center mb-4">
|
||||
<div class="w-10 h-10 rounded-lg bg-cyan-100 flex items-center justify-center mr-3">
|
||||
<i class="fa fa-globe text-cyan-600 text-lg"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-gray-800">网络流量</h3>
|
||||
<p class="text-xs text-gray-500">实时带宽使用</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="p-3 bg-blue-50 rounded-lg">
|
||||
<div class="flex items-center mb-2">
|
||||
<i class="fa fa-arrow-down text-blue-600 mr-2"></i>
|
||||
<span class="text-xs text-gray-600">下载速度</span>
|
||||
</div>
|
||||
<div class="text-lg font-medium text-gray-800" id="downloadSpeed">0 MB/s</div>
|
||||
</div>
|
||||
<div class="p-3 bg-green-50 rounded-lg">
|
||||
<div class="flex items-center mb-2">
|
||||
<i class="fa fa-arrow-up text-green-600 mr-2"></i>
|
||||
<span class="text-xs text-gray-600">上传速度</span>
|
||||
</div>
|
||||
<div class="text-lg font-medium text-gray-800" id="uploadSpeed">0 MB/s</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 flex justify-between text-xs text-gray-500">
|
||||
<span>总流入: <span id="totalDownload">0 GB</span></span>
|
||||
<span>总流出: <span id="totalUpload">0 GB</span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 系统信息 -->
|
||||
<div class="border border-gray-200 rounded-lg p-5">
|
||||
<div class="flex items-center mb-4">
|
||||
<div class="w-10 h-10 rounded-lg bg-indigo-100 flex items-center justify-center mr-3">
|
||||
<i class="fa fa-info-circle text-indigo-600 text-lg"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-gray-800">系统信息</h3>
|
||||
<p class="text-xs text-gray-500">服务器状态</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-3 text-sm">
|
||||
<div class="flex justify-between py-2 border-b border-gray-100">
|
||||
<span class="text-gray-500">操作系统</span>
|
||||
<span class="text-gray-800 font-medium" id="osInfo">Ubuntu 22.04 LTS</span>
|
||||
</div>
|
||||
<div class="flex justify-between py-2 border-b border-gray-100">
|
||||
<span class="text-gray-500">运行时间</span>
|
||||
<span class="text-gray-800 font-medium" id="uptime">0 天 0 时 0 分</span>
|
||||
</div>
|
||||
<div class="flex justify-between py-2 border-b border-gray-100">
|
||||
<span class="text-gray-500">进程数</span>
|
||||
<span class="text-gray-800 font-medium" id="processCount">0</span>
|
||||
</div>
|
||||
<div class="flex justify-between py-2">
|
||||
<span class="text-gray-500">负载均值</span>
|
||||
<span class="text-gray-800 font-medium" id="loadAvg">0.00, 0.00, 0.00</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 刷新间隔定时器
|
||||
let refreshTimer = null;
|
||||
let currentRefreshInterval = 5000;
|
||||
|
||||
// 刷新硬件信息
|
||||
async function refreshHardwareInfo() {
|
||||
try {
|
||||
const response = await fetch(`${window.API_BASE}/system-info`);
|
||||
const result = await response.json();
|
||||
|
||||
if (result.code === 0 && result.data) {
|
||||
const data = result.data;
|
||||
|
||||
// 更新CPU
|
||||
const cpu = data.cpu || {};
|
||||
const cpuPercent = cpu.percent || 0;
|
||||
const cpuEl = document.getElementById('cpuPercent');
|
||||
if (cpuEl) {
|
||||
cpuEl.textContent = cpuPercent + '%';
|
||||
document.getElementById('cpuBar').style.width = cpuPercent + '%';
|
||||
document.getElementById('cpuCores').textContent = (cpu.cores || 0) + ' 核心';
|
||||
}
|
||||
|
||||
// 更新内存
|
||||
const mem = data.memory || {};
|
||||
const memUsed = mem.used_gb || 0;
|
||||
const memTotal = mem.total_gb || 0;
|
||||
const memPercent = mem.percent || 0;
|
||||
document.getElementById('memoryPercent').textContent = memPercent + '%';
|
||||
document.getElementById('memoryBar').style.width = memPercent + '%';
|
||||
document.getElementById('memoryUsed').textContent = memUsed + ' GB';
|
||||
document.getElementById('memoryAvailable').textContent = (mem.available_gb || 0) + ' GB';
|
||||
document.getElementById('memoryCached').textContent = (mem.cached_gb || 0) + ' GB';
|
||||
|
||||
// 更新磁盘
|
||||
const disk = data.disk || {};
|
||||
const diskUsed = disk.used_gb || 0;
|
||||
const diskTotal = disk.total_gb || 0;
|
||||
const diskPercent = disk.percent || 0;
|
||||
document.getElementById('diskPercent').textContent = diskPercent + '%';
|
||||
document.getElementById('diskBar').style.width = diskPercent + '%';
|
||||
document.getElementById('diskUsed').textContent = diskUsed + ' GB';
|
||||
document.getElementById('diskAvailable').textContent = (diskTotal - diskUsed) + ' GB';
|
||||
|
||||
// 更新网络
|
||||
const net = data.network || {};
|
||||
document.getElementById('totalDownload').textContent = (net.download_mb || 0) + ' GB';
|
||||
document.getElementById('totalUpload').textContent = (net.upload_mb || 0) + ' GB';
|
||||
|
||||
// 更新系统信息
|
||||
const sys = data.system || {};
|
||||
const uptime = sys.uptime_seconds || 0;
|
||||
const days = Math.floor(uptime / 86400);
|
||||
const hours = Math.floor((uptime % 86400) / 3600);
|
||||
const mins = Math.floor((uptime % 3600) / 60);
|
||||
document.getElementById('uptime').textContent = days + ' 天 ' + hours + ' 时 ' + mins + ' 分';
|
||||
document.getElementById('processCount').textContent = sys.process_count || 0;
|
||||
|
||||
// 更新GPU信息
|
||||
updateGPUInfo(data.gpu || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取系统信息失败:', error);
|
||||
useMockData();
|
||||
}
|
||||
}
|
||||
|
||||
// 使用模拟数据
|
||||
function useMockData() {
|
||||
const cpuUsage = Math.floor(Math.random() * 30) + 20;
|
||||
document.getElementById('cpuPercent').textContent = cpuUsage + '%';
|
||||
document.getElementById('cpuBar').style.width = cpuUsage + '%';
|
||||
document.getElementById('core1').textContent = Math.floor(Math.random() * 40 + 20) + '%';
|
||||
document.getElementById('core2').textContent = Math.floor(Math.random() * 40 + 15) + '%';
|
||||
document.getElementById('core3').textContent = Math.floor(Math.random() * 40 + 25) + '%';
|
||||
document.getElementById('core4').textContent = Math.floor(Math.random() * 40 + 10) + '%';
|
||||
|
||||
const memUsed = (Math.random() * 4 + 6).toFixed(1);
|
||||
const memTotal = 16;
|
||||
const memPercent = Math.floor((memUsed / memTotal) * 100);
|
||||
document.getElementById('memoryPercent').textContent = memPercent + '%';
|
||||
document.getElementById('memoryBar').style.width = memPercent + '%';
|
||||
document.getElementById('memoryUsed').textContent = memUsed + ' GB';
|
||||
document.getElementById('memoryAvailable').textContent = (memTotal - memUsed).toFixed(1) + ' GB';
|
||||
document.getElementById('memoryCached').textContent = (Math.random() * 3 + 1).toFixed(1) + ' GB';
|
||||
|
||||
const diskUsed = Math.floor(Math.random() * 100 + 150);
|
||||
const diskTotal = 512;
|
||||
const diskPercent = Math.floor((diskUsed / diskTotal) * 100);
|
||||
document.getElementById('diskPercent').textContent = diskPercent + '%';
|
||||
document.getElementById('diskBar').style.width = diskPercent + '%';
|
||||
document.getElementById('diskUsed').textContent = diskUsed + ' GB';
|
||||
document.getElementById('diskAvailable').textContent = (diskTotal - diskUsed) + ' GB';
|
||||
document.getElementById('diskSpeed').textContent = (Math.random() * 500 + 100).toFixed(0) + ' MB/s';
|
||||
|
||||
updateGPUInfo();
|
||||
|
||||
document.getElementById('downloadSpeed').textContent = (Math.random() * 100 + 10).toFixed(1) + ' MB/s';
|
||||
document.getElementById('uploadSpeed').textContent = (Math.random() * 50 + 5).toFixed(1) + ' MB/s';
|
||||
document.getElementById('totalDownload').textContent = (Math.random() * 500 + 100).toFixed(1) + ' GB';
|
||||
document.getElementById('totalUpload').textContent = (Math.random() * 200 + 50).toFixed(1) + ' GB';
|
||||
|
||||
const days = Math.floor(Math.random() * 30);
|
||||
const hours = Math.floor(Math.random() * 24);
|
||||
const mins = Math.floor(Math.random() * 60);
|
||||
document.getElementById('uptime').textContent = days + ' 天 ' + hours + ' 时 ' + mins + ' 分';
|
||||
document.getElementById('processCount').textContent = Math.floor(Math.random() * 200 + 100);
|
||||
document.getElementById('loadAvg').textContent = (Math.random() * 2).toFixed(2) + ', ' + (Math.random() * 1.5).toFixed(2) + ', ' + (Math.random() * 1).toFixed(2);
|
||||
}
|
||||
|
||||
// GPU配置
|
||||
const GPU_COUNT = 4;
|
||||
const gpuConfigs = [
|
||||
{ name: 'NVIDIA RTX 3090', memory: 24 },
|
||||
{ name: 'NVIDIA RTX 4090', memory: 24 },
|
||||
{ name: 'NVIDIA A100', memory: 80 },
|
||||
{ name: 'NVIDIA V100', memory: 32 },
|
||||
{ name: 'NVIDIA T4', memory: 16 },
|
||||
{ name: 'NVIDIA L40S', memory: 48 },
|
||||
{ name: 'NVIDIA H100', memory: 80 },
|
||||
{ name: 'NVIDIA RTX 4080', memory: 16 }
|
||||
];
|
||||
|
||||
// 初始化GPU列表
|
||||
async function initGPUList() {
|
||||
try {
|
||||
const response = await fetch(`${window.API_BASE}/system-info`);
|
||||
const result = await response.json();
|
||||
const gpuData = (result.data && result.data.gpu) || [];
|
||||
updateGPUInfo(gpuData);
|
||||
} catch (error) {
|
||||
console.error('初始化GPU列表失败:', error);
|
||||
useMockGPUData();
|
||||
}
|
||||
}
|
||||
|
||||
// 更新GPU信息
|
||||
function updateGPUInfo(gpuData) {
|
||||
if (gpuData && gpuData.length > 0) {
|
||||
const gpuCount = gpuData.length;
|
||||
document.getElementById('gpuCount').textContent = `检测到 ${gpuCount} 块 GPU`;
|
||||
|
||||
let totalUsedMemory = 0;
|
||||
let totalMemory = 0;
|
||||
|
||||
const gpuList = document.getElementById('gpuList');
|
||||
if (gpuList) {
|
||||
let gpuCardsHTML = '';
|
||||
for (let i = 0; i < gpuCount; i++) {
|
||||
const gpu = gpuData[i];
|
||||
totalUsedMemory += gpu.memory_used_gb;
|
||||
totalMemory += gpu.memory_total_gb;
|
||||
|
||||
gpuCardsHTML += `
|
||||
<div class="border border-gray-200 rounded-lg p-2 bg-gradient-to-br from-gray-50 to-gray-100" id="gpuCard${i}">
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<div class="flex items-center min-w-0">
|
||||
<div class="w-6 h-6 rounded bg-red-100 flex items-center justify-center mr-2 flex-shrink-0">
|
||||
<i class="fa fa-microchip text-red-600 text-xs"></i>
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<div class="text-xs font-medium text-gray-800 truncate" id="gpuName${i}" title="${gpu.name}">${gpu.name}</div>
|
||||
<div class="text-[10px] text-gray-400">PCIe</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right flex-shrink-0 ml-2">
|
||||
<span class="text-sm font-bold text-gray-800" id="gpuPercent${i}">${gpu.gpu_percent}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative h-1.5 bg-gray-200 rounded-full overflow-hidden mb-2">
|
||||
<div id="gpuBar${i}" class="absolute left-0 top-0 h-full bg-gradient-to-r from-green-400 via-yellow-400 to-red-400 transition-all duration-500" style="width: ${gpu.gpu_percent}%"></div>
|
||||
</div>
|
||||
<div class="grid grid-cols-4 gap-1 text-center text-[10px]">
|
||||
<div class="bg-white/80 rounded py-0.5">
|
||||
<div class="text-gray-400">显存</div>
|
||||
<div class="font-medium text-gray-700" id="gpuMem${i}">${gpu.memory_used_gb}/${gpu.memory_total_gb} GB</div>
|
||||
</div>
|
||||
<div class="bg-white/80 rounded py-0.5">
|
||||
<div class="text-gray-400">温度</div>
|
||||
<div class="font-medium ${gpu.temperature >= 80 ? 'text-red-600' : gpu.temperature >= 70 ? 'text-yellow-600' : 'text-gray-800'}" id="gpuTemp${i}">${gpu.temperature}°C</div>
|
||||
</div>
|
||||
<div class="bg-white/80 rounded py-0.5">
|
||||
<div class="text-gray-400">功耗</div>
|
||||
<div class="font-medium text-gray-700" id="gpuPower${i}">${gpu.power_w} W</div>
|
||||
</div>
|
||||
<div class="bg-white/80 rounded py-0.5">
|
||||
<div class="text-gray-400">Fan</div>
|
||||
<div class="font-medium text-gray-700" id="gpuFan${i}">${gpu.fan_speed || 0}%</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-1 grid grid-cols-2 gap-1 text-center text-[9px] text-gray-400">
|
||||
<div>Clock: <span id="gpuClock${i}">${gpu.clock_mhz || 0} MHz</span></div>
|
||||
<div>Driver: <span id="gpuDriver${i}">${gpu.driver_version || '-'}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
// 如果GPU数量不足4个,补充显示总显存
|
||||
if (gpuCount < 4) {
|
||||
gpuCardsHTML += `
|
||||
<div class="border border-gray-200 rounded-lg p-2 bg-gradient-to-br from-gray-50 to-gray-100">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<div class="w-6 h-6 rounded bg-gray-200 flex items-center justify-center mr-2">
|
||||
<i class="fa fa-server text-gray-600 text-xs"></i>
|
||||
</div>
|
||||
<div class="text-xs font-medium text-gray-600">总显存使用</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<span class="text-sm font-bold text-gray-800" id="gpuTotalMemory">${totalUsedMemory}/${totalMemory} GB</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
gpuList.innerHTML = gpuCardsHTML;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
useMockGPUData();
|
||||
}
|
||||
|
||||
// 使用模拟GPU数据
|
||||
function useMockGPUData() {
|
||||
const gpuCount = Math.min(GPU_COUNT, 8);
|
||||
let totalUsedMemory = 0;
|
||||
let totalMemory = 0;
|
||||
|
||||
const gpuList = document.getElementById('gpuList');
|
||||
if (gpuList) {
|
||||
let gpuCardsHTML = '';
|
||||
for (let i = 0; i < gpuCount; i++) {
|
||||
const config = gpuConfigs[i % gpuConfigs.length];
|
||||
const gpuUsage = Math.floor(Math.random() * 60 + 20);
|
||||
const memUsed = (Math.random() * config.memory * 0.7 + config.memory * 0.1).toFixed(1);
|
||||
const temp = Math.floor(Math.random() * 30 + 40);
|
||||
const power = Math.floor(Math.random() * 150 + 100);
|
||||
const fan = Math.floor(gpuUsage + Math.random() * 10);
|
||||
|
||||
totalUsedMemory += parseFloat(memUsed);
|
||||
totalMemory += config.memory;
|
||||
|
||||
gpuCardsHTML += `
|
||||
<div class="border border-gray-200 rounded-lg p-2 bg-gradient-to-br from-gray-50 to-gray-100" id="gpuCard${i}">
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<div class="flex items-center min-w-0">
|
||||
<div class="w-6 h-6 rounded bg-red-100 flex items-center justify-center mr-2 flex-shrink-0">
|
||||
<i class="fa fa-microchip text-red-600 text-xs"></i>
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<div class="text-xs font-medium text-gray-800 truncate" id="gpuName${i}" title="${config.name}">${config.name}</div>
|
||||
<div class="text-[10px] text-gray-400">PCIe ${Math.floor(Math.random() * 4 + 1)}:00.0</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right flex-shrink-0 ml-2">
|
||||
<span class="text-sm font-bold text-gray-800" id="gpuPercent${i}">${gpuUsage}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative h-1.5 bg-gray-200 rounded-full overflow-hidden mb-2">
|
||||
<div id="gpuBar${i}" class="absolute left-0 top-0 h-full bg-gradient-to-r from-green-400 via-yellow-400 to-red-400 transition-all duration-500" style="width: ${gpuUsage}%"></div>
|
||||
</div>
|
||||
<div class="grid grid-cols-4 gap-1 text-center text-[10px]">
|
||||
<div class="bg-white/80 rounded py-0.5">
|
||||
<div class="text-gray-400">显存</div>
|
||||
<div class="font-medium text-gray-700" id="gpuMem${i}">${parseFloat(memUsed).toFixed(1)}/${config.memory} GB</div>
|
||||
</div>
|
||||
<div class="bg-white/80 rounded py-0.5">
|
||||
<div class="text-gray-400">温度</div>
|
||||
<div class="font-medium ${temp >= 80 ? 'text-red-600' : temp >= 70 ? 'text-yellow-600' : 'text-gray-800'}" id="gpuTemp${i}">${temp}°C</div>
|
||||
</div>
|
||||
<div class="bg-white/80 rounded py-0.5">
|
||||
<div class="text-gray-400">功耗</div>
|
||||
<div class="font-medium text-gray-700" id="gpuPower${i}">${power} W</div>
|
||||
</div>
|
||||
<div class="bg-white/80 rounded py-0.5">
|
||||
<div class="text-gray-400">Fan</div>
|
||||
<div class="font-medium text-gray-700" id="gpuFan${i}">${fan}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
gpuList.innerHTML = gpuCardsHTML;
|
||||
document.getElementById('gpuCount').textContent = `检测到 ${gpuCount} 块 GPU`;
|
||||
}
|
||||
|
||||
const gpuTotalMem = document.getElementById('gpuTotalMemory');
|
||||
if (gpuTotalMem) {
|
||||
gpuTotalMem.textContent = `${totalUsedMemory.toFixed(1)}/${totalMemory} GB`;
|
||||
}
|
||||
}
|
||||
|
||||
// 启动硬件监控自动刷新
|
||||
function startRefreshTimer() {
|
||||
if (refreshTimer) {
|
||||
clearInterval(refreshTimer);
|
||||
}
|
||||
refreshTimer = setInterval(refreshHardwareInfo, currentRefreshInterval);
|
||||
}
|
||||
|
||||
// 改变刷新频率
|
||||
function changeRefreshRate() {
|
||||
const select = document.getElementById('refreshInterval');
|
||||
currentRefreshInterval = parseInt(select.value);
|
||||
startRefreshTimer();
|
||||
}
|
||||
|
||||
// 保存配置
|
||||
function saveConfig() {
|
||||
window.showMessage('提示', '配置保存功能开发中...', 'info');
|
||||
}
|
||||
|
||||
// 导出页面渲染模块
|
||||
window.PageRenderer = {
|
||||
renderLogViewerPage,
|
||||
renderToolsPage,
|
||||
renderConfigPage,
|
||||
refreshHardwareInfo,
|
||||
useMockData,
|
||||
initGPUList,
|
||||
updateGPUInfo,
|
||||
useMockGPUData,
|
||||
startRefreshTimer,
|
||||
changeRefreshRate,
|
||||
saveConfig
|
||||
};
|
||||
392
web/js/services/system.js
Normal file
392
web/js/services/system.js
Normal file
@@ -0,0 +1,392 @@
|
||||
/**
|
||||
* 系统监控服务
|
||||
* 处理系统性能指标获取和展示
|
||||
*/
|
||||
|
||||
// 日志自动刷新相关变量
|
||||
let logRefreshTimer = null;
|
||||
let logCountdownTimer = null;
|
||||
let logCurrentInterval = 10;
|
||||
let logFullContent = '';
|
||||
|
||||
// 设置自动刷新间隔
|
||||
function setRefreshInterval() {
|
||||
const select = document.getElementById('logRefreshInterval');
|
||||
const countdownEl = document.getElementById('logRefreshCountdown');
|
||||
const secondsEl = document.getElementById('countdownNumber');
|
||||
|
||||
if (!select) return;
|
||||
|
||||
logCurrentInterval = parseInt(select.value) || 10;
|
||||
|
||||
// 清除之前的定时器
|
||||
if (logRefreshTimer) {
|
||||
clearInterval(logRefreshTimer);
|
||||
logRefreshTimer = null;
|
||||
}
|
||||
if (logCountdownTimer) {
|
||||
clearInterval(logCountdownTimer);
|
||||
logCountdownTimer = null;
|
||||
}
|
||||
|
||||
// 如果选择关闭,不显示倒计时
|
||||
if (select.value === '0') {
|
||||
countdownEl.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示倒计时
|
||||
countdownEl.classList.remove('hidden');
|
||||
secondsEl.textContent = logCurrentInterval;
|
||||
|
||||
// 启动倒计时
|
||||
let countdown = logCurrentInterval;
|
||||
logCountdownTimer = setInterval(() => {
|
||||
countdown--;
|
||||
if (countdown <= 0) {
|
||||
countdown = logCurrentInterval;
|
||||
}
|
||||
secondsEl.textContent = countdown;
|
||||
}, 1000);
|
||||
|
||||
// 启动自动刷新
|
||||
logRefreshTimer = setInterval(() => {
|
||||
if (typeof refreshLogs === 'function') {
|
||||
refreshLogs();
|
||||
}
|
||||
}, logCurrentInterval * 1000);
|
||||
}
|
||||
|
||||
// 获取系统性能监控数据
|
||||
async function fetchSystemMetrics() {
|
||||
try {
|
||||
const response = await fetch(`${window.API_BASE}/health`);
|
||||
const result = await response.json();
|
||||
if (result.code === 0 && result.data) {
|
||||
const data = result.data;
|
||||
// 更新CPU使用率
|
||||
const cpuEl = document.getElementById('cpuUsage');
|
||||
if (cpuEl && data.cpu_percent !== undefined) {
|
||||
cpuEl.textContent = data.cpu_percent;
|
||||
cpuEl.className = data.cpu_percent > 80 ? 'text-red-500 font-medium' : '';
|
||||
}
|
||||
// 更新内存使用率
|
||||
const memEl = document.getElementById('memUsage');
|
||||
if (memEl && data.memory_percent !== undefined) {
|
||||
memEl.textContent = data.memory_percent;
|
||||
memEl.className = data.memory_percent > 80 ? 'text-red-500 font-medium' : '';
|
||||
}
|
||||
// 更新磁盘使用率
|
||||
const diskEl = document.getElementById('diskUsage');
|
||||
if (diskEl && data.disk_percent !== undefined) {
|
||||
diskEl.textContent = data.disk_percent;
|
||||
diskEl.className = data.disk_percent > 80 ? 'text-red-500 font-medium' : '';
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取系统监控数据失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 停止日志自动刷新(离开页面时调用)
|
||||
function stopLogAutoRefresh() {
|
||||
if (logRefreshTimer) {
|
||||
clearInterval(logRefreshTimer);
|
||||
logRefreshTimer = null;
|
||||
}
|
||||
if (logCountdownTimer) {
|
||||
clearInterval(logCountdownTimer);
|
||||
logCountdownTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新日志
|
||||
function refreshLogs() {
|
||||
if (typeof switchLogTab === 'function') {
|
||||
const currentLogTab = window.currentLogTab || 'system';
|
||||
if (currentLogTab === 'system') {
|
||||
if (typeof loadLogFiles === 'function' && document.getElementById('logTypeSelect')?.value) {
|
||||
loadSelectedLog();
|
||||
} else {
|
||||
loadLogFiles();
|
||||
}
|
||||
} else {
|
||||
loadTrainingLogFiles();
|
||||
}
|
||||
} else {
|
||||
loadLogFiles();
|
||||
}
|
||||
// 重置倒计时
|
||||
const select = document.getElementById('logRefreshInterval');
|
||||
const secondsEl = document.getElementById('countdownNumber');
|
||||
if (select && select.value !== '0' && secondsEl) {
|
||||
secondsEl.textContent = logCurrentInterval;
|
||||
}
|
||||
}
|
||||
|
||||
// 滚动到日志底部
|
||||
function scrollToLogBottom() {
|
||||
const logContent = document.getElementById('logContent');
|
||||
if (logContent) {
|
||||
logContent.scrollTop = logContent.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
// 当前日志类型:system 或 training
|
||||
window.currentLogTab = 'system';
|
||||
|
||||
// 切换日志类型标签
|
||||
function switchLogTab(tab) {
|
||||
window.currentLogTab = tab;
|
||||
const systemTab = document.getElementById('logTabSystem');
|
||||
const trainingTab = document.getElementById('logTabTraining');
|
||||
const systemOptions = document.getElementById('systemLogOptions');
|
||||
const trainingOptions = document.getElementById('trainingLogOptions');
|
||||
|
||||
if (tab === 'system') {
|
||||
systemTab.className = 'px-4 py-1.5 text-sm rounded-md transition-colors bg-white shadow-sm text-primary';
|
||||
trainingTab.className = 'px-4 py-1.5 text-sm rounded-md transition-colors text-gray-600 hover:text-gray-800';
|
||||
systemOptions.classList.remove('hidden');
|
||||
trainingOptions.classList.add('hidden');
|
||||
loadLogFiles();
|
||||
} else {
|
||||
trainingTab.className = 'px-4 py-1.5 text-sm rounded-md transition-colors bg-white shadow-sm text-primary';
|
||||
systemTab.className = 'px-4 py-1.5 text-sm rounded-md transition-colors text-gray-600 hover:text-gray-800';
|
||||
trainingOptions.classList.remove('hidden');
|
||||
systemOptions.classList.add('hidden');
|
||||
loadTrainingLogFiles();
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化日志查看器
|
||||
function initLogViewer() {
|
||||
const datePicker = document.getElementById('logDatePicker');
|
||||
if (datePicker) {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
datePicker.value = today;
|
||||
}
|
||||
// 加载默认日志类型
|
||||
loadLogFiles();
|
||||
// 启动自动刷新
|
||||
setRefreshInterval();
|
||||
}
|
||||
|
||||
// 加载训练日志文件列表
|
||||
async function loadTrainingLogFiles() {
|
||||
const logSelect = document.getElementById('trainingLogSelect');
|
||||
if (!logSelect) return;
|
||||
|
||||
logSelect.innerHTML = '<option value="">加载中...</option>';
|
||||
|
||||
try {
|
||||
const response = await fetch(`${window.API_BASE}/training-log-files`);
|
||||
const result = await response.json();
|
||||
|
||||
if (result.code === 0 && result.data) {
|
||||
logSelect.innerHTML = '<option value="">请选择训练日志</option>';
|
||||
result.data.forEach(log => {
|
||||
const option = document.createElement('option');
|
||||
option.value = log.file;
|
||||
option.textContent = `${log.name} (PID: ${log.pid}, ${log.date}, ${log.size})`;
|
||||
logSelect.appendChild(option);
|
||||
});
|
||||
// 如果有日志文件,自动加载第一个
|
||||
if (result.data.length > 0) {
|
||||
logSelect.value = result.data[0].file;
|
||||
loadSelectedTrainingLog();
|
||||
} else {
|
||||
document.getElementById('logContent').textContent = '暂无训练日志';
|
||||
document.getElementById('logFileInfo').textContent = '无训练日志';
|
||||
}
|
||||
} else {
|
||||
logSelect.innerHTML = '<option value="">暂无训练日志</option>';
|
||||
document.getElementById('logContent').textContent = '暂无训练日志';
|
||||
document.getElementById('logFileInfo').textContent = '无训练日志';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载训练日志列表失败:', error);
|
||||
logSelect.innerHTML = '<option value="">加载失败</option>';
|
||||
document.getElementById('logContent').textContent = '加载训练日志列表失败: ' + error.message;
|
||||
}
|
||||
}
|
||||
|
||||
// 加载选中的训练日志
|
||||
async function loadSelectedTrainingLog() {
|
||||
const logSelect = document.getElementById('trainingLogSelect');
|
||||
const logFile = logSelect.value;
|
||||
const logContent = document.getElementById('logContent');
|
||||
const logFileInfo = document.getElementById('logFileInfo');
|
||||
|
||||
if (!logFile) {
|
||||
logContent.textContent = '请选择训练日志';
|
||||
logFileInfo.textContent = '无训练日志';
|
||||
return;
|
||||
}
|
||||
|
||||
logContent.textContent = '加载中...';
|
||||
logFileInfo.textContent = '加载中...';
|
||||
|
||||
try {
|
||||
const response = await fetch(`${window.API_BASE}/training-log-content?file=${encodeURIComponent(logFile)}`);
|
||||
const result = await response.json();
|
||||
|
||||
if (result.code === 0 && result.data) {
|
||||
logFullContent = result.data.content || '';
|
||||
logContent.textContent = logFullContent || '(空日志)';
|
||||
logFileInfo.textContent = result.data.file + ' (' + result.data.size + ')';
|
||||
// 清空搜索
|
||||
document.getElementById('logSearchInput').value = '';
|
||||
document.getElementById('logMatchCount').textContent = '';
|
||||
// 滚动到底部
|
||||
scrollToLogBottom();
|
||||
} else {
|
||||
logContent.textContent = '加载失败: ' + (result.message || '未知错误');
|
||||
logFileInfo.textContent = '加载失败';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载训练日志内容失败:', error);
|
||||
logContent.textContent = '加载失败: ' + error.message;
|
||||
logFileInfo.textContent = '加载失败';
|
||||
}
|
||||
}
|
||||
|
||||
// 加载日志文件列表
|
||||
async function loadLogFiles() {
|
||||
const datePicker = document.getElementById('logDatePicker');
|
||||
const logTypeSelect = document.getElementById('logTypeSelect');
|
||||
const selectedDate = datePicker ? datePicker.value : new Date().toISOString().split('T')[0];
|
||||
|
||||
if (!logTypeSelect) return;
|
||||
logTypeSelect.innerHTML = '<option value="">加载中...</option>';
|
||||
|
||||
try {
|
||||
const response = await fetch(`${window.API_BASE}/log-files?date=${selectedDate}`);
|
||||
const result = await response.json();
|
||||
|
||||
if (result.code === 0 && result.data) {
|
||||
logTypeSelect.innerHTML = '<option value="">请选择日志文件</option>';
|
||||
result.data.forEach(log => {
|
||||
const option = document.createElement('option');
|
||||
option.value = log.file;
|
||||
option.textContent = log.name + ' (' + log.size + ')';
|
||||
logTypeSelect.appendChild(option);
|
||||
});
|
||||
// 如果有日志文件,自动加载第一个
|
||||
if (result.data.length > 0) {
|
||||
logTypeSelect.value = result.data[0].file;
|
||||
loadSelectedLog();
|
||||
} else {
|
||||
logTypeSelect.innerHTML = '<option value="">暂无日志文件</option>';
|
||||
document.getElementById('logContent').textContent = '该日期暂无日志文件';
|
||||
document.getElementById('logFileInfo').textContent = '无日志文件';
|
||||
}
|
||||
} else {
|
||||
logTypeSelect.innerHTML = '<option value="">暂无日志文件</option>';
|
||||
document.getElementById('logContent').textContent = '该日期暂无日志文件';
|
||||
document.getElementById('logFileInfo').textContent = '无日志文件';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载日志文件列表失败:', error);
|
||||
logTypeSelect.innerHTML = '<option value="">加载失败</option>';
|
||||
document.getElementById('logContent').textContent = '加载日志文件列表失败: ' + error.message;
|
||||
}
|
||||
}
|
||||
|
||||
// 加载选中的日志
|
||||
async function loadSelectedLog() {
|
||||
const logTypeSelect = document.getElementById('logTypeSelect');
|
||||
const logFile = logTypeSelect.value;
|
||||
const logContent = document.getElementById('logContent');
|
||||
const logFileInfo = document.getElementById('logFileInfo');
|
||||
|
||||
if (!logFile) {
|
||||
logContent.textContent = '请选择日志文件';
|
||||
logFileInfo.textContent = '无日志文件';
|
||||
return;
|
||||
}
|
||||
|
||||
logContent.textContent = '加载中...';
|
||||
logFileInfo.textContent = '加载中...';
|
||||
|
||||
try {
|
||||
const response = await fetch(`${window.API_BASE}/log-content?file=${encodeURIComponent(logFile)}`);
|
||||
const result = await response.json();
|
||||
|
||||
if (result.code === 0) {
|
||||
logFullContent = result.data.content || '';
|
||||
logContent.textContent = logFullContent || '(空日志)';
|
||||
logFileInfo.textContent = result.data.file + ' (' + result.data.size + ')';
|
||||
// 清空搜索框和匹配计数
|
||||
document.getElementById('logSearchInput').value = '';
|
||||
document.getElementById('logMatchCount').textContent = '';
|
||||
// 滚动到最底部
|
||||
scrollToLogBottom();
|
||||
} else {
|
||||
logContent.textContent = '加载失败: ' + (result.message || '未知错误');
|
||||
logFileInfo.textContent = '加载失败';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载日志内容失败:', error);
|
||||
logContent.textContent = '加载日志内容失败: ' + error.message;
|
||||
logFileInfo.textContent = '加载失败';
|
||||
}
|
||||
}
|
||||
|
||||
// 过滤日志内容
|
||||
function filterLogContent() {
|
||||
const searchInput = document.getElementById('logSearchInput');
|
||||
const matchCount = document.getElementById('logMatchCount');
|
||||
const logContent = document.getElementById('logContent');
|
||||
|
||||
if (!searchInput || !matchCount || !logContent) return;
|
||||
|
||||
const keyword = searchInput.value.trim();
|
||||
|
||||
if (!keyword) {
|
||||
logContent.textContent = logFullContent || '(空日志)';
|
||||
matchCount.textContent = '';
|
||||
scrollToLogBottom();
|
||||
return;
|
||||
}
|
||||
|
||||
const lines = logFullContent.split('\n');
|
||||
const matchingLines = lines.filter(line => line.toLowerCase().includes(keyword.toLowerCase()));
|
||||
|
||||
if (matchingLines.length > 0) {
|
||||
logContent.textContent = matchingLines.join('\n');
|
||||
matchCount.textContent = `(${matchingLines.length}条匹配)`;
|
||||
// 滚动到最底部查看最新匹配
|
||||
scrollToLogBottom();
|
||||
} else {
|
||||
logContent.textContent = '未找到匹配的日志';
|
||||
matchCount.textContent = '(0条匹配)';
|
||||
}
|
||||
}
|
||||
|
||||
// 清空日志内容显示
|
||||
function clearLogContent() {
|
||||
document.getElementById('logContent').textContent = '日志内容将在这里显示...';
|
||||
document.getElementById('logFileInfo').textContent = '请选择日志文件';
|
||||
const logTypeSelect = document.getElementById('logTypeSelect');
|
||||
if (logTypeSelect) logTypeSelect.value = '';
|
||||
document.getElementById('logSearchInput').value = '';
|
||||
document.getElementById('logMatchCount').textContent = '';
|
||||
logFullContent = '';
|
||||
}
|
||||
|
||||
// 导出服务函数
|
||||
window.SystemService = {
|
||||
fetchSystemMetrics,
|
||||
setRefreshInterval,
|
||||
stopLogAutoRefresh,
|
||||
refreshLogs,
|
||||
scrollToLogBottom,
|
||||
switchLogTab,
|
||||
initLogViewer,
|
||||
loadTrainingLogFiles,
|
||||
loadSelectedTrainingLog,
|
||||
loadLogFiles,
|
||||
loadSelectedLog,
|
||||
filterLogContent,
|
||||
clearLogContent
|
||||
};
|
||||
201
web/js/services/training.js
Normal file
201
web/js/services/training.js
Normal file
@@ -0,0 +1,201 @@
|
||||
/**
|
||||
* 训练服务模块
|
||||
* 处理训练任务相关的操作和进度跟踪
|
||||
*/
|
||||
|
||||
// 训练进度缓存
|
||||
window.trainingProgressCache = window.trainingProgressCache || {};
|
||||
// 使用 window 避免重复声明
|
||||
if (typeof window._progressRefreshTimer === 'undefined') {
|
||||
window._progressRefreshTimer = null;
|
||||
}
|
||||
|
||||
// 渲染训练进度
|
||||
function renderTrainingProgress(val, row) {
|
||||
const progressData = window.trainingProgressCache[row.id];
|
||||
if (progressData && progressData.status === 'running') {
|
||||
if (progressData.progress > 0) {
|
||||
return `
|
||||
<div class="flex flex-col">
|
||||
<span class="text-sm font-medium text-primary">${progressData.progress}%</span>
|
||||
<span class="text-xs text-gray-500">${progressData.step || ''} ${progressData.speed || ''}</span>
|
||||
<span class="text-xs text-gray-400">ETA: ${progressData.eta || '--:--'}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
return `${val || 0}%`;
|
||||
}
|
||||
|
||||
// 刷新训练进度
|
||||
async function refreshTrainingProgress() {
|
||||
if (typeof window.currentPage !== 'string' || window.currentPage !== 'fine-tune') return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${window.API_BASE}/fine-tune`);
|
||||
const result = await response.json();
|
||||
|
||||
if (result.code === 0 && result.data) {
|
||||
// 刷新运行中或已完成的任务(有进度信息)
|
||||
const activeTasks = result.data.filter(task =>
|
||||
task.status === 'running' || task.status === 'pending'
|
||||
);
|
||||
|
||||
for (const task of activeTasks) {
|
||||
try {
|
||||
// 并行获取进度和PID状态
|
||||
const [progressResponse, statusResponse] = await Promise.all([
|
||||
fetch(`${window.API_BASE}/fine-tune/progress/${task.id}`),
|
||||
fetch(`${window.API_BASE}/fine-tune/${task.id}`)
|
||||
]);
|
||||
const progressResult = await progressResponse.json();
|
||||
const statusResult = await statusResponse.json();
|
||||
|
||||
if (progressResult.code === 0 && progressResult.data) {
|
||||
window.trainingProgressCache[task.id] = progressResult.data;
|
||||
}
|
||||
|
||||
// 如果状态已改变(PID已结束),更新表格中的状态显示
|
||||
if (statusResult.code === 0 && statusResult.data) {
|
||||
const actualStatus = statusResult.data.status;
|
||||
if (task.status !== actualStatus) {
|
||||
// 找到对应的行并更新状态
|
||||
const row = document.querySelector(`tr[data-id="${task.id}"]`);
|
||||
if (row) {
|
||||
const statusCell = row.querySelector('td:nth-child(3)');
|
||||
if (statusCell) {
|
||||
statusCell.innerHTML = `<span class="px-2 py-1 rounded text-xs ${actualStatus === 'running' ? 'bg-green-100 text-green-700' : actualStatus === 'failed' ? 'bg-red-100 text-red-700' : 'bg-blue-100 text-blue-700'}">${actualStatus}</span>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`获取任务 ${task.id} 信息失败:`, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('刷新训练进度失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 检查并更新任务状态(用于 fine-tune 页面)
|
||||
async function checkAndUpdateTaskStatus() {
|
||||
if (typeof window.currentPage !== 'string' || window.currentPage !== 'fine-tune') return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${window.API_BASE}/fine-tune`);
|
||||
const result = await response.json();
|
||||
|
||||
if (result.code === 0 && result.data) {
|
||||
// 获取所有 running 状态的任务
|
||||
const runningTasks = result.data.filter(task => task.status === 'running');
|
||||
|
||||
for (const task of runningTasks) {
|
||||
try {
|
||||
// 调用 status API 获取实际状态(会检查 PID)
|
||||
const statusResponse = await fetch(`${window.API_BASE}/fine-tune/${task.id}`);
|
||||
const statusResult = await statusResponse.json();
|
||||
|
||||
if (statusResult.code === 0 && statusResult.data) {
|
||||
const actualStatus = statusResult.data.status;
|
||||
// 如果实际状态不是 running,更新表格显示
|
||||
if (actualStatus !== 'running') {
|
||||
const row = document.querySelector(`tr[data-id="${task.id}"]`);
|
||||
if (row) {
|
||||
const statusCell = row.querySelector('td:nth-child(3)');
|
||||
if (statusCell) {
|
||||
const statusClass = actualStatus === 'failed'
|
||||
? 'bg-red-100 text-red-700'
|
||||
: 'bg-blue-100 text-blue-700';
|
||||
statusCell.innerHTML = `<span class="px-2 py-1 rounded text-xs ${statusClass}">${actualStatus}</span>`;
|
||||
console.log(`[Status] 任务 ${task.id} 状态已更新: running -> ${actualStatus}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`检查任务 ${task.id} 状态失败:`, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('检查任务状态失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 启动训练进度自动刷新
|
||||
function startProgressRefresh() {
|
||||
stopProgressRefresh();
|
||||
window._progressRefreshTimer = setInterval(() => {
|
||||
refreshTrainingProgress();
|
||||
checkAndUpdateTaskStatus();
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// 停止训练进度刷新
|
||||
function stopProgressRefresh() {
|
||||
if (window._progressRefreshTimer) {
|
||||
clearInterval(window._progressRefreshTimer);
|
||||
window._progressRefreshTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 停止训练任务
|
||||
async function stopItem(taskId) {
|
||||
window.showConfirm('确认停止', '确定要停止这个训练任务吗?进程将被终止。', async () => {
|
||||
try {
|
||||
const response = await fetch(`${window.API_BASE}/fine-tune/stop/${taskId}`, {
|
||||
method: 'POST'
|
||||
});
|
||||
const result = await response.json();
|
||||
if (result.code === 0) {
|
||||
window.showMessage('成功', '训练任务已停止', 'success');
|
||||
// 刷新当前页面
|
||||
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 viewTrainingLog(taskId, taskName) {
|
||||
window.loadPage('logs');
|
||||
}
|
||||
|
||||
// 查看调优任务日志 - 跳转到training-log.html页面
|
||||
function viewFineTuneLogs(taskId, taskName) {
|
||||
// 保存 taskId 到 sessionStorage
|
||||
sessionStorage.setItem('trainingLogTaskId', taskId.toString());
|
||||
sessionStorage.setItem('trainingLogTaskName', taskName);
|
||||
// 跳转到日志页面
|
||||
window.navigateToPage('training-log');
|
||||
}
|
||||
|
||||
// 跳转到训练日志二级页面
|
||||
function navigateToTrainingLog(taskId) {
|
||||
// 传递 taskId 到 sessionStorage
|
||||
sessionStorage.setItem('trainingLogTaskId', taskId.toString());
|
||||
// 跳转到日志页面
|
||||
window.navigateToPage('training-log');
|
||||
}
|
||||
|
||||
// 导出训练服务
|
||||
window.TrainingService = {
|
||||
renderTrainingProgress,
|
||||
refreshTrainingProgress,
|
||||
checkAndUpdateTaskStatus,
|
||||
startProgressRefresh,
|
||||
stopProgressRefresh,
|
||||
stopItem,
|
||||
viewTrainingLog,
|
||||
viewFineTuneLogs,
|
||||
navigateToTrainingLog
|
||||
};
|
||||
181
web/js/utils.js
Normal file
181
web/js/utils.js
Normal file
@@ -0,0 +1,181 @@
|
||||
/**
|
||||
* 工具函数模块
|
||||
* 包含弹窗、消息提示等通用功能
|
||||
*/
|
||||
|
||||
// ============ 自定义消息弹窗 ============
|
||||
window.showMessage = function(title, message, type = 'info', onConfirm) {
|
||||
const modal = document.getElementById('customModal');
|
||||
const modalTitle = document.getElementById('modalTitle');
|
||||
const modalMessage = document.getElementById('modalMessage');
|
||||
const modalIcon = document.getElementById('modalIcon');
|
||||
const modalConfirmBtn = document.getElementById('modalConfirmBtn');
|
||||
const modalConfirmBtn2 = document.getElementById('modalConfirmBtn2');
|
||||
const modalBtnGroup = document.getElementById('modalBtnGroup');
|
||||
const modalSingleBtnGroup = document.getElementById('modalSingleBtnGroup');
|
||||
|
||||
// 设置标题
|
||||
modalTitle.textContent = title;
|
||||
modalTitle.className = 'text-lg font-medium text-gray-800 mb-2';
|
||||
|
||||
// 设置消息
|
||||
modalMessage.innerHTML = message;
|
||||
|
||||
// 设置图标和按钮颜色
|
||||
if (type === 'success') {
|
||||
modalIcon.innerHTML = '<div class="w-12 h-12 mx-auto mb-4 rounded-full bg-green-100 flex items-center justify-center"><i class="fa fa-check text-xl text-green-600"></i></div>';
|
||||
} else if (type === 'error') {
|
||||
modalIcon.innerHTML = '<div class="w-12 h-12 mx-auto mb-4 rounded-full bg-red-100 flex items-center justify-center"><i class="fa fa-times text-xl text-red-600"></i></div>';
|
||||
} else if (type === 'warning') {
|
||||
modalIcon.innerHTML = '<div class="w-12 h-12 mx-auto mb-4 rounded-full bg-yellow-100 flex items-center justify-center"><i class="fa fa-exclamation text-xl text-yellow-600"></i></div>';
|
||||
} else {
|
||||
modalIcon.innerHTML = '<div class="w-12 h-12 mx-auto mb-4 rounded-full bg-blue-100 flex items-center justify-center"><i class="fa fa-info text-xl text-blue-600"></i></div>';
|
||||
}
|
||||
|
||||
// 单按钮模式
|
||||
modalBtnGroup.classList.add('hidden');
|
||||
modalSingleBtnGroup.classList.remove('hidden');
|
||||
const confirmBtn = modalConfirmBtn2;
|
||||
if (type === 'error') {
|
||||
confirmBtn.className = 'px-6 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors';
|
||||
} else {
|
||||
confirmBtn.className = 'px-6 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors';
|
||||
}
|
||||
|
||||
// 显示弹窗
|
||||
modal.classList.remove('hidden');
|
||||
document.body.style.overflow = 'hidden';
|
||||
|
||||
// 保存回调到按钮属性
|
||||
confirmBtn._onConfirm = onConfirm;
|
||||
|
||||
// 使用 function 而不是箭头函数
|
||||
confirmBtn.onclick = function() {
|
||||
closeModal();
|
||||
const callback = this._onConfirm;
|
||||
if (typeof callback === 'function') {
|
||||
callback();
|
||||
}
|
||||
this._onConfirm = null;
|
||||
};
|
||||
};
|
||||
|
||||
// 关闭消息弹窗
|
||||
function closeModal() {
|
||||
const modal = document.getElementById('customModal');
|
||||
modal.classList.add('hidden');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
// 确认弹窗(两个按钮)
|
||||
window.showConfirm = function(title, message, onConfirm, onCancel, type = 'info') {
|
||||
const modal = document.getElementById('customModal');
|
||||
const modalTitle = document.getElementById('modalTitle');
|
||||
const modalMessage = document.getElementById('modalMessage');
|
||||
const modalIcon = document.getElementById('modalIcon');
|
||||
const modalConfirmBtn = document.getElementById('modalConfirmBtn');
|
||||
const modalCancelBtn = document.getElementById('modalCancelBtn');
|
||||
const modalBtnGroup = document.getElementById('modalBtnGroup');
|
||||
const modalSingleBtnGroup = document.getElementById('modalSingleBtnGroup');
|
||||
|
||||
if (!modalConfirmBtn) {
|
||||
console.error('modalConfirmBtn not found');
|
||||
return;
|
||||
}
|
||||
|
||||
modalTitle.textContent = title;
|
||||
modalMessage.innerHTML = message;
|
||||
|
||||
// 根据类型设置图标
|
||||
if (type === 'warning') {
|
||||
modalIcon.innerHTML = '<div class="w-12 h-12 mx-auto mb-4 rounded-full bg-yellow-100 flex items-center justify-center"><i class="fa fa-exclamation text-xl text-yellow-600"></i></div>';
|
||||
} else {
|
||||
modalIcon.innerHTML = '<div class="w-12 h-12 mx-auto mb-4 rounded-full bg-blue-100 flex items-center justify-center"><i class="fa fa-question text-xl text-blue-600"></i></div>';
|
||||
}
|
||||
|
||||
// 显示双按钮组,隐藏单按钮组
|
||||
modalBtnGroup.classList.remove('hidden');
|
||||
modalSingleBtnGroup.classList.add('hidden');
|
||||
modalConfirmBtn.textContent = '确定';
|
||||
modalConfirmBtn.className = 'px-6 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors';
|
||||
|
||||
modal.classList.remove('hidden');
|
||||
document.body.style.overflow = 'hidden';
|
||||
|
||||
// 保存回调到按钮属性
|
||||
modalConfirmBtn._onConfirm = onConfirm;
|
||||
modalConfirmBtn._onCancel = onCancel;
|
||||
|
||||
// 使用 function 而不是箭头函数,确保 this 指向正确
|
||||
modalConfirmBtn.onclick = function() {
|
||||
closeModal();
|
||||
const callback = this._onConfirm;
|
||||
if (typeof callback === 'function') {
|
||||
callback();
|
||||
}
|
||||
this._onConfirm = null;
|
||||
this._onCancel = null;
|
||||
};
|
||||
|
||||
modalCancelBtn.onclick = function() {
|
||||
closeModal();
|
||||
const callback = this._onCancel || modalConfirmBtn._onCancel;
|
||||
if (typeof callback === 'function') {
|
||||
callback();
|
||||
}
|
||||
modalConfirmBtn._onConfirm = null;
|
||||
modalConfirmBtn._onCancel = null;
|
||||
};
|
||||
};
|
||||
|
||||
// ============ Web日志系统 ============
|
||||
const webLogger = {
|
||||
_currentPage: 'main',
|
||||
|
||||
// 初始化当前页面名称
|
||||
init: function(pageName) {
|
||||
this._currentPage = pageName || 'unknown';
|
||||
},
|
||||
|
||||
// 发送日志到服务器
|
||||
_sendLog: async function(level, message) {
|
||||
try {
|
||||
await fetch(`${window.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);
|
||||
}
|
||||
};
|
||||
|
||||
// 导出 webLogger 到全局
|
||||
window.webLogger = webLogger;
|
||||
Reference in New Issue
Block a user