287 lines
12 KiB
HTML
287 lines
12 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>模型管理 / 远光软件微调平台</title>
|
|
<script src="../lib/tailwindcss/tailwind.js"></script>
|
|
<link href="../lib/font-awesome/css/font-awesome.min.css" rel="stylesheet">
|
|
<style>
|
|
.bg-primary { background-color: #1890ff; }
|
|
.text-primary { color: #1890ff; }
|
|
.border-primary { border-color: #1890ff; }
|
|
:root { --primary: #1890ff; }
|
|
.form-input {
|
|
display: block;
|
|
width: 100%;
|
|
padding: 0.5rem 0.75rem;
|
|
font-size: 0.875rem;
|
|
line-height: 1.25rem;
|
|
color: #1f2937;
|
|
background-color: #fff;
|
|
border: 1px solid #d1d5db;
|
|
border-radius: 0.375rem;
|
|
transition: border-color 0.15s ease-in-out;
|
|
}
|
|
.form-input:focus {
|
|
outline: none;
|
|
border-color: #1890ff;
|
|
box-shadow: 0 0 0 3px rgba(24, 144, 255, 0.1);
|
|
}
|
|
.form-label {
|
|
display: block;
|
|
font-size: 0.875rem;
|
|
font-weight: 500;
|
|
color: #374151;
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
.tab-btn {
|
|
padding: 8px 20px !important;
|
|
font-size: 14px !important;
|
|
font-weight: 500 !important;
|
|
border-radius: 6px !important;
|
|
border: 1px solid transparent !important;
|
|
cursor: pointer !important;
|
|
background: transparent !important;
|
|
color: #4b5563 !important;
|
|
transition: all 0.2s !important;
|
|
min-width: 100px;
|
|
}
|
|
.tab-btn:hover {
|
|
background: white !important;
|
|
border-color: #d1d5db !important;
|
|
}
|
|
.tab-btn.tab-active {
|
|
background: white !important;
|
|
color: #1890ff !important;
|
|
border-color: #1890ff !important;
|
|
box-shadow: 0 1px 3px rgba(24,144,255,0.2) !important;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body class="antialiased bg-gray-50">
|
|
<!-- Tab 导航 -->
|
|
<div style="background: #fff; border-bottom: 1px solid #e5e7eb; padding: 16px;">
|
|
<h2 style="font-size: 18px; font-weight: 600; color: #1f2937; margin-bottom: 12px;">模型管理</h2>
|
|
<div style="display: flex; gap: 8px; background: #f3f4f6; padding: 6px; border-radius: 8px;">
|
|
<button onclick="switchTab('all')" id="tab-all" class="tab-btn tab-active" style="display: inline-flex; align-items: center; justify-content: center;">
|
|
全部模型
|
|
</button>
|
|
<button onclick="switchTab('training')" id="tab-training" class="tab-btn" style="display: inline-flex; align-items: center; justify-content: center;">
|
|
训练基座
|
|
</button>
|
|
<button onclick="switchTab('inference')" id="tab-inference" class="tab-btn" style="display: inline-flex; align-items: center; justify-content: center;">
|
|
推理对比
|
|
</button>
|
|
<button onclick="switchTab('evaluation')" id="tab-evaluation" class="tab-btn" style="display: inline-flex; align-items: center; justify-content: center;">
|
|
评测模型
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 表格容器 -->
|
|
<div style="padding: 16px;">
|
|
<!-- 工具栏 -->
|
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
|
|
<div style="display: flex; align-items: center; gap: 16px;">
|
|
<input type="text" id="searchInput" placeholder="搜索模型名称..." style="width: 256px; padding: 8px 12px; border: 1px solid #d1d5db; border-radius: 6px; font-size: 14px;" oninput="filterModels()">
|
|
</div>
|
|
<button onclick="window.location.href='model-manage-create.html'" style="padding: 8px 16px; background: #1890ff; color: white; border: none; border-radius: 6px; cursor: pointer; display: flex; align-items: center; font-size: 14px;">
|
|
<span style="margin-right: 8px;">+</span>添加模型
|
|
</button>
|
|
</div>
|
|
|
|
<!-- 模型表格 -->
|
|
<div class="bg-white rounded-lg shadow-sm">
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full">
|
|
<thead class="bg-gray-50">
|
|
<tr>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">模型名称</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">模型类型</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">用途</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">模型来源</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">描述</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">创建时间</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="modelsBody" class="bg-white divide-y divide-gray-200">
|
|
<!-- 动态加载 -->
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<!-- 空状态 -->
|
|
<div id="emptyState" class="hidden px-6 py-12 text-center">
|
|
<i class="fa fa-inbox text-4xl text-gray-300 mb-3"></i>
|
|
<p class="text-gray-500">暂无模型数据</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// 动态获取 API 基础地址
|
|
const getApiBase = () => {
|
|
const protocol = window.location.protocol;
|
|
const hostname = window.location.hostname;
|
|
return `${protocol}//${hostname}:8080/api`;
|
|
};
|
|
const API_BASE = getApiBase();
|
|
|
|
let allModels = [];
|
|
let currentTab = 'all';
|
|
|
|
// Tab 切换
|
|
function switchTab(tab) {
|
|
currentTab = tab;
|
|
// 更新按钮样式
|
|
document.querySelectorAll('.tab-btn').forEach(btn => {
|
|
btn.classList.remove('tab-active');
|
|
});
|
|
const activeTab = document.getElementById(`tab-${tab}`);
|
|
activeTab.classList.add('tab-active');
|
|
|
|
renderModels();
|
|
}
|
|
|
|
// 加载模型数据
|
|
async function loadModels() {
|
|
try {
|
|
const response = await fetch(`${API_BASE}/model-manage`);
|
|
const result = await response.json();
|
|
|
|
if (result.code === 0) {
|
|
allModels = result.data || [];
|
|
renderModels();
|
|
}
|
|
} catch (error) {
|
|
console.error('加载模型失败:', error);
|
|
}
|
|
}
|
|
|
|
// 筛选模型
|
|
function filterModels() {
|
|
renderModels();
|
|
}
|
|
|
|
// 渲染模型列表
|
|
function renderModels() {
|
|
const searchValue = document.getElementById('searchInput').value.toLowerCase();
|
|
let filteredModels = allModels;
|
|
|
|
// 按 Tab 筛选
|
|
if (currentTab !== 'all') {
|
|
filteredModels = filteredModels.filter(m => m.purpose === currentTab);
|
|
}
|
|
|
|
// 按搜索关键词筛选
|
|
if (searchValue) {
|
|
filteredModels = filteredModels.filter(m =>
|
|
m.name?.toLowerCase().includes(searchValue) ||
|
|
m.description?.toLowerCase().includes(searchValue)
|
|
);
|
|
}
|
|
|
|
const tbody = document.getElementById('modelsBody');
|
|
const emptyState = document.getElementById('emptyState');
|
|
|
|
if (filteredModels.length === 0) {
|
|
tbody.innerHTML = '';
|
|
emptyState.classList.remove('hidden');
|
|
return;
|
|
}
|
|
|
|
emptyState.classList.add('hidden');
|
|
|
|
tbody.innerHTML = filteredModels.map(item => {
|
|
// 模型类型
|
|
const typeMap = {
|
|
'LLM': '大语言模型',
|
|
'CV': '计算机视觉',
|
|
'NLP': '自然语言处理',
|
|
'Embedding': '向量模型',
|
|
'Other': '其他'
|
|
};
|
|
const typeDisplay = typeMap[item.type] || item.type || '-';
|
|
|
|
// 用途
|
|
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 purposeDisplay = purposeMap[item.purpose] || purposeMap['inference'];
|
|
|
|
// 模型来源
|
|
const sourceMap = {
|
|
'local': '本地模型',
|
|
'api': '在线模型',
|
|
'online': '在线模型'
|
|
};
|
|
const sourceDisplay = sourceMap[item.model_source] || item.model_source || '-';
|
|
|
|
return `
|
|
<tr class="hover:bg-gray-50">
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<div class="text-sm font-medium text-gray-900">${item.name || '-'}</div>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<span class="px-2 py-1 text-xs font-medium rounded bg-blue-100 text-blue-700">${typeDisplay}</span>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<span class="px-2 py-1 text-xs font-medium rounded ${purposeDisplay.class}">${purposeDisplay.text}</span>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<span class="px-2 py-1 text-xs font-medium rounded bg-gray-100 text-gray-700">${sourceDisplay}</span>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<div class="text-sm text-gray-500 max-w-xs truncate">${item.description || '-'}</div>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
${item.create_time ? new Date(item.create_time).toLocaleString('zh-CN') : '-'}
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
<button onclick="editModel(${item.id})" class="text-primary hover:text-primary/80 mr-3">
|
|
<i class="fa fa-edit"></i> 编辑
|
|
</button>
|
|
<button onclick="deleteModel(${item.id})" class="text-red-500 hover:text-red-600">
|
|
<i class="fa fa-trash"></i> 删除
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
// 编辑模型
|
|
function editModel(id) {
|
|
window.location.href = `model-manage-create.html?id=${id}`;
|
|
}
|
|
|
|
// 删除模型
|
|
async function deleteModel(id) {
|
|
if (!confirm('确定要删除此模型吗?')) return;
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/model-manage/${id}`, {
|
|
method: 'DELETE'
|
|
});
|
|
const result = await response.json();
|
|
|
|
if (result.code === 0) {
|
|
loadModels();
|
|
} else {
|
|
alert('删除失败: ' + result.message);
|
|
}
|
|
} catch (error) {
|
|
console.error('删除模型失败:', error);
|
|
alert('删除失败');
|
|
}
|
|
}
|
|
|
|
// 页面加载时初始化
|
|
loadModels();
|
|
</script>
|
|
</body>
|
|
</html>
|