/** * 管理页面 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 = `
JSON格式包含完整的标注结果和QA对信息,适合程序处理。
CSV格式是扁平化的表格数据,适合在Excel中查看和分析。
将导出项目下所有数据集的标注,每个数据集使用数据集名称命名。
`; 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.name)}

ID: ${project.id}
${project.status || 'active'}
${escapeHtml(project.description || t('common.description') || '-')}
${t('project.version')} ${escapeHtml(project.version || '-')}
${t('project.category')} ${escapeHtml(project.category || '-')}
${t('common.createdAt')} ${formatDateTime(project.created_at)}
📊
${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 = `
请填写到具体几点(例如:2024-12-31 18:00)
`; 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.name)}

ID: ${dataset.id}
${dataset.status || 'active'}
${escapeHtml(dataset.description || t('common.description') || '-')}
${t('common.loading')}
${t('project.version')} ${escapeHtml(dataset.version || '-')}
${t('project.annotator')} ${escapeHtml(dataset.annotator_name || '-')}
`).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 = `
可以选择多个.jsonl格式的文件,每个文件将作为一个数据集导入

数据集元数据(可选)

如果填写了以下字段,将优先使用这些值,而不是文件中的元数据
指定负责标注该数据集的用户(可选)
`; 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 += `
'; } 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 = `
可以选择多个.jsonl格式的文件,每个文件将作为一个数据集导入

导入模式

项目信息(创建新项目时必填)

请填写到具体几点(例如:2024-12-31 18:00)

数据集命名配置

数据集名称格式:{前缀}_{文件名},如果不填写,将使用项目名称作为前缀
`; 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 += `
'; } 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 || '未知错误')); } }