/**
* 管理页面 JavaScript
* 提供用户管理、数据库管理和标注配置管理功能
*/
// 全局状态
let currentSection = 'users';
let editingId = null;
let currentProjectId = null;
// 用户缓存
let usersCache = null;
// 项目懒加载状态
let projectsLoadedCount = 0;
let projectsLoading = false;
let projectsHasMore = true;
const PROJECTS_LOAD_SIZE = 20; // 每次加载的项目数量
// 项目数据集懒加载状态
let projectDatasetsLoadedCount = 0;
let projectDatasetsLoading = false;
let projectDatasetsHasMore = true;
const PROJECT_DATASETS_LOAD_SIZE = 20; // 每次加载的数据集数量
let projectDatasetsObserver = null;
// ==================== i18n 辅助函数 ====================
/**
* 更新指定容器内的所有 i18n 翻译
* @param {HTMLElement} container - 需要更新翻译的容器元素
*/
function updateTemplateTranslations(container) {
if (!window.i18next) return;
// 更新所有带有 data-i18n 属性的元素
container.querySelectorAll('[data-i18n]').forEach(element => {
const key = element.getAttribute('data-i18n');
const translation = window.i18next.t(key);
// 如果元素内有 span 元素,只更新文本节点
if (element.children.length === 1 && element.children[0].tagName === 'SPAN') {
const span = element.children[0];
if (span.hasAttribute('data-i18n')) {
span.textContent = window.i18next.t(span.getAttribute('data-i18n'));
} else {
element.textContent = translation;
}
} else {
element.textContent = translation;
}
});
// 更新带有 data-i18n-title 属性的元素的 title
container.querySelectorAll('[data-i18n-title]').forEach(element => {
const key = element.getAttribute('data-i18n-title');
element.title = window.i18next.t(key);
});
// 更新带有 data-i18n-placeholder 属性的 placeholder
container.querySelectorAll('[data-i18n-placeholder]').forEach(element => {
const key = element.getAttribute('data-i18n-placeholder');
element.placeholder = window.i18next.t(key);
});
// 更新 select 选项的文本
container.querySelectorAll('select').forEach(select => {
select.querySelectorAll('option').forEach(option => {
const i18nKey = option.getAttribute('data-i18n');
if (i18nKey) {
if (option.firstChild) {
option.firstChild.textContent = window.i18next.t(i18nKey);
} else {
option.textContent = window.i18next.t(i18nKey);
}
}
});
});
}
// DOM 加载完成后初始化
document.addEventListener('DOMContentLoaded', () => {
initPaginators();
initNavigation();
initModals();
initEventListeners();
// 如果默认是用户管理section,加载用户管理模块
if (currentSection === 'users') {
loadUserManagement();
}
});
// ==================== 分页器初始化 ====================
function initPaginators() {
// 初始化项目懒加载
initProjectsLazyLoad();
}
// ==================== 导航管理 ====================
function initNavigation() {
const navItems = document.querySelectorAll('.nav-item');
navItems.forEach(item => {
item.addEventListener('click', () => {
// 如果是跳转到用户页面的按钮,直接跳转
if (item.id === 'goToUserBtn') {
window.location.href = '/user';
return;
}
const section = item.dataset.section;
if (section) {
switchSection(section);
}
});
});
}
function switchSection(section) {
// 更新导航状态
document.querySelectorAll('.nav-item').forEach(item => {
item.classList.toggle('active', item.dataset.section === section);
});
// 更新内容区
document.querySelectorAll('.content-section').forEach(sec => {
sec.classList.toggle('active', sec.id === `${section}-section`);
});
currentSection = section;
// 重置各section的tab状态
switch(section) {
case 'users':
loadUserManagement();
break;
case 'datasets':
loadDatasetManagement();
break;
case 'projects':
// 如果当前在详情页,返回项目列表
const detailContainer = document.getElementById('project-detail-container');
const projectsList = document.getElementById('projects-list');
if (detailContainer && detailContainer.style.display !== 'none') {
// 在详情页,返回列表
resetProjectsTabs();
}
// 重置项目管理页面的tab状态
resetProjectsTabs();
// 确保懒加载已初始化
if (!projectsObserver) {
initProjectsLazyLoad();
} else {
// 重置懒加载状态并重新加载
projectsLoadedCount = 0;
projectsHasMore = true;
const container = document.getElementById('projectsCardContainer');
if (container) container.innerHTML = `
${t('common.loading')}
`;
loadProjects(true);
}
break;
case 'annotation-configs':
loadAnnotationConfigManagement();
break;
case 'seed-questions':
loadSeedQuestionManagement();
break;
case 'system-config':
loadSystemConfigManagement();
break;
}
}
function resetProjectsTabs() {
// 重置项目管理页面到项目列表状态
const projectsList = document.getElementById('projects-list');
const detailContainer = document.getElementById('project-detail-container');
if (projectsList) projectsList.style.display = 'flex';
if (detailContainer) {
detailContainer.style.display = 'none';
detailContainer.innerHTML = ''; // 清空详情页内容
}
currentProjectId = null;
}
// ==================== 模态框管理 ====================
function initModals() {
const modal = document.getElementById('modal');
const closeBtn = document.getElementById('modalClose');
const cancelBtn = document.getElementById('modalCancel');
closeBtn.addEventListener('click', closeModal);
cancelBtn.addEventListener('click', closeModal);
}
function openModal(title, content, onSubmit, hideSubmit = false) {
const modal = document.getElementById('modal');
const modalTitle = document.getElementById('modalTitle');
const modalBody = document.getElementById('modalBody');
const submitBtn = document.getElementById('modalSubmit');
const cancelBtn = document.getElementById('modalCancel');
modalTitle.textContent = title;
modalBody.innerHTML = content;
modal.classList.add('active');
// 控制提交按钮显示/隐藏
if (hideSubmit) {
submitBtn.style.display = 'none';
} else {
submitBtn.style.display = 'inline-block';
// 移除旧的事件监听器
const newSubmitBtn = submitBtn.cloneNode(true);
submitBtn.parentNode.replaceChild(newSubmitBtn, submitBtn);
// 添加新的事件监听器
document.getElementById('modalSubmit').addEventListener('click', async () => {
if (onSubmit) {
await onSubmit();
}
});
}
}
function closeModal() {
const modal = document.getElementById('modal');
modal.classList.remove('active');
editingId = null;
}
// ==================== 事件监听器 ====================
function initEventListeners() {
// 退出登录
document.getElementById('logoutBtn').addEventListener('click', () => {
if (confirm(t('actions.confirmLogout'))) {
clearToken();
window.location.href = '/auth';
}
});
// 监听从项目管理页面保存数据集的事件
window.addEventListener('datasetSavedFromProject', (event) => {
const { projectId } = event.detail;
// 如果当前在项目管理页面且是同一个项目,刷新数据
if (currentSection === 'projects' && currentProjectId === projectId) {
loadProjectDatasets(projectId, true);
}
});
// 项目管理
document.getElementById('addProjectBtn').addEventListener('click', () => {
showProjectForm();
});
document.getElementById('importProjectBtn').addEventListener('click', () => {
showImportProjectForm();
});
document.getElementById('showProjectUsageBtn').addEventListener('click', () => {
showProjectUsage();
});
// 使用事件委托处理动态加载的详情页按钮
document.addEventListener('click', (e) => {
// 处理详情页内的按钮点击
if (e.target.id === 'addDatasetToProjectBtn' || e.target.closest('#addDatasetToProjectBtn')) {
if (currentProjectId) {
showAddDatasetToProjectModal();
}
} else if (e.target.id === 'importDatasetToProjectBtn' || e.target.closest('#importDatasetToProjectBtn')) {
if (currentProjectId) {
showImportDatasetToProjectForm();
}
} else if (e.target.id === 'exportProjectAnnotationsBtn' || e.target.closest('#exportProjectAnnotationsBtn')) {
if (currentProjectId) {
exportProjectAnnotations(currentProjectId);
}
} else if (e.target.id === 'addConfigToProjectBtn' || e.target.closest('#addConfigToProjectBtn')) {
if (currentProjectId) {
showAddConfigToProjectModal();
}
}
// 处理项目详情内的子标签页切换(数据集管理/配置管理)
else if (e.target.closest('#project-detail-container .project-tabs .tab-btn')) {
const btn = e.target.closest('.tab-btn');
if (btn && btn.dataset.tab) {
switchProjectDetailTab(btn.dataset.tab);
}
}
});
}
// ==================== 用户管理模块加载 ====================
async function loadUserManagement() {
const container = document.getElementById('user-management-container');
if (!container) return;
// 如果已加载,直接返回
if (container.innerHTML.trim() !== '') {
return;
}
try {
// 加载HTML
const htmlResponse = await fetch('/user-management.html');
if (!htmlResponse.ok) {
throw new Error('加载用户管理HTML失败');
}
container.innerHTML = await htmlResponse.text();
// 立即更新模板中的翻译
if (window.updateTemplateTranslations) {
window.updateTemplateTranslations(container);
}
// 加载CSS(如果未加载)
await ensureStylesheetLoaded('/static/css/user-management.css');
// 加载JS(如果未加载)
await ensureScriptLoaded('/static/js/user-management.js');
// 初始化模块
if (window.UserManagement) {
window.UserManagement.init(container);
}
} catch (error) {
console.error('加载用户管理模块失败:', error);
showError('加载用户管理模块失败: ' + (error.message || '未知错误'));
}
}
// ==================== 依赖加载辅助函数 ====================
// 确保样式表已加载
async function ensureStylesheetLoaded(href) {
const linkId = href.replace(/[^a-zA-Z0-9]/g, '_');
if (document.getElementById(linkId)) {
return;
}
const link = document.createElement('link');
link.id = linkId;
link.rel = 'stylesheet';
link.href = href;
document.head.appendChild(link);
return new Promise((resolve) => {
link.onload = resolve;
link.onerror = resolve; // 即使失败也继续
});
}
// 确保脚本已加载
async function ensureScriptLoaded(src) {
const scriptId = src.replace(/[^a-zA-Z0-9]/g, '_');
const existingScript = document.getElementById(scriptId);
if (existingScript) {
// 如果脚本元素已存在,等待它加载完成
if (existingScript.complete || existingScript.readyState === 'complete' || existingScript.readyState === 'loaded') {
return Promise.resolve();
}
// 等待现有脚本的onload事件
return new Promise((resolve) => {
existingScript.addEventListener('load', resolve);
existingScript.addEventListener('error', resolve); // 即使失败也继续
// 如果脚本已经加载完成但事件没触发,设置一个超时
setTimeout(resolve, 100);
});
}
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.id = scriptId;
script.src = src;
script.onload = resolve;
script.onerror = () => {
console.error('Failed to load script:', src);
resolve(); // 即使失败也继续,让后续代码处理错误
};
document.head.appendChild(script);
});
}
// ==================== 数据库管理模块加载 ====================
async function loadDatasetManagement() {
const container = document.getElementById('dataset-management-container');
if (!container) return;
// 如果已加载,直接返回
if (container.innerHTML.trim() !== '') {
return;
}
try {
// 加载HTML
const htmlResponse = await fetch('/dataset-management.html');
if (!htmlResponse.ok) {
throw new Error('加载数据库管理HTML失败');
}
container.innerHTML = await htmlResponse.text();
// 立即更新模板中的翻译
if (window.updateTemplateTranslations) {
window.updateTemplateTranslations(container);
}
// 加载CSS(如果未加载)
await ensureStylesheetLoaded('/static/css/dataset-management.css');
// 加载JS(如果未加载)
await ensureScriptLoaded('/static/js/dataset-management.js');
// 初始化模块
if (window.DatasetManagement) {
window.DatasetManagement.init(container);
}
} catch (error) {
console.error('加载数据库管理模块失败:', error);
showError('加载数据库管理模块失败: ' + (error.message || '未知错误'));
}
}
// 确保 DatasetManagement 模块已加载(仅加载 JS,不加载 HTML/CSS)
async function ensureDatasetManagementLoaded() {
// 如果模块已存在,直接返回
if (window.DatasetManagement) {
return Promise.resolve();
}
try {
// 只加载 JS 脚本
await ensureScriptLoaded('/static/js/dataset-management.js');
// 等待模块初始化
let retries = 0;
while (!window.DatasetManagement && retries < 10) {
await new Promise(resolve => setTimeout(resolve, 100));
retries++;
}
if (!window.DatasetManagement) {
throw new Error('DatasetManagement 模块加载超时');
}
return Promise.resolve();
} catch (error) {
console.error('加载 DatasetManagement 模块失败:', error);
showError('加载数据库管理模块失败: ' + (error.message || '未知错误'));
throw error;
}
}
// 从项目管理页面编辑数据库
async function editDatasetFromProject(datasetId) {
try {
// 确保模块已加载
await ensureDatasetManagementLoaded();
// 保存当前项目ID,用于保存成功后刷新数据
const projectIdToRefresh = currentProjectId;
// 设置标志,表示从项目管理页面调用
window._editingDatasetFromProject = true;
window._projectIdToRefresh = projectIdToRefresh;
// 调用编辑方法(不切换标签页)
if (window.DatasetManagement && window.DatasetManagement.editDataset) {
await window.DatasetManagement.editDataset(datasetId);
} else {
throw new Error('DatasetManagement.editDataset 方法不可用');
}
} catch (error) {
console.error('编辑数据库失败:', error);
showError('编辑数据库失败: ' + (error.message || '未知错误'));
// 清除标志
window._editingDatasetFromProject = false;
window._projectIdToRefresh = null;
}
}
// ==================== 标注配置管理模块加载 ====================
async function loadAnnotationConfigManagement() {
const container = document.getElementById('annotation-config-management-container');
if (!container) return;
// 如果已加载,直接返回
if (container.innerHTML.trim() !== '') {
return;
}
try {
// 加载HTML
const htmlResponse = await fetch('/annotation-config-management.html');
if (!htmlResponse.ok) {
throw new Error('加载标注配置管理HTML失败');
}
container.innerHTML = await htmlResponse.text();
// 立即更新模板中的翻译
if (window.updateTemplateTranslations) {
window.updateTemplateTranslations(container);
}
// 加载CSS(如果未加载)
await ensureStylesheetLoaded('/static/css/annotation-config-management.css');
// 加载JS(如果未加载)
await ensureScriptLoaded('/static/js/annotation-config-management.js');
// 初始化模块
if (window.AnnotationConfigManagement) {
window.AnnotationConfigManagement.init(container);
}
} catch (error) {
console.error('加载标注配置管理模块失败:', error);
showError('加载标注配置管理模块失败: ' + (error.message || '未知错误'));
}
}
// ==================== 种子问题管理模块加载 ====================
async function loadSeedQuestionManagement() {
const container = document.getElementById('seed-question-management-container');
if (!container) return;
// 如果已加载,直接返回
if (container.innerHTML.trim() !== '') {
return;
}
try {
// 加载HTML
const htmlResponse = await fetch('/seed-question-management.html');
if (!htmlResponse.ok) {
throw new Error('加载种子问题管理HTML失败');
}
container.innerHTML = await htmlResponse.text();
// 立即更新模板中的翻译
if (window.updateTemplateTranslations) {
window.updateTemplateTranslations(container);
}
// 加载CSS(如果未加载)
await ensureStylesheetLoaded('/static/css/seed-question-management.css');
// 加载JS(如果未加载)
await ensureScriptLoaded('/static/js/seed-question-management.js');
// 初始化模块
if (window.SeedQuestionManagement) {
window.SeedQuestionManagement.init(container);
}
} catch (error) {
console.error('加载种子问题管理模块失败:', error);
showError('加载种子问题管理模块失败: ' + (error.message || '未知错误'));
}
}
// ==================== 系统配置管理模块加载 ====================
async function loadSystemConfigManagement() {
const container = document.getElementById('system-config-management-container');
if (!container) return;
// 如果已加载,直接返回
if (container.innerHTML.trim() !== '') {
return;
}
try {
// 加载HTML
const htmlResponse = await fetch('/system-config-management.html');
if (!htmlResponse.ok) {
throw new Error('加载系统配置管理HTML失败');
}
container.innerHTML = await htmlResponse.text();
// 立即更新模板中的翻译
if (window.updateTemplateTranslations) {
window.updateTemplateTranslations(container);
}
// 加载CSS(如果未加载)
await ensureStylesheetLoaded('/static/css/system-config-management.css');
// 加载JS(如果未加载)
await ensureScriptLoaded('/static/js/system-config-management.js');
// 初始化模块
if (window.SystemConfigManagement) {
window.SystemConfigManagement.init(container);
}
} catch (error) {
console.error('加载系统配置管理模块失败:', error);
showError('加载系统配置管理模块失败: ' + (error.message || '未知错误'));
}
}
// ==================== 导出项目所有标注 ====================
async function exportProjectAnnotations(projectId) {
// 显示格式选择对话框
const title = '导出项目所有标注';
const content = `
`;
openModal(title, content, async () => {
await handleExportProjectAnnotations(projectId);
});
}
async function handleExportProjectAnnotations(projectId) {
const format = document.getElementById('exportProjectFormat').value;
if (!format) {
showError('请选择导出格式');
return;
}
try {
// 获取token
const token = getToken();
const headers = {};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
// 构建导出URL
const exportUrl = `${API_BASE_URL}/projects/${projectId}/export-annotations?format=${format}`;
// 使用fetch下载文件
const response = await fetch(exportUrl, {
method: 'GET',
headers: headers
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({ detail: '导出失败' }));
throw new Error(errorData.detail || errorData.message || `HTTP错误: ${response.status}`);
}
// 获取文件名(从Content-Disposition头中提取,或使用默认名称)
const contentDisposition = response.headers.get('Content-Disposition');
let filename = `project_${projectId}_annotations.zip`;
if (contentDisposition) {
const filenameMatch = contentDisposition.match(/filename="?([^"]+)"?/);
if (filenameMatch) {
filename = filenameMatch[1];
}
}
// 获取文件内容
const blob = await response.blob();
// 创建下载链接
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
// 清理
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
showSuccess(`项目所有标注已导出为ZIP格式(${format.toUpperCase()})`);
closeModal();
} catch (error) {
console.error('导出失败:', error);
// 如果是401未授权错误,清除token并跳转到登录页
if (error.status === 401 || (error.message && error.message.includes('401'))) {
clearToken();
const currentPath = window.location.pathname;
if (currentPath !== '/auth') {
const redirectUrl = encodeURIComponent(window.location.href);
window.location.href = `/auth?redirect=${redirectUrl}`;
}
}
showError('导出失败: ' + (error.message || '未知错误'));
}
}
// ==================== 显示项目用法说明 ====================
function showProjectUsage() {
// 构建通用的项目功能说明内容
const usageContent = `
${t('project.usageGuide')}
${t('project.usage.section1.title')}
- ${t('project.usage.section1.item1')}
- ${t('project.usage.section1.item2')}
- ${t('project.usage.section1.item3')}
- ${t('project.usage.section1.item4')}
- ${t('project.usage.section1.item5')}
- ${t('project.usage.section1.item6')}
${t('project.usage.section2.title')}
- ${t('project.usage.section2.item1')}
- ${t('project.usage.section2.item2')}
- ${t('project.usage.section2.item3')}
- ${t('project.usage.section2.item4')}
${t('project.usage.section3.title')}
- ${t('project.usage.section3.item1')}
- ${t('project.usage.section3.item2')}
- ${t('project.usage.section3.item3')}
- ${t('project.usage.section3.item4')}
${t('project.usage.section4.title')}
- 可以将现有数据集添加到项目中
- 支持从JSONL文件导入数据集到项目,支持批量导入多个文件
- 数据集会自动继承项目的标注配置(如果数据集没有自己的配置)
- 可以查看和管理项目下的所有数据集,包括数据集的统计信息
- 支持从项目中移除数据集(移除后,数据集的project_id会被设为NULL)
${t('project.usage.section5.title')}
- ${t('project.usage.section5.item1')}
- ${t('project.usage.section5.item2')}
- ${t('project.usage.section5.item3')}
- ${t('project.usage.section5.item4')}
- ${t('project.usage.section5.item5')}
${t('project.usage.section6.title')}
- ${t('project.usage.section6.item1')}
- ${t('project.usage.section6.item2')}
- ${t('project.usage.section6.item3')}
${t('project.usage.section7.title')}
- ${t('project.usage.section7.item1')}
- ${t('project.usage.section7.item2')}
- ${t('project.usage.section7.item3')}
- ${t('project.usage.section7.item4')}
- ${t('project.usage.section7.item5')}
${t('project.usage.bestPractices.title')}
- ${t('project.usage.bestPractices.configOrder')}:${t('project.usage.bestPractices.configOrderDesc')}
- ${t('project.usage.bestPractices.inheritance')}:${t('project.usage.bestPractices.inheritanceDesc')}
- ${t('project.usage.bestPractices.batchImport')}:${t('project.usage.bestPractices.batchImportDesc')}
- ${t('project.usage.bestPractices.backup')}:${t('project.usage.bestPractices.backupDesc')}
- ${t('project.usage.bestPractices.management')}:${t('project.usage.bestPractices.managementDesc')}
`;
// 显示对话框(隐藏提交按钮,只显示关闭按钮)
openModal(t('project.usageGuide'), usageContent, null, true);
}
// ==================== 工具函数 ====================
function escapeHtml(text) {
if (text == null) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function formatDateTime(dateTimeStr) {
if (!dateTimeStr) return '-';
try {
const date = new Date(dateTimeStr);
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
} catch (error) {
return dateTimeStr;
}
}
function showSuccess(message) {
showMessage(message, 'success');
}
function showError(message) {
showMessage(message, 'error');
}
function showMessage(message, type = 'info') {
// 创建消息元素
const messageEl = document.createElement('div');
messageEl.className = `message ${type} show`;
messageEl.textContent = message;
messageEl.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
z-index: 2000;
padding: 16px 20px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
animation: slideDown 0.3s ease-out;
max-width: 400px;
`;
// 添加样式
if (type === 'success') {
messageEl.style.background = '#d4edda';
messageEl.style.color = '#155724';
messageEl.style.border = '1px solid #c3e6cb';
} else if (type === 'error') {
messageEl.style.background = '#f8d7da';
messageEl.style.color = '#721c24';
messageEl.style.border = '1px solid #f5c6cb';
} else {
messageEl.style.background = '#d1ecf1';
messageEl.style.color = '#0c5460';
messageEl.style.border = '1px solid #bee5eb';
}
document.body.appendChild(messageEl);
// 3秒后自动移除
setTimeout(() => {
messageEl.style.animation = 'fadeOut 0.3s ease-out';
setTimeout(() => {
if (messageEl.parentNode) {
messageEl.parentNode.removeChild(messageEl);
}
}, 300);
}, 3000);
}
// ==================== 项目管理 ====================
let projectsObserver = null;
function initProjectsLazyLoad() {
// 如果已经初始化过,先清理
if (projectsObserver) {
const loader = document.getElementById('projectsLoader');
if (loader) {
projectsObserver.unobserve(loader);
}
}
// 使用 Intersection Observer 监听滚动到底部
const container = document.getElementById('projectsCardContainer');
if (!container) return;
// 创建或获取加载指示器
let loader = document.getElementById('projectsLoader');
if (!loader) {
loader = document.createElement('div');
loader.id = 'projectsLoader';
loader.className = 'projects-loader';
loader.style.display = 'none';
loader.innerHTML = `${t('common.loading')}
`;
const projectsList = document.getElementById('projects-list');
if (projectsList) {
projectsList.appendChild(loader);
}
}
// 创建 Intersection Observer
projectsObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting && projectsHasMore && !projectsLoading) {
loadProjects(false);
}
});
}, {
root: null,
rootMargin: '100px', // 提前100px开始加载
threshold: 0.1
});
// 观察加载指示器
projectsObserver.observe(loader);
// 初始加载(如果还没有加载过)
if (projectsLoadedCount === 0) {
loadProjects(true);
}
}
async function loadProjects(reset = false) {
if (projectsLoading || (!projectsHasMore && !reset)) return;
projectsLoading = true;
const loader = document.getElementById('projectsLoader');
if (loader) loader.style.display = 'block';
try {
const skip = reset ? 0 : projectsLoadedCount;
const limit = PROJECTS_LOAD_SIZE;
const projects = await getProjects(skip, limit);
if (reset) {
projectsLoadedCount = 0;
const container = document.getElementById('projectsCardContainer');
if (container) container.innerHTML = '';
}
if (projects.length === 0) {
projectsHasMore = false;
if (loader) loader.style.display = 'none';
const container = document.getElementById('projectsCardContainer');
if (container && container.children.length === 0) {
container.innerHTML = `${t('project.noProjects')}
`;
}
projectsLoading = false;
return;
}
// 判断是否还有更多数据
if (projects.length < limit) {
projectsHasMore = false;
} else {
// 尝试获取下一页来判断是否还有更多数据
try {
const nextPage = await getProjects(skip + limit, 1);
projectsHasMore = nextPage.length > 0;
} catch (error) {
projectsHasMore = true; // 出错时假设还有更多
}
}
projectsLoadedCount += projects.length;
await renderProjectsTable(projects, reset);
// 如果没有更多数据,隐藏加载指示器
if (!projectsHasMore && loader) {
loader.style.display = 'none';
}
} catch (error) {
console.error('加载项目失败:', error);
showError('加载项目失败: ' + (error.message || '未知错误'));
if (loader) loader.style.display = 'none';
} finally {
projectsLoading = false;
}
}
async function renderProjectsTable(projects, reset = false) {
const container = document.getElementById('projectsCardContainer');
if (!container) return;
// 获取每个项目的统计信息
const projectsWithStats = await Promise.all(projects.map(async (project) => {
try {
const stats = await getProjectStats(project.id);
return { ...project, datasets_count: stats.datasets_count || 0, configs_count: stats.configs_count || 0 };
} catch (error) {
return { ...project, datasets_count: 0, configs_count: 0 };
}
}));
const cardsHTML = projectsWithStats.map(project => `
${escapeHtml(project.description || t('common.description') || '-')}
📊
${project.datasets_count || 0}
${t('project.datasets')}
⚙️
${project.configs_count || 0}
${t('project.configs')}
`).join('');
if (reset) {
container.innerHTML = cardsHTML;
} else {
container.insertAdjacentHTML('beforeend', cardsHTML);
}
}
function showProjectForm(project = null) {
editingId = project ? project.id : null;
const title = project ? t('project.editProject') : t('project.addProject');
const content = `
`;
openModal(title, content, async () => {
await saveProject();
});
}
async function saveProject() {
const name = document.getElementById('projectName').value.trim();
const description = document.getElementById('projectDescription').value.trim();
const version = document.getElementById('projectVersion').value.trim();
const status = document.getElementById('projectStatus').value;
const category = document.getElementById('projectCategory').value.trim();
const tagsStr = document.getElementById('projectTags').value.trim();
const source = document.getElementById('projectSource').value.trim();
const sourceUrl = document.getElementById('projectSourceUrl').value.trim();
const displayExtraFieldsStr = document.getElementById('projectDisplayExtraFields').value.trim();
const evaluationPurpose = document.getElementById('projectEvaluationPurpose').value.trim();
const deadline = document.getElementById('projectDeadline').value.trim();
// 验证必填字段
if (!description) {
showError('任务描述不能为空');
return;
}
if (!evaluationPurpose) {
showError('评估目的不能为空');
return;
}
if (!deadline) {
showError('要求完成时间不能为空');
return;
}
try {
const data = {
name,
description: description || null,
version: version || null,
status: status || 'active',
category: category || null,
source: source || null,
source_url: sourceUrl || null
};
// 处理标签
if (tagsStr) {
data.tags = tagsStr.split(',').map(t => t.trim()).filter(t => t);
}
// 处理display_extra_fields
if (displayExtraFieldsStr) {
data.display_extra_fields = displayExtraFieldsStr.split(',').map(f => f.trim()).filter(f => f);
}
// 处理元数据(评估目的和完成时间)
const metadata = {};
metadata.evaluation_purpose = evaluationPurpose;
// datetime-local 格式是 YYYY-MM-DDTHH:mm,需要转换为 ISO 8601 格式
// 如果已经是正确的格式,直接使用;否则转换
if (deadline.includes('T')) {
metadata.deadline = deadline.substring(0, 16); // 确保格式为 YYYY-MM-DDTHH:mm
} else {
metadata.deadline = deadline.replace(' ', 'T').substring(0, 16);
}
data.metadata = metadata;
if (editingId) {
await updateProject(editingId, data);
showSuccess('项目更新成功');
} else {
await createProject(data);
showSuccess('项目创建成功');
}
closeModal();
if (!editingId) {
// 重置并重新加载项目列表
projectsLoadedCount = 0;
projectsHasMore = true;
const container = document.getElementById('projectsCardContainer');
if (container) container.innerHTML = '加载中...
';
loadProjects(true);
} else {
// 编辑项目后,刷新当前显示的项目卡片
loadProjects(true);
}
// 如果正在查看项目详情,刷新详情视图
if (editingId && currentProjectId === editingId) {
await viewProjectDetail(editingId);
}
} catch (error) {
showError('保存失败: ' + (error.message || '未知错误'));
}
}
async function editProject(projectId) {
try {
const project = await getProject(projectId);
showProjectForm(project);
} catch (error) {
showError('加载项目失败: ' + (error.message || '未知错误'));
}
}
async function deleteProjectHandler(projectId) {
try {
// 获取项目信息和数据集列表
const project = await getProject(projectId, true, false);
const datasets = await getProjectDatasets(projectId, 0, 1000);
const datasetsCount = datasets.length;
// 显示选择对话框
const title = t('project.deleteProject');
const projectName = project.name || `${t('project.project')} ${projectId}`;
let datasetsInfo = '';
if (datasetsCount > 0) {
const datasetsList = datasets.slice(0, 5).map(d => escapeHtml(d.name || `${t('dataset.dataset')} ${d.id}`)).join(t('common.comma'));
const moreText = datasetsCount > 5 ? t('project.totalDatasets', { count: datasetsCount }) : '';
datasetsInfo = `
${t('project.projectContainsDatasets')}:
${datasetsList}${moreText}
`;
}
const content = `
${t('project.selectAction')}:
${t('project.projectLabel')}: ${escapeHtml(projectName)} (ID: ${projectId})
${datasetsInfo}
`;
openModal(title, content, async () => {
const modalBody = document.getElementById('modalBody');
const selectedAction = modalBody.querySelector('input[name="deleteAction"]:checked');
if (!selectedAction) {
showError(t('project.selectActionRequired'));
return;
}
const action = selectedAction.value;
await handleDeleteProjectAction(projectId, action, datasets);
});
} catch (error) {
showError(t('project.loadProjectInfoFailed') + ': ' + (error.message || t('common.unknownError')));
}
}
async function handleDeleteProjectAction(projectId, action, datasets) {
try {
if (action === 'delete') {
// 先删除所有数据集
if (datasets && datasets.length > 0) {
// 关闭选择对话框,显示删除进度
closeModal();
showMessage(`${t('project.deletingDatasets', { count: datasets.length })}...`, 'info');
let successCount = 0;
let failCount = 0;
const errors = [];
for (const dataset of datasets) {
try {
await apiDelete(`/datasets/${dataset.id}`);
successCount++;
} catch (error) {
failCount++;
errors.push(`${t('dataset.dataset')} ${dataset.name || dataset.id}: ${error.message || t('actions.delete') + t('common.failed')}`);
console.error(`${t('project.deleteDatasetFailed')} ${dataset.id}:`, error);
}
}
if (failCount > 0) {
showError(`${t('project.deleteDatasetError')}: ${t('project.success')} ${successCount} ${t('common.count')}, ${t('common.failed')} ${failCount} ${t('common.count')}. ${t('project.projectDeleteCancelled')}.`);
if (errors.length > 0) {
console.error(t('project.deleteDatasetErrorDetails'), errors);
}
return;
}
}
}
// 删除项目
await deleteProject(projectId);
showSuccess(action === 'delete' ? t('project.projectAndDatasetsDeleted') : t('project.projectDeleted'));
// 如果模态框还在打开状态,关闭它
if (action === 'remove' || (action === 'delete' && (!datasets || datasets.length === 0))) {
closeModal();
}
// 重置并重新加载项目列表
projectsLoadedCount = 0;
projectsHasMore = true;
const container = document.getElementById('projectsCardContainer');
if (container) container.innerHTML = `${t('common.loading')}
`;
loadProjects(true);
} catch (error) {
const actionText = action === 'delete' ? t('project.deleteProjectAndDatasets') : t('project.deleteProjectOnly');
showError(`${actionText}${t('common.failed')}: ` + (error.message || t('common.unknownError')));
}
}
async function viewProjectDetail(projectId) {
currentProjectId = projectId;
try {
const projectsList = document.getElementById('projects-list');
const detailContainer = document.getElementById('project-detail-container');
if (!projectsList || !detailContainer) {
showError('页面元素未找到');
return;
}
// 隐藏项目列表,显示详情页容器
projectsList.style.display = 'none';
detailContainer.style.display = 'block';
// 如果详情页内容未加载,则动态加载
if (detailContainer.innerHTML.trim() === '') {
try {
const response = await fetch('/project-detail.html');
if (!response.ok) {
throw new Error('加载详情页失败');
}
const html = await response.text();
detailContainer.innerHTML = html;
// 更新模板中的 i18n 翻译
updateTemplateTranslations(detailContainer);
} catch (error) {
console.error('加载详情页HTML失败:', error);
showError('加载详情页失败: ' + (error.message || '未知错误'));
// 恢复显示项目列表
projectsList.style.display = 'flex';
detailContainer.style.display = 'none';
return;
}
}
// 加载项目详情数据
const project = await getProject(projectId, true, true);
const stats = await getProjectStats(projectId);
// 填充项目信息
const detailContent = detailContainer.querySelector('#projectDetailContent');
if (detailContent) {
detailContent.querySelector('#projectDetailId').textContent = project.id;
detailContent.querySelector('#projectDetailName').textContent = project.name || '-';
detailContent.querySelector('#projectDetailTitle').textContent = project.name || t('project.projectDetail');
detailContent.querySelector('#projectDetailVersion').textContent = project.version || '-';
// 状态显示为带样式的标签
const statusElement = detailContent.querySelector('#projectDetailStatus');
const statusValue = project.status || 'active';
statusElement.textContent = t(`status.${statusValue}`);
statusElement.className = 'project-info-value status-badge ' + (statusValue === 'active' ? 'active' : 'inactive');
detailContent.querySelector('#projectDetailCategory').textContent = project.category || '-';
detailContent.querySelector('#projectDetailCreator').textContent = project.creator || '-';
detailContent.querySelector('#projectDetailCreatedAt').textContent = formatDateTime(project.created_at);
detailContent.querySelector('#projectDetailUpdatedAt').textContent = formatDateTime(project.updated_at);
detailContent.querySelector('#projectDetailDescription').textContent = project.description || t('common.noDescription');
// 显示评估目的和完成时间(从metadata中读取)
const evaluationPurpose = project.metadata && project.metadata.evaluation_purpose ? project.metadata.evaluation_purpose : '-';
const deadline = project.metadata && project.metadata.deadline ? formatDateTime(project.metadata.deadline) : '-';
detailContent.querySelector('#projectDetailEvaluationPurpose').textContent = evaluationPurpose;
detailContent.querySelector('#projectDetailDeadline').textContent = deadline;
detailContent.querySelector('#projectDetailDatasetsCount').textContent = stats.datasets_count || 0;
detailContent.querySelector('#projectDetailConfigsCount').textContent = stats.configs_count || 0;
}
// 重置数据集懒加载状态
projectDatasetsLoadedCount = 0;
projectDatasetsHasMore = true;
projectDatasetsLoading = false;
// 加载数据集和配置
loadProjectDatasets(projectId, true);
loadProjectConfigs(projectId);
} catch (error) {
console.error('加载项目详情失败:', error);
showError('加载项目详情失败: ' + (error.message || '未知错误'));
// 恢复显示项目列表
const projectsList = document.getElementById('projects-list');
const detailContainer = document.getElementById('project-detail-container');
if (projectsList) projectsList.style.display = 'flex';
if (detailContainer) detailContainer.style.display = 'none';
}
}
function switchToProjectsList() {
resetProjectsTabs();
}
function switchProjectDetailTab(tab) {
// 切换项目详情内的子标签页(数据集管理/配置管理/标注结果分析)
const detailContainer = document.getElementById('project-detail-container');
if (!detailContainer) return;
detailContainer.querySelectorAll('.project-tabs .tab-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.tab === tab);
});
detailContainer.querySelectorAll('.project-tabs .tab-content').forEach(content => {
content.classList.toggle('active', content.id === tab);
});
// 如果切换到分析Tab,加载分析数据
if (tab === 'project-analysis' && currentProjectId) {
loadAnnotationAnalysis(currentProjectId);
}
}
// 加载标注结果分析
async function loadAnnotationAnalysis(projectId) {
try {
// 确保CSS已加载
await ensureStylesheetLoaded('/static/css/annotation-analysis.css');
// 确保JS已加载
await ensureScriptLoaded('/static/js/annotation-analysis.js');
// 等待一小段时间确保脚本完全解析
await new Promise(resolve => setTimeout(resolve, 100));
// 检查 Chart.js 是否可用(已在 HTML 中预加载)
if (typeof Chart === 'undefined') {
console.error('Chart.js 未加载!请检查 manager.html 中的 Chart.js 引入');
const container = document.getElementById('analysisConfigsContainer');
if (container) {
container.innerHTML = `${t('project.chartLibLoadFailed')}
`;
}
return;
}
// 初始化分析页面
if (window.AnnotationAnalysis) {
window.AnnotationAnalysis.init(projectId);
} else {
console.error('AnnotationAnalysis未定义!');
// 显示错误信息
const container = document.getElementById('analysisConfigsContainer');
if (container) {
container.innerHTML = '加载分析模块失败,请刷新页面重试
';
}
}
} catch (error) {
console.error('加载标注结果分析时出错:', error);
const container = document.getElementById('analysisConfigsContainer');
if (container) {
container.innerHTML = `${t('project.loadFailed')}: ${error.message}
`;
}
}
}
// 辅助函数:在详情页容器中查找元素
function getDetailElement(id) {
const detailContainer = document.getElementById('project-detail-container');
if (!detailContainer) return null;
return detailContainer.querySelector(`#${id}`);
}
async function loadProjectDatasets(projectId, reset = false) {
if (projectDatasetsLoading || (!projectDatasetsHasMore && !reset)) return;
projectDatasetsLoading = true;
const loader = getDetailElement('projectDatasetsLoader');
if (loader) loader.style.display = 'block';
try {
const skip = reset ? 0 : projectDatasetsLoadedCount;
const limit = PROJECT_DATASETS_LOAD_SIZE;
const datasets = await getProjectDatasets(projectId, skip, limit);
if (reset) {
projectDatasetsLoadedCount = 0;
const container = getDetailElement('projectDatasetsCardContainer');
if (container) container.innerHTML = '';
}
if (datasets.length === 0) {
projectDatasetsHasMore = false;
if (loader) loader.style.display = 'none';
const container = getDetailElement('projectDatasetsCardContainer');
if (container && container.children.length === 0) {
container.innerHTML = `${t('dataset.noDatasets')}
`;
}
projectDatasetsLoading = false;
return;
}
if (datasets.length < limit) {
projectDatasetsHasMore = false;
}
projectDatasetsLoadedCount += datasets.length;
renderProjectDatasetsCards(datasets, reset);
if (loader) loader.style.display = 'none';
projectDatasetsLoading = false;
// 初始化懒加载(如果还没有初始化)
if (reset) {
initProjectDatasetsLazyLoad(projectId);
}
} catch (error) {
console.error('加载项目数据集失败:', error);
showError('加载项目数据集失败: ' + (error.message || '未知错误'));
if (loader) loader.style.display = 'none';
projectDatasetsLoading = false;
}
}
function renderProjectDatasetsCards(datasets, reset = false) {
const detailContainer = document.getElementById('project-detail-container');
if (!detailContainer) return;
const container = detailContainer.querySelector('#projectDatasetsCardContainer');
if (!container) return;
if (datasets.length === 0 && reset) {
container.innerHTML = `${t('dataset.noDatasets')}
`;
return;
}
const cardsHTML = datasets.map(dataset => `
${escapeHtml(dataset.description || t('common.description') || '-')}
`).join('');
if (reset) {
container.innerHTML = cardsHTML;
} else {
container.insertAdjacentHTML('beforeend', cardsHTML);
}
// 异步加载每个数据集的标注进度
datasets.forEach(async dataset => {
try {
// 使用 DatasetManagement 模块的方法
const progress = window.DatasetManagement
? await window.DatasetManagement.getDatasetAnnotationProgress(dataset.id)
: await apiGet(`/datasets/${dataset.id}/annotation-progress`).catch(() => null);
const card = container.querySelector(`[data-dataset-id="${dataset.id}"]`);
if (card) {
const progressContainer = card.querySelector('.project-card-annotation-progress');
if (progressContainer) {
if (progress) {
const progressHtml = window.DatasetManagement
? window.DatasetManagement.renderAnnotationProgress(progress)
: (() => {
// Fallback 函数,如果模块未加载
if (!progress || progress.total_items === 0) {
return `${t('common.noData')}`;
}
const overallRate = progress.overall_progress_rate || 0;
const progressColor = overallRate >= 80 ? '#2e7d32' : overallRate >= 50 ? '#f57c00' : '#d32f2f';
let html = `
${overallRate.toFixed(1)}%
${progress.annotated_items || 0} / ${progress.total_items}
`;
// 显示各个配置的进度条
if (progress.config_progress && progress.config_progress.length > 0) {
const configDetails = progress.config_progress.map(cp => {
const cpColor = cp.progress_rate >= 80 ? '#2e7d32' : cp.progress_rate >= 50 ? '#f57c00' : '#d32f2f';
return `
${cp.config_name}:
${cp.progress_rate.toFixed(1)}%
`;
}).join('');
html += `
${t('common.viewDetails')}
${configDetails}
`;
}
html += '
';
return html;
})();
progressContainer.innerHTML = progressHtml;
} else {
progressContainer.innerHTML = `${t('common.noData')}
`;
}
}
}
} catch (error) {
console.error(`加载数据集 ${dataset.id} 的标注进度失败:`, error);
const card = container.querySelector(`[data-dataset-id="${dataset.id}"]`);
if (card) {
const progressContainer = card.querySelector('.project-card-annotation-progress');
if (progressContainer) {
progressContainer.innerHTML = `${t('common.loadFailed')}
`;
}
}
}
});
}
function initProjectDatasetsLazyLoad(projectId) {
// 如果已经初始化过,先清理
if (projectDatasetsObserver) {
const loader = getDetailElement('projectDatasetsLoader');
if (loader) {
projectDatasetsObserver.unobserve(loader);
}
}
// 使用 Intersection Observer 监听滚动到底部
const container = getDetailElement('projectDatasetsCardContainer');
if (!container) return;
// 创建或获取加载指示器
let loader = getDetailElement('projectDatasetsLoader');
if (!loader) {
loader = document.createElement('div');
loader.id = 'projectDatasetsLoader';
loader.style.display = 'none';
loader.style.textAlign = 'center';
loader.style.padding = '20px';
loader.style.color = '#999';
loader.innerHTML = '加载中...
';
const detailContainer = document.getElementById('project-detail-container');
const datasetsTab = detailContainer?.querySelector('#project-datasets');
if (datasetsTab) {
datasetsTab.appendChild(loader);
}
}
// 创建 Intersection Observer
projectDatasetsObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting && projectDatasetsHasMore && !projectDatasetsLoading) {
loadProjectDatasets(projectId, false);
}
});
}, {
root: null,
rootMargin: '100px', // 提前100px开始加载
threshold: 0.1
});
// 观察加载指示器
projectDatasetsObserver.observe(loader);
}
async function showAddDatasetToProjectModal() {
try {
// 获取所有数据集
const allDatasets = await apiGet('/datasets/?skip=0&limit=1000');
// 获取项目当前的数据集
const projectDatasets = await getProjectDatasets(currentProjectId, 0, 1000);
const projectDatasetIds = new Set(projectDatasets.map(d => d.id));
// 过滤出未关联的数据集
const availableDatasets = allDatasets.filter(d => !projectDatasetIds.has(d.id));
if (availableDatasets.length === 0) {
showError('没有可添加的数据集');
return;
}
const content = `
`;
openModal('添加数据集到项目', content, async () => {
const datasetId = parseInt(document.getElementById('datasetSelectForProject').value);
if (!datasetId) {
showError('请选择数据集');
return;
}
await addDatasetToProjectHandler(currentProjectId, datasetId);
});
} catch (error) {
showError('加载数据集列表失败: ' + (error.message || '未知错误'));
}
}
async function addDatasetToProjectHandler(projectId, datasetId) {
try {
await addDatasetToProject(projectId, datasetId);
showSuccess('数据集已添加到项目');
closeModal();
// 重置懒加载状态并重新加载
projectDatasetsLoadedCount = 0;
projectDatasetsHasMore = true;
loadProjectDatasets(projectId, true);
// 更新统计信息
const stats = await getProjectStats(projectId);
const datasetsCountEl = getDetailElement('projectDetailDatasetsCount');
if (datasetsCountEl) datasetsCountEl.textContent = stats.datasets_count || 0;
} catch (error) {
showError('添加数据集失败: ' + (error.message || '未知错误'));
}
}
async function removeDatasetFromProjectHandler(projectId, datasetId) {
try {
// 获取数据集信息以显示名称
const dataset = await apiGet(`/datasets/${datasetId}`);
const datasetName = dataset.name || `数据集 ${datasetId}`;
// 显示选择对话框
const title = '移除数据集';
const content = `
请选择操作方式:
数据集:${escapeHtml(datasetName)} (ID: ${datasetId})
`;
openModal(title, content, async () => {
const modalBody = document.getElementById('modalBody');
const selectedAction = modalBody.querySelector('input[name="removeAction"]:checked');
if (!selectedAction) {
showError('请选择操作方式');
return;
}
const action = selectedAction.value;
await handleRemoveDatasetAction(projectId, datasetId, action);
});
} catch (error) {
showError('加载数据集信息失败: ' + (error.message || '未知错误'));
}
}
async function handleRemoveDatasetAction(projectId, datasetId, action) {
try {
if (action === 'remove') {
// 仅移出项目
await removeDatasetFromProject(projectId, datasetId);
showSuccess('数据集已从项目移除');
} else if (action === 'delete') {
// 删除数据集
await apiDelete(`/datasets/${datasetId}`);
showSuccess('数据集已删除');
}
closeModal();
// 重置懒加载状态并重新加载
projectDatasetsLoadedCount = 0;
projectDatasetsHasMore = true;
loadProjectDatasets(projectId, true);
// 更新统计信息
const stats = await getProjectStats(projectId);
const datasetsCountEl = getDetailElement('projectDetailDatasetsCount');
if (datasetsCountEl) datasetsCountEl.textContent = stats.datasets_count || 0;
} catch (error) {
const actionText = action === 'remove' ? '移除' : '删除';
showError(`${actionText}数据集失败: ` + (error.message || '未知错误'));
}
}
// ==================== 导入数据集到项目 ====================
// 加载用户列表(用于下拉选择)
async function loadUsersForSelect() {
if (usersCache) {
return usersCache;
}
try {
const users = await apiGet('/users/?skip=0&limit=1000');
usersCache = users;
return users;
} catch (error) {
console.error('加载用户列表失败:', error);
return [];
}
}
async function showImportDatasetToProjectForm() {
if (!currentProjectId) {
showError('请先选择项目');
return;
}
// 加载用户列表
const users = await loadUsersForSelect();
const userOptions = '' + users.map(user =>
``
).join('');
const title = '导入数据集到项目';
const content = `
`;
openModal(title, content, async () => {
await handleImportDatasetToProject();
});
// 设置文件选择监听
const fileInput = document.getElementById('importDatasetToProjectFile');
const filesList = document.getElementById('importDatasetToProjectFilesList');
const filesPreview = document.getElementById('importDatasetToProjectFilesPreview');
fileInput.addEventListener('change', () => {
const files = Array.from(fileInput.files);
if (files.length === 0) {
filesList.style.display = 'none';
return;
}
filesList.style.display = 'block';
filesPreview.innerHTML = files.map((file, index) => `
${index + 1}. ${escapeHtml(file.name)}
`).join('');
});
}
async function handleImportDatasetToProject() {
if (!currentProjectId) {
showError('请先选择项目');
return;
}
const fileInput = document.getElementById('importDatasetToProjectFile');
const files = Array.from(fileInput.files);
if (files.length === 0) {
showError('请至少选择一个文件');
return;
}
// 验证文件格式
for (const file of files) {
if (!file.name.endsWith('.jsonl')) {
showError(`文件 ${file.name} 不是.jsonl格式`);
return;
}
}
// 收集元数据(这些将应用到所有文件)
const name = document.getElementById('importDatasetToProjectName').value.trim();
const description = document.getElementById('importDatasetToProjectDescription').value.trim();
const version = document.getElementById('importDatasetToProjectVersion').value.trim();
const category = document.getElementById('importDatasetToProjectCategory').value.trim();
const status = document.getElementById('importDatasetToProjectStatus').value;
const tags = document.getElementById('importDatasetToProjectTags').value.trim();
const source = document.getElementById('importDatasetToProjectSource').value.trim();
const sourceUrl = document.getElementById('importDatasetToProjectSourceUrl').value.trim();
const annotatorIdInput = document.getElementById('importDatasetToProjectAnnotatorId').value;
const annotatorId = annotatorIdInput ? parseInt(annotatorIdInput) : null;
// 显示进度
const progressDiv = document.getElementById('importDatasetToProjectProgress');
const resultDiv = document.getElementById('importDatasetToProjectResult');
progressDiv.style.display = 'block';
resultDiv.style.display = 'none';
try {
// 使用导入项目的API,但指定project_id
const formData = new FormData();
files.forEach(file => {
formData.append('files', file);
});
formData.append('project_id', currentProjectId);
if (name) formData.append('dataset_name_prefix', name);
if (description) formData.append('project_description', description);
if (version) formData.append('project_version', version);
if (category) formData.append('project_category', category);
if (status) formData.append('project_status', status);
if (tags) formData.append('project_tags', tags);
if (source) formData.append('project_source', source);
if (sourceUrl) formData.append('project_source_url', sourceUrl);
if (annotatorId) formData.append('annotator_id', annotatorId);
// 调用导入项目API
const result = await importProject(formData);
// 隐藏进度,显示结果
progressDiv.style.display = 'none';
resultDiv.style.display = 'block';
const successMsg = `导入完成!成功导入 ${result.successful_files}/${result.total_files} 个文件到项目 "${result.project_name}",共 ${result.total_imported} 条QA对,失败 ${result.total_failed} 条`;
let resultHtml = `${successMsg}
`;
if (result.file_results && result.file_results.length > 0) {
resultHtml += '文件导入详情
';
resultHtml += '';
resultHtml += result.file_results.map(r => {
if (r.success) {
return `
${escapeHtml(r.filename)}: 成功导入 ${r.imported_count} 条,失败 ${r.failed_count} 条
${r.errors && r.errors.length > 0 ? `
${r.errors.slice(0, 3).map(e => escapeHtml(e)).join('
')}
` : ''}
`;
} else {
return `
${escapeHtml(r.filename)}: 导入失败 - ${escapeHtml(r.error || '未知错误')}
`;
}
}).join('');
resultHtml += '
';
}
if (result.errors && result.errors.length > 0) {
resultHtml += `错误详情 (显示前${Math.min(result.errors.length, 10)}个)
`;
resultHtml += ``;
resultHtml += result.errors.slice(0, 10).map(err => `- ${escapeHtml(err)}
`).join('');
resultHtml += '
';
}
resultDiv.innerHTML = resultHtml;
// 刷新项目数据集列表
if (result.total_imported > 0) {
setTimeout(() => {
projectDatasetsLoadedCount = 0;
projectDatasetsHasMore = true;
loadProjectDatasets(currentProjectId, true);
// 更新统计信息
getProjectStats(currentProjectId).then(stats => {
const datasetsCountEl = getDetailElement('projectDetailDatasetsCount');
if (datasetsCountEl) datasetsCountEl.textContent = stats.datasets_count || 0;
});
}, 1000);
}
// 3秒后自动关闭模态框
setTimeout(() => {
closeModal();
}, 3000);
} catch (error) {
progressDiv.style.display = 'none';
// 如果是401未授权错误,清除token并跳转到登录页
if (error.status === 401) {
clearToken();
const currentPath = window.location.pathname;
if (currentPath !== '/auth') {
const redirectUrl = encodeURIComponent(window.location.href);
window.location.href = `/auth?redirect=${redirectUrl}`;
}
}
showError('导入失败: ' + (error.message || '未知错误'));
}
}
async function loadProjectConfigs(projectId) {
try {
const configs = await getProjectConfigs(projectId);
renderProjectConfigsTable(configs);
} catch (error) {
console.error('Failed to load project configs:', error);
showError(t('project.loadConfigsFailed') + ': ' + (error.message || t('common.unknownError')));
}
}
function renderProjectConfigsTable(configs) {
const detailContainer = document.getElementById('project-detail-container');
if (!detailContainer) return;
const tbody = detailContainer.querySelector('#projectConfigsTableBody');
if (!tbody) return;
if (configs.length === 0) {
tbody.innerHTML = `| ${t('project.noConfigs')} |
`;
return;
}
tbody.innerHTML = configs.map((config, index) => `
|
${index + 1}
|
${config.id} |
${escapeHtml(config.name)} |
${escapeHtml(config.annotation_type || config.type || '-')} |
${escapeHtml(config.description || '-')} |
${config.required ? t('common.yes') : t('common.no')}
|
|
`).join('');
}
async function showAddConfigToProjectModal() {
try {
// 获取所有标注配置
const allConfigs = await apiGet('/annotation-configs/?skip=0&limit=1000');
// 获取项目当前的配置
const projectConfigs = await getProjectConfigs(currentProjectId);
const projectConfigIds = new Set(projectConfigs.map(c => c.id));
// 过滤出未关联的配置
const availableConfigs = allConfigs.filter(c => !projectConfigIds.has(c.id));
if (availableConfigs.length === 0) {
showError(t('project.noAvailableConfigs'));
return;
}
const content = `
`;
openModal(t('project.addConfigToProject'), content, async () => {
const configId = parseInt(document.getElementById('configSelectForProject').value);
if (!configId) {
showError(t('project.selectConfigRequired'));
return;
}
await addConfigToProjectHandler(currentProjectId, configId);
});
} catch (error) {
showError(t('project.loadConfigsListFailed') + ': ' + (error.message || t('common.unknownError')));
}
}
async function addConfigToProjectHandler(projectId, configId) {
try {
await addConfigToProject(projectId, configId);
showSuccess('标注配置已添加到项目');
closeModal();
loadProjectConfigs(projectId);
// 更新统计信息
const stats = await getProjectStats(projectId);
const configsCountEl = getDetailElement('projectDetailConfigsCount');
if (configsCountEl) configsCountEl.textContent = stats.configs_count || 0;
} catch (error) {
showError('添加标注配置失败: ' + (error.message || '未知错误'));
}
}
async function removeConfigFromProjectHandler(projectId, configId) {
if (!confirm('确定要从项目中移除这个标注配置吗?')) {
return;
}
try {
await removeConfigFromProject(projectId, configId);
showSuccess('标注配置已从项目移除');
loadProjectConfigs(projectId);
// 更新统计信息
const stats = await getProjectStats(projectId);
const configsCountEl = getDetailElement('projectDetailConfigsCount');
if (configsCountEl) configsCountEl.textContent = stats.configs_count || 0;
} catch (error) {
showError('移除标注配置失败: ' + (error.message || '未知错误'));
}
}
async function moveConfigOrder(projectId, configId, direction) {
try {
await apiPost(`/projects/${projectId}/configs/${configId}/move?direction=${direction}`);
showSuccess('顺序调整成功');
loadProjectConfigs(projectId);
} catch (error) {
showError('调整顺序失败: ' + (error.message || '未知错误'));
}
}
// ==================== 导入项目 ====================
function showImportProjectForm() {
const title = '导入项目';
const content = `
`;
openModal(title, content, async () => {
await handleImportProject();
});
// 设置文件选择监听
const fileInput = document.getElementById('importProjectFiles');
const filesList = document.getElementById('importProjectFilesList');
const filesPreview = document.getElementById('importProjectFilesPreview');
const datasetNamesDiv = document.getElementById('importProjectDatasetNames');
const datasetNamesList = document.getElementById('importProjectDatasetNamesList');
const prefixInput = document.getElementById('importProjectDatasetPrefix');
const projectNameInput = document.getElementById('importProjectName');
const modeRadios = document.querySelectorAll('input[name="importMode"]');
const existingProjectSelect = document.getElementById('existingProjectSelect');
const existingProjectIdSelect = document.getElementById('importProjectExistingId');
const newProjectInfoSection = document.getElementById('newProjectInfoSection');
// 文件选择监听
fileInput.addEventListener('change', () => {
updateFilesPreview();
updateDatasetNamesPreview();
});
// 导入模式切换
modeRadios.forEach(radio => {
radio.addEventListener('change', () => {
if (radio.value === 'existing') {
// 导入到现有项目:显示项目选择,隐藏项目信息表单
existingProjectSelect.style.display = 'block';
newProjectInfoSection.style.display = 'none';
document.getElementById('importProjectName').required = false;
loadProjectsForSelect();
} else {
// 创建新项目:隐藏项目选择,显示项目信息表单
existingProjectSelect.style.display = 'none';
newProjectInfoSection.style.display = 'block';
document.getElementById('importProjectName').required = true;
}
// 更新数据集名称预览(因为前缀可能会变化)
updateDatasetNamesPreview();
});
});
// 前缀和项目名称变化时更新数据集名称预览
prefixInput.addEventListener('input', updateDatasetNamesPreview);
projectNameInput.addEventListener('input', () => {
if (!prefixInput.value.trim()) {
updateDatasetNamesPreview();
}
});
// 现有项目选择变化时也更新预览
existingProjectIdSelect.addEventListener('change', () => {
if (!prefixInput.value.trim()) {
updateDatasetNamesPreview();
}
});
function updateFilesPreview() {
const files = Array.from(fileInput.files);
if (files.length === 0) {
filesList.style.display = 'none';
return;
}
filesList.style.display = 'block';
filesPreview.innerHTML = files.map((file, index) => `
${index + 1}. ${escapeHtml(file.name)}
`).join('');
}
function updateDatasetNamesPreview() {
const files = Array.from(fileInput.files);
if (files.length === 0) {
datasetNamesDiv.style.display = 'none';
return;
}
datasetNamesDiv.style.display = 'block';
// 确定前缀:优先使用自定义前缀,否则根据模式使用项目名称
let prefix = prefixInput.value.trim();
if (!prefix) {
const selectedMode = document.querySelector('input[name="importMode"]:checked').value;
if (selectedMode === 'existing') {
// 导入到现有项目:从下拉框获取项目名称
const selectedOption = existingProjectIdSelect.options[existingProjectIdSelect.selectedIndex];
prefix = selectedOption ? selectedOption.text : '项目名';
} else {
// 创建新项目:从输入框获取
prefix = projectNameInput.value.trim() || '项目名';
}
}
datasetNamesList.innerHTML = files.map((file, index) => {
const filenameWithoutExt = file.name.replace(/\.(jsonl|json)$/i, '');
const defaultName = `${prefix}_${filenameWithoutExt}`;
return `
${escapeHtml(file.name)}:
`;
}).join('');
}
async function loadProjectsForSelect() {
try {
const projects = await apiGet('/projects?limit=1000');
existingProjectIdSelect.innerHTML = '' +
projects.map(p => ``).join('');
} catch (error) {
console.error('加载项目列表失败:', error);
}
}
}
async function handleImportProject() {
const fileInput = document.getElementById('importProjectFiles');
const files = Array.from(fileInput.files);
if (files.length === 0) {
showError('请至少选择一个文件');
return;
}
// 验证文件格式
for (const file of files) {
if (!file.name.endsWith('.jsonl')) {
showError(`文件 ${file.name} 不是.jsonl格式`);
return;
}
}
// 获取导入模式
const importMode = document.querySelector('input[name="importMode"]:checked').value;
const projectId = importMode === 'existing'
? document.getElementById('importProjectExistingId').value
: null;
if (importMode === 'existing' && !projectId) {
showError('请选择要导入的项目');
return;
}
if (importMode === 'new') {
const projectName = document.getElementById('importProjectName').value.trim();
if (!projectName) {
showError('项目名称不能为空');
return;
}
}
// 收集项目信息
const projectName = document.getElementById('importProjectName').value.trim();
const projectDescription = document.getElementById('importProjectDescription').value.trim();
const projectVersion = document.getElementById('importProjectVersion').value.trim();
const projectStatus = document.getElementById('importProjectStatus').value;
const projectCategory = document.getElementById('importProjectCategory').value.trim();
const projectTags = document.getElementById('importProjectTags').value.trim();
const projectSource = document.getElementById('importProjectSource').value.trim();
const projectSourceUrl = document.getElementById('importProjectSourceUrl').value.trim();
const projectEvaluationPurpose = document.getElementById('importProjectEvaluationPurpose').value.trim();
const projectDeadline = document.getElementById('importProjectDeadline').value.trim();
// 如果是创建新项目,验证必填字段
if (importMode === 'new') {
if (!projectDescription) {
showError('任务描述不能为空');
return;
}
if (!projectEvaluationPurpose) {
showError('评估目的不能为空');
return;
}
if (!projectDeadline) {
showError('要求完成时间不能为空');
return;
}
}
const datasetPrefix = document.getElementById('importProjectDatasetPrefix').value.trim();
// 收集数据集名称映射
const nameMapping = {};
const datasetNameInputs = document.querySelectorAll('#importProjectDatasetNamesList input[data-file-index]');
datasetNameInputs.forEach(input => {
const filename = input.getAttribute('data-filename');
const datasetName = input.value.trim();
if (datasetName) {
nameMapping[filename] = datasetName;
}
});
// 显示进度
const progressDiv = document.getElementById('importProjectProgress');
const resultDiv = document.getElementById('importProjectResult');
progressDiv.style.display = 'block';
resultDiv.style.display = 'none';
try {
// 构建FormData
const formData = new FormData();
files.forEach(file => {
formData.append('files', file);
});
if (projectId) {
formData.append('project_id', projectId);
} else {
if (projectName) formData.append('project_name', projectName);
// 创建新项目时,这些字段是必填的
formData.append('project_description', projectDescription);
if (projectVersion) formData.append('project_version', projectVersion);
if (projectStatus) formData.append('project_status', projectStatus);
if (projectCategory) formData.append('project_category', projectCategory);
if (projectTags) formData.append('project_tags', projectTags);
if (projectSource) formData.append('project_source', projectSource);
if (projectSourceUrl) formData.append('project_source_url', projectSourceUrl);
formData.append('project_evaluation_purpose', projectEvaluationPurpose);
// datetime-local 格式转换为 ISO 8601 格式
const deadlineFormatted = projectDeadline.includes('T')
? projectDeadline.substring(0, 16)
: projectDeadline.replace(' ', 'T').substring(0, 16);
formData.append('project_deadline', deadlineFormatted);
}
if (datasetPrefix) {
formData.append('dataset_name_prefix', datasetPrefix);
}
if (Object.keys(nameMapping).length > 0) {
formData.append('dataset_name_mapping', JSON.stringify(nameMapping));
}
// 调用API
const result = await importProject(formData);
// 隐藏进度,显示结果
progressDiv.style.display = 'none';
resultDiv.style.display = 'block';
const projectAction = result.created ? '创建' : '更新';
const successMsg = `导入完成!${projectAction}项目 "${result.project_name}",成功导入 ${result.successful_files}/${result.total_files} 个文件,共 ${result.total_imported} 条QA对,失败 ${result.total_failed} 条`;
let resultHtml = `${successMsg}
`;
if (result.file_results && result.file_results.length > 0) {
resultHtml += '文件导入详情
';
resultHtml += '';
resultHtml += result.file_results.map(r => {
if (r.success) {
return `
${escapeHtml(r.filename)}: 成功导入 ${r.imported_count} 条,失败 ${r.failed_count} 条
${r.errors && r.errors.length > 0 ? `
${r.errors.slice(0, 3).map(e => escapeHtml(e)).join('
')}
` : ''}
`;
} else {
return `
${escapeHtml(r.filename)}: 导入失败 - ${escapeHtml(r.error || '未知错误')}
`;
}
}).join('');
resultHtml += '
';
}
if (result.errors && result.errors.length > 0) {
resultHtml += `错误详情 (显示前${Math.min(result.errors.length, 10)}个)
`;
resultHtml += ``;
resultHtml += result.errors.slice(0, 10).map(err => `- ${escapeHtml(err)}
`).join('');
resultHtml += '
';
}
resultDiv.innerHTML = resultHtml;
// 刷新项目列表
if (result.project_id) {
setTimeout(() => {
loadProjects();
if (importMode === 'existing') {
// 如果导入到现有项目,刷新项目详情
if (currentProjectId === parseInt(result.project_id)) {
loadProjectDetail(result.project_id);
}
}
}, 1000);
}
// 3秒后自动关闭模态框
setTimeout(() => {
closeModal();
}, 3000);
} catch (error) {
progressDiv.style.display = 'none';
if (error.status === 401) {
clearToken();
const currentPath = window.location.pathname;
if (currentPath !== '/auth') {
const redirectUrl = encodeURIComponent(window.location.href);
window.location.href = `/auth?redirect=${redirectUrl}`;
}
}
showError('导入失败: ' + (error.message || '未知错误'));
}
}