| |
| |
| |
| |
|
|
| |
| 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; |
|
|
| |
|
|
| |
| |
| |
| |
| function updateTemplateTranslations(container) { |
| if (!window.i18next) return; |
|
|
| |
| container.querySelectorAll('[data-i18n]').forEach(element => { |
| const key = element.getAttribute('data-i18n'); |
| const translation = window.i18next.t(key); |
|
|
| |
| 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; |
| } |
| }); |
|
|
| |
| container.querySelectorAll('[data-i18n-title]').forEach(element => { |
| const key = element.getAttribute('data-i18n-title'); |
| element.title = window.i18next.t(key); |
| }); |
|
|
| |
| container.querySelectorAll('[data-i18n-placeholder]').forEach(element => { |
| const key = element.getAttribute('data-i18n-placeholder'); |
| element.placeholder = window.i18next.t(key); |
| }); |
|
|
| |
| 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); |
| } |
| } |
| }); |
| }); |
| } |
|
|
| |
| document.addEventListener('DOMContentLoaded', () => { |
| initPaginators(); |
| initNavigation(); |
| initModals(); |
| initEventListeners(); |
| |
| 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; |
|
|
| |
| 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(); |
| } |
| |
| resetProjectsTabs(); |
| |
| if (!projectsObserver) { |
| initProjectsLazyLoad(); |
| } else { |
| |
| projectsLoadedCount = 0; |
| projectsHasMore = true; |
| const container = document.getElementById('projectsCardContainer'); |
| if (container) container.innerHTML = `<div class="loading" style="text-align: center; padding: 40px; color: #999;">${t('common.loading')}</div>`; |
| 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 { |
| |
| 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); |
| } |
|
|
| |
| await ensureStylesheetLoaded('/static/css/user-management.css'); |
|
|
| |
| 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(); |
| } |
| |
| 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 { |
| |
| 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); |
| } |
|
|
| |
| await ensureStylesheetLoaded('/static/css/dataset-management.css'); |
|
|
| |
| await ensureScriptLoaded('/static/js/dataset-management.js'); |
|
|
| |
| if (window.DatasetManagement) { |
| window.DatasetManagement.init(container); |
| } |
| } catch (error) { |
| console.error('加载数据库管理模块失败:', error); |
| showError('加载数据库管理模块失败: ' + (error.message || '未知错误')); |
| } |
| } |
|
|
| |
| async function ensureDatasetManagementLoaded() { |
| |
| if (window.DatasetManagement) { |
| return Promise.resolve(); |
| } |
|
|
| try { |
| |
| 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(); |
|
|
| |
| 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 { |
| |
| 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); |
| } |
|
|
| |
| await ensureStylesheetLoaded('/static/css/annotation-config-management.css'); |
|
|
| |
| 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 { |
| |
| 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); |
| } |
|
|
| |
| await ensureStylesheetLoaded('/static/css/seed-question-management.css'); |
|
|
| |
| 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 { |
| |
| 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); |
| } |
|
|
| |
| await ensureStylesheetLoaded('/static/css/system-config-management.css'); |
|
|
| |
| 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 = ` |
| <form id="exportProjectForm"> |
| <div class="form-group"> |
| <label>导出格式 *</label> |
| <select id="exportProjectFormat" required> |
| <option value="json">JSON格式(完整数据)</option> |
| <option value="csv">CSV格式(表格数据)</option> |
| </select> |
| <small style="color: #666; display: block; margin-top: 4px;"> |
| JSON格式包含完整的标注结果和QA对信息,适合程序处理。<br> |
| CSV格式是扁平化的表格数据,适合在Excel中查看和分析。<br> |
| 将导出项目下所有数据集的标注,每个数据集使用数据集名称命名。 |
| </small> |
| </div> |
| </form> |
| `; |
|
|
| openModal(title, content, async () => { |
| await handleExportProjectAnnotations(projectId); |
| }); |
| } |
|
|
| async function handleExportProjectAnnotations(projectId) { |
| const format = document.getElementById('exportProjectFormat').value; |
|
|
| if (!format) { |
| showError('请选择导出格式'); |
| return; |
| } |
|
|
| try { |
| |
| const token = getToken(); |
| const headers = {}; |
| if (token) { |
| headers['Authorization'] = `Bearer ${token}`; |
| } |
|
|
| |
| const exportUrl = `${API_BASE_URL}/projects/${projectId}/export-annotations?format=${format}`; |
|
|
| |
| 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}`); |
| } |
|
|
| |
| 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); |
| |
| 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 = ` |
| <div style="max-width: 800px; line-height: 1.6;"> |
| <div style="margin-bottom: 20px;"> |
| <h3 style="color: #333; border-bottom: 2px solid #2196F3; padding-bottom: 8px;"> |
| ${t('project.usageGuide')} |
| </h3> |
| <div style="background: #f5f5f5; padding: 15px; border-radius: 5px; margin-top: 10px;"> |
| <h4 style="margin-top: 0; color: #2196F3;">${t('project.usage.section1.title')}</h4> |
| <ul style="margin: 10px 0; padding-left: 20px;"> |
| <li>${t('project.usage.section1.item1')}</li> |
| <li>${t('project.usage.section1.item2')}</li> |
| <li>${t('project.usage.section1.item3')}</li> |
| <li>${t('project.usage.section1.item4')}</li> |
| <li>${t('project.usage.section1.item5')}</li> |
| <li>${t('project.usage.section1.item6')}</li> |
| </ul> |
| |
| <h4 style="margin-top: 15px; color: #2196F3;">${t('project.usage.section2.title')}</h4> |
| <ul style="margin: 10px 0; padding-left: 20px;"> |
| <li>${t('project.usage.section2.item1')}</li> |
| <li>${t('project.usage.section2.item2')}</li> |
| <li>${t('project.usage.section2.item3')}</li> |
| <li>${t('project.usage.section2.item4')}</li> |
| </ul> |
| |
| <h4 style="margin-top: 15px; color: #2196F3;">${t('project.usage.section3.title')}</h4> |
| <ul style="margin: 10px 0; padding-left: 20px;"> |
| <li>${t('project.usage.section3.item1')}</li> |
| <li>${t('project.usage.section3.item2')}</li> |
| <li>${t('project.usage.section3.item3')}</li> |
| <li>${t('project.usage.section3.item4')}</li> |
| </ul> |
| |
| <h4 style="margin-top: 15px; color: #2196F3;">${t('project.usage.section4.title')}</h4> |
| <ul style="margin: 10px 0; padding-left: 20px;"> |
| <li>可以将现有数据集添加到项目中</li> |
| <li>支持从JSONL文件导入数据集到项目,支持批量导入多个文件</li> |
| <li>数据集会自动继承项目的标注配置(如果数据集没有自己的配置)</li> |
| <li>可以查看和管理项目下的所有数据集,包括数据集的统计信息</li> |
| <li>支持从项目中移除数据集(移除后,数据集的project_id会被设为NULL)</li> |
| </ul> |
| |
| <h4 style="margin-top: 15px; color: #2196F3;">${t('project.usage.section5.title')}</h4> |
| <ul style="margin: 10px 0; padding-left: 20px;"> |
| <li>${t('project.usage.section5.item1')}</li> |
| <li>${t('project.usage.section5.item2')}</li> |
| <li>${t('project.usage.section5.item3')}</li> |
| <li>${t('project.usage.section5.item4')}</li> |
| <li>${t('project.usage.section5.item5')}</li> |
| </ul> |
| |
| <h4 style="margin-top: 15px; color: #2196F3;">${t('project.usage.section6.title')}</h4> |
| <ul style="margin: 10px 0; padding-left: 20px;"> |
| <li>${t('project.usage.section6.item1')}</li> |
| <li>${t('project.usage.section6.item2')}</li> |
| <li>${t('project.usage.section6.item3')}</li> |
| </ul> |
| |
| <h4 style="margin-top: 15px; color: #2196F3;">${t('project.usage.section7.title')}</h4> |
| <ul style="margin: 10px 0; padding-left: 20px;"> |
| <li>${t('project.usage.section7.item1')}</li> |
| <li>${t('project.usage.section7.item2')}</li> |
| <li>${t('project.usage.section7.item3')}</li> |
| <li>${t('project.usage.section7.item4')}</li> |
| <li>${t('project.usage.section7.item5')}</li> |
| </ul> |
| </div> |
| </div> |
| |
| <div style="margin-bottom: 20px;"> |
| <h3 style="color: #333; border-bottom: 2px solid #FF9800; padding-bottom: 8px;"> |
| ${t('project.usage.bestPractices.title')} |
| </h3> |
| <div style="background: #fff3cd; padding: 15px; border-radius: 5px; border-left: 4px solid #FF9800; margin-top: 10px;"> |
| <ul style="margin: 10px 0; padding-left: 20px;"> |
| <li><strong>${t('project.usage.bestPractices.configOrder')}:</strong>${t('project.usage.bestPractices.configOrderDesc')}</li> |
| <li><strong>${t('project.usage.bestPractices.inheritance')}:</strong>${t('project.usage.bestPractices.inheritanceDesc')}</li> |
| <li><strong>${t('project.usage.bestPractices.batchImport')}:</strong>${t('project.usage.bestPractices.batchImportDesc')}</li> |
| <li><strong>${t('project.usage.bestPractices.backup')}:</strong>${t('project.usage.bestPractices.backupDesc')}</li> |
| <li><strong>${t('project.usage.bestPractices.management')}:</strong>${t('project.usage.bestPractices.managementDesc')}</li> |
| </ul> |
| </div> |
| </div> |
| </div> |
| `; |
|
|
| |
| 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); |
|
|
| |
| 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); |
| } |
| } |
|
|
| |
| 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 = `<div class="loading" style="text-align: center; padding: 20px; color: #999;">${t('common.loading')}</div>`; |
|
|
| const projectsList = document.getElementById('projects-list'); |
| if (projectsList) { |
| projectsList.appendChild(loader); |
| } |
| } |
|
|
| |
| projectsObserver = new IntersectionObserver((entries) => { |
| entries.forEach(entry => { |
| if (entry.isIntersecting && projectsHasMore && !projectsLoading) { |
| loadProjects(false); |
| } |
| }); |
| }, { |
| root: null, |
| rootMargin: '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 = `<div class="loading" style="text-align: center; padding: 40px; color: #999;">${t('project.noProjects')}</div>`; |
| } |
| 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 => ` |
| <div class="project-card"> |
| <div class="project-card-header"> |
| <div class="project-card-title-section"> |
| <h3 class="project-card-title">${escapeHtml(project.name)}</h3> |
| <span class="project-card-id">ID: ${project.id}</span> |
| </div> |
| <span class="status-badge ${project.status === 'active' ? 'active' : 'inactive'}"> |
| ${project.status || 'active'} |
| </span> |
| </div> |
| <div class="project-card-body"> |
| <div class="project-card-description"> |
| ${escapeHtml(project.description || t('common.description') || '-')} |
| </div> |
| <div class="project-card-meta"> |
| <div class="project-card-meta-item"> |
| <span class="project-card-meta-label">${t('project.version')}</span> |
| <span class="project-card-meta-value">${escapeHtml(project.version || '-')}</span> |
| </div> |
| <div class="project-card-meta-item"> |
| <span class="project-card-meta-label">${t('project.category')}</span> |
| <span class="project-card-meta-value">${escapeHtml(project.category || '-')}</span> |
| </div> |
| <div class="project-card-meta-item"> |
| <span class="project-card-meta-label">${t('common.createdAt')}</span> |
| <span class="project-card-meta-value">${formatDateTime(project.created_at)}</span> |
| </div> |
| </div> |
| <div class="project-card-stats"> |
| <div class="project-card-stat-item"> |
| <div class="project-card-stat-icon">📊</div> |
| <div class="project-card-stat-content"> |
| <div class="project-card-stat-value">${project.datasets_count || 0}</div> |
| <div class="project-card-stat-label">${t('project.datasets')}</div> |
| </div> |
| </div> |
| <div class="project-card-stat-item"> |
| <div class="project-card-stat-icon">⚙️</div> |
| <div class="project-card-stat-content"> |
| <div class="project-card-stat-value">${project.configs_count || 0}</div> |
| <div class="project-card-stat-label">${t('project.configs')}</div> |
| </div> |
| </div> |
| </div> |
| </div> |
| <div class="project-card-footer"> |
| <button class="btn btn-sm btn-primary" onclick="viewProjectDetail(${project.id})">${t('project.viewDetail')}</button> |
| <button class="btn btn-sm btn-secondary" onclick="editProject(${project.id})">${t('actions.edit')}</button> |
| <button class="btn btn-sm btn-danger" onclick="deleteProjectHandler(${project.id})">${t('actions.delete')}</button> |
| </div> |
| </div> |
| `).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 = ` |
| <form id="projectForm"> |
| <div class="form-group"> |
| <label>${t('project.projectNameRequired')}</label> |
| <input type="text" id="projectName" value="${project ? escapeHtml(project.name) : ''}" |
| required minlength="1" maxlength="200"> |
| </div> |
| <div class="form-group"> |
| <label>${t('project.descriptionRequired')}</label> |
| <textarea id="projectDescription" rows="3" maxlength="1000" required>${project ? escapeHtml(project.description || '') : ''}</textarea> |
| </div> |
| <div class="form-group"> |
| <label>${t('project.version')}</label> |
| <input type="text" id="projectVersion" value="${project ? escapeHtml(project.version || '') : ''}" |
| maxlength="50"> |
| </div> |
| <div class="form-group"> |
| <label>${t('common.status')}</label> |
| <select id="projectStatus"> |
| <option value="active" ${project && project.status === 'active' ? 'selected' : ''}>${t('status.active')}</option> |
| <option value="inactive" ${project && project.status === 'inactive' ? 'selected' : ''}>${t('status.inactive')}</option> |
| <option value="archived" ${project && project.status === 'archived' ? 'selected' : ''}>${t('status.archived')}</option> |
| </select> |
| </div> |
| <div class="form-group"> |
| <label>${t('project.category')}</label> |
| <input type="text" id="projectCategory" value="${project ? escapeHtml(project.category || '') : ''}" |
| maxlength="100"> |
| </div> |
| <div class="form-group"> |
| <label>${t('project.tagsComma')}</label> |
| <input type="text" id="projectTags" value="${project && project.tags ? project.tags.join(', ') : ''}" |
| placeholder="${t('project.tagsPlaceholder')}"> |
| </div> |
| <div class="form-group"> |
| <label>数据来源</label> |
| <input type="text" id="projectSource" value="${project ? escapeHtml(project.source || '') : ''}" |
| maxlength="200"> |
| </div> |
| <div class="form-group"> |
| <label>数据来源URL</label> |
| <input type="url" id="projectSourceUrl" value="${project ? escapeHtml(project.source_url || '') : ''}" |
| maxlength="500"> |
| </div> |
| <div class="form-group"> |
| <label>要显示的extra字段(用逗号分隔)</label> |
| <input type="text" id="projectDisplayExtraFields" |
| value="${project && project.display_extra_fields ? project.display_extra_fields.join(', ') : ''}" |
| placeholder="例如: field1, field2"> |
| </div> |
| <div class="form-group"> |
| <label>评估目的 <span style="color: red;">*</span></label> |
| <textarea id="projectEvaluationPurpose" rows="2" maxlength="500" required |
| placeholder="请输入项目的评估目的">${project && project.metadata && project.metadata.evaluation_purpose ? escapeHtml(project.metadata.evaluation_purpose) : ''}</textarea> |
| </div> |
| <div class="form-group"> |
| <label>完成时间 <span style="color: red;">*</span></label> |
| <input type="datetime-local" id="projectDeadline" required |
| value="${project && project.metadata && project.metadata.deadline ? (() => { |
| const deadline = project.metadata.deadline; |
| // 将 ISO 8601 格式 (YYYY-MM-DDTHH:mm) 转换为 datetime-local 格式 |
| // datetime-local 需要 YYYY-MM-DDTHH:mm 格式,但可能需要处理时区 |
| if (deadline.includes('T')) { |
| return deadline.substring(0, 16); // 取前16个字符 (YYYY-MM-DDTHH:mm) |
| } else if (deadline.includes(' ')) { |
| return deadline.replace(' ', 'T').substring(0, 16); |
| } |
| return deadline; |
| })() : ''}"> |
| <small style="color: #666; display: block; margin-top: 4px;"> |
| 请填写到具体几点(例如:2024-12-31 18:00) |
| </small> |
| </div> |
| </form> |
| `; |
|
|
| 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); |
| } |
|
|
| |
| if (displayExtraFieldsStr) { |
| data.display_extra_fields = displayExtraFieldsStr.split(',').map(f => f.trim()).filter(f => f); |
| } |
|
|
| |
| const metadata = {}; |
| metadata.evaluation_purpose = evaluationPurpose; |
| |
| |
| if (deadline.includes('T')) { |
| metadata.deadline = deadline.substring(0, 16); |
| } 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 = '<div class="loading" style="text-align: center; padding: 40px; color: #999;">加载中...</div>'; |
| 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 = ` |
| <div style="margin-top: 12px; padding: 12px; background: #f5f5f5; border-radius: 4px;"> |
| <div style="font-weight: bold; margin-bottom: 8px;">${t('project.projectContainsDatasets')}:</div> |
| <div style="color: #666; font-size: 13px;">${datasetsList}${moreText}</div> |
| </div> |
| `; |
| } |
|
|
| const content = ` |
| <div style="margin-bottom: 16px;"> |
| <p>${t('project.selectAction')}:</p> |
| <p style="color: #666; font-size: 14px; margin-top: 8px;"> |
| ${t('project.projectLabel')}: <strong>${escapeHtml(projectName)}</strong> (ID: ${projectId}) |
| </p> |
| ${datasetsInfo} |
| </div> |
| <form id="deleteProjectForm"> |
| <div class="form-group"> |
| <label style="display: flex; align-items: center; gap: 8px; cursor: pointer; padding: 12px; border: 2px solid #2196F3; border-radius: 4px; margin-bottom: 12px;"> |
| <input type="radio" name="deleteAction" value="remove" checked style="margin: 0; width: auto; height: auto;"> |
| <div style="flex: 1;"> |
| <div style="font-weight: bold; margin-bottom: 4px;">${t('project.deleteProjectOnly')}</div> |
| <div style="color: #666; font-size: 12px;">${t('project.deleteProjectOnlyDesc')}</div> |
| </div> |
| </label> |
| <label style="display: flex; align-items: center; gap: 8px; cursor: pointer; padding: 12px; border: 2px solid #d32f2f; border-radius: 4px;"> |
| <input type="radio" name="deleteAction" value="delete" style="margin: 0; width: auto; height: auto;"> |
| <div style="flex: 1;"> |
| <div style="font-weight: bold; margin-bottom: 4px; color: #d32f2f;">${t('project.deleteProjectAndDatasets')}</div> |
| <div style="color: #666; font-size: 12px;">${t('project.deleteProjectAndDatasetsDesc', { count: datasetsCount })}</div> |
| </div> |
| </label> |
| </div> |
| </form> |
| `; |
|
|
| 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 = `<div class="loading" style="text-align: center; padding: 40px; color: #999;">${t('common.loading')}</div>`; |
| 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; |
|
|
| |
| 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'); |
|
|
| |
| 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); |
| }); |
|
|
| |
| if (tab === 'project-analysis' && currentProjectId) { |
| loadAnnotationAnalysis(currentProjectId); |
| } |
| } |
|
|
| |
| async function loadAnnotationAnalysis(projectId) { |
| try { |
| |
| await ensureStylesheetLoaded('/static/css/annotation-analysis.css'); |
|
|
| |
| await ensureScriptLoaded('/static/js/annotation-analysis.js'); |
|
|
| |
| await new Promise(resolve => setTimeout(resolve, 100)); |
|
|
| |
| if (typeof Chart === 'undefined') { |
| console.error('Chart.js 未加载!请检查 manager.html 中的 Chart.js 引入'); |
| const container = document.getElementById('analysisConfigsContainer'); |
| if (container) { |
| container.innerHTML = `<div style="color: red; text-align: center; padding: 40px;">${t('project.chartLibLoadFailed')}</div>`; |
| } |
| return; |
| } |
|
|
| |
| if (window.AnnotationAnalysis) { |
| window.AnnotationAnalysis.init(projectId); |
| } else { |
| console.error('AnnotationAnalysis未定义!'); |
| |
| const container = document.getElementById('analysisConfigsContainer'); |
| if (container) { |
| container.innerHTML = '<div style="color: red; text-align: center; padding: 40px;">加载分析模块失败,请刷新页面重试</div>'; |
| } |
| } |
| } catch (error) { |
| console.error('加载标注结果分析时出错:', error); |
| const container = document.getElementById('analysisConfigsContainer'); |
| if (container) { |
| container.innerHTML = `<div style="color: red; text-align: center; padding: 40px;">${t('project.loadFailed')}: ${error.message}</div>`; |
| } |
| } |
| } |
|
|
| |
| 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 = `<div class="loading" style="text-align: center; padding: 40px; color: #999; grid-column: 1 / -1;">${t('dataset.noDatasets')}</div>`; |
| } |
| 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 = `<div class="loading" style="text-align: center; padding: 40px; color: #999; grid-column: 1 / -1;">${t('dataset.noDatasets')}</div>`; |
| return; |
| } |
|
|
| const cardsHTML = datasets.map(dataset => ` |
| <div class="project-card" data-dataset-id="${dataset.id}"> |
| <div class="project-card-header"> |
| <div class="project-card-title-section"> |
| <h3 class="project-card-title">${escapeHtml(dataset.name)}</h3> |
| <span class="project-card-id">ID: ${dataset.id}</span> |
| </div> |
| <span class="status-badge ${dataset.status === 'active' ? 'active' : 'inactive'}"> |
| ${dataset.status || 'active'} |
| </span> |
| </div> |
| <div class="project-card-body"> |
| <div class="project-card-description"> |
| ${escapeHtml(dataset.description || t('common.description') || '-')} |
| </div> |
| <div class="project-card-annotation-progress" style="margin-top: 12px; padding-top: 12px; border-top: 1px solid #e0e0e0;"> |
| <div class="loading" style="text-align: center; padding: 8px; color: #999; font-size: 12px;">${t('common.loading')}</div> |
| </div> |
| <div class="project-card-meta"> |
| <div class="project-card-meta-item"> |
| <span class="project-card-meta-label">${t('project.version')}</span> |
| <span class="project-card-meta-value">${escapeHtml(dataset.version || '-')}</span> |
| </div> |
| <div class="project-card-meta-item"> |
| <span class="project-card-meta-label">${t('project.annotator')}</span> |
| <span class="project-card-meta-value">${escapeHtml(dataset.annotator_name || '-')}</span> |
| </div> |
| </div> |
| </div> |
| <div class="project-card-footer"> |
| <button class="btn btn-sm btn-success" onclick="window.location.href='/annotation?dataset_id=${dataset.id}'">${t('dataset.annotate')}</button> |
| <button class="btn btn-sm btn-primary" onclick="editDatasetFromProject(${dataset.id})">${t('actions.edit')}</button> |
| <button class="btn btn-sm btn-danger" onclick="removeDatasetFromProjectHandler(${currentProjectId}, ${dataset.id})">${t('actions.remove')}</button> |
| </div> |
| </div> |
| `).join(''); |
|
|
| if (reset) { |
| container.innerHTML = cardsHTML; |
| } else { |
| container.insertAdjacentHTML('beforeend', cardsHTML); |
| } |
|
|
| |
| datasets.forEach(async dataset => { |
| try { |
| |
| 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) |
| : (() => { |
| |
| if (!progress || progress.total_items === 0) { |
| return `<span style="color: #999;">${t('common.noData')}</span>`; |
| } |
| const overallRate = progress.overall_progress_rate || 0; |
| const progressColor = overallRate >= 80 ? '#2e7d32' : overallRate >= 50 ? '#f57c00' : '#d32f2f'; |
|
|
| let html = ` |
| <div style="display: flex; flex-direction: column; gap: 4px;"> |
| <div style="display: flex; align-items: center; gap: 8px;"> |
| <div style="flex: 1; height: 8px; background: #e0e0e0; border-radius: 4px; overflow: hidden;"> |
| <div style="height: 100%; width: ${overallRate}%; background: ${progressColor}; transition: width 0.3s;"></div> |
| </div> |
| <span style="font-size: 12px; color: ${progressColor}; font-weight: bold; min-width: 50px;"> |
| ${overallRate.toFixed(1)}% |
| </span> |
| </div> |
| <div style="font-size: 11px; color: #666;"> |
| ${progress.annotated_items || 0} / ${progress.total_items} |
| </div> |
| `; |
|
|
| |
| 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 ` |
| <div style="display: flex; align-items: center; gap: 8px; margin-top: 4px;"> |
| <span style="font-size: 11px; color: #666; min-width: 100px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" title="${cp.config_name}"> |
| ${cp.config_name}: |
| </span> |
| <div style="flex: 1; height: 6px; background: #e0e0e0; border-radius: 3px; overflow: hidden;"> |
| <div style="height: 100%; width: ${cp.progress_rate}%; background: ${cpColor};"></div> |
| </div> |
| <span style="font-size: 11px; color: ${cpColor}; min-width: 45px;"> |
| ${cp.progress_rate.toFixed(1)}% |
| </span> |
| </div> |
| `; |
| }).join(''); |
|
|
| html += ` |
| <details style="margin-top: 4px;" open> |
| <summary style="cursor: pointer; font-size: 11px; color: #1976d2; user-select: none;"> |
| ${t('common.viewDetails')} |
| </summary> |
| <div style="margin-top: 8px; padding: 8px; background: #f5f5f5; border-radius: 4px;"> |
| ${configDetails} |
| </div> |
| </details> |
| `; |
| } |
|
|
| html += '</div>'; |
| return html; |
| })(); |
| progressContainer.innerHTML = progressHtml; |
| } else { |
| progressContainer.innerHTML = `<div style="text-align: center; padding: 8px; color: #999; font-size: 12px;">${t('common.noData')}</div>`; |
| } |
| } |
| } |
| } 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 = `<div style="text-align: center; padding: 8px; color: #999; font-size: 12px;">${t('common.loadFailed')}</div>`; |
| } |
| } |
| } |
| }); |
| } |
|
|
| function initProjectDatasetsLazyLoad(projectId) { |
| |
| if (projectDatasetsObserver) { |
| const loader = getDetailElement('projectDatasetsLoader'); |
| if (loader) { |
| projectDatasetsObserver.unobserve(loader); |
| } |
| } |
|
|
| |
| 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 = '<div class="loading">加载中...</div>'; |
|
|
| const detailContainer = document.getElementById('project-detail-container'); |
| const datasetsTab = detailContainer?.querySelector('#project-datasets'); |
| if (datasetsTab) { |
| datasetsTab.appendChild(loader); |
| } |
| } |
|
|
| |
| projectDatasetsObserver = new IntersectionObserver((entries) => { |
| entries.forEach(entry => { |
| if (entry.isIntersecting && projectDatasetsHasMore && !projectDatasetsLoading) { |
| loadProjectDatasets(projectId, false); |
| } |
| }); |
| }, { |
| root: null, |
| rootMargin: '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 = ` |
| <form id="addDatasetToProjectForm"> |
| <div class="form-group"> |
| <label>选择数据集 *</label> |
| <select id="datasetSelectForProject" required> |
| <option value="">请选择数据集...</option> |
| ${availableDatasets.map(d => ` |
| <option value="${d.id}">${escapeHtml(d.name)} (ID: ${d.id})</option> |
| `).join('')} |
| </select> |
| </div> |
| </form> |
| `; |
|
|
| 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 = ` |
| <div style="margin-bottom: 16px;"> |
| <p>请选择操作方式:</p> |
| <p style="color: #666; font-size: 14px; margin-top: 8px;"> |
| 数据集:<strong>${escapeHtml(datasetName)}</strong> (ID: ${datasetId}) |
| </p> |
| </div> |
| <form id="removeDatasetForm"> |
| <div class="form-group"> |
| <label style="display: flex; align-items: center; gap: 8px; cursor: pointer; padding: 12px; border: 2px solid #2196F3; border-radius: 4px; margin-bottom: 12px;"> |
| <input type="radio" name="removeAction" value="remove" checked style="margin: 0; width: auto; height: auto;"> |
| <div style="flex: 1;"> |
| <div style="font-weight: bold; margin-bottom: 4px;">仅移出项目</div> |
| <div style="color: #666; font-size: 12px;">数据集将从项目中移除,但不会被删除,仍可在数据集管理中查看</div> |
| </div> |
| </label> |
| <label style="display: flex; align-items: center; gap: 8px; cursor: pointer; padding: 12px; border: 2px solid #d32f2f; border-radius: 4px;"> |
| <input type="radio" name="removeAction" value="delete" style="margin: 0; width: auto; height: auto;"> |
| <div style="flex: 1;"> |
| <div style="font-weight: bold; margin-bottom: 4px; color: #d32f2f;">删除数据集</div> |
| <div style="color: #666; font-size: 12px;">数据集将被永久删除,包括所有关联的QA对,此操作不可恢复</div> |
| </div> |
| </label> |
| </div> |
| </form> |
| `; |
|
|
| 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 = '<option value="">无</option>' + users.map(user => |
| `<option value="${user.id}">${escapeHtml(user.username)}${user.full_name ? ' (' + escapeHtml(user.full_name) + ')' : ''}</option>` |
| ).join(''); |
|
|
| const title = '导入数据集到项目'; |
| const content = ` |
| <form id="importDatasetToProjectForm"> |
| <div class="form-group"> |
| <label>选择JSONL文件 *</label> |
| <input type="file" id="importDatasetToProjectFile" accept=".jsonl" multiple required> |
| <small style="color: #666; display: block; margin-top: 4px;"> |
| 可以选择多个.jsonl格式的文件,每个文件将作为一个数据集导入 |
| </small> |
| <div id="importDatasetToProjectFilesList" style="margin-top: 12px; display: none;"> |
| <h4 style="margin: 0 0 8px 0; font-size: 14px; font-weight: bold;">已选择的文件:</h4> |
| <div id="importDatasetToProjectFilesPreview" style="max-height: 200px; overflow-y: auto; border: 1px solid #e0e0e0; padding: 8px; border-radius: 4px;"></div> |
| </div> |
| </div> |
| <div style="border-top: 1px solid #e0e0e0; margin: 16px 0; padding-top: 16px;"> |
| <h4 style="margin: 0 0 12px 0; font-size: 14px; font-weight: bold;">数据集元数据(可选)</h4> |
| <small style="color: #666; display: block; margin-bottom: 12px;"> |
| 如果填写了以下字段,将优先使用这些值,而不是文件中的元数据 |
| </small> |
| <div class="form-group"> |
| <label>数据集名称 *</label> |
| <input type="text" id="importDatasetToProjectName" placeholder="如果不填写,将使用文件名"> |
| </div> |
| <div class="form-group"> |
| <label>描述</label> |
| <textarea id="importDatasetToProjectDescription" rows="2" placeholder="数据集描述"></textarea> |
| </div> |
| <div class="form-group"> |
| <label>版本</label> |
| <input type="text" id="importDatasetToProjectVersion" placeholder="例如: 1.0"> |
| </div> |
| <div class="form-group"> |
| <label>分类</label> |
| <input type="text" id="importDatasetToProjectCategory" placeholder="数据集分类"> |
| </div> |
| <div class="form-group"> |
| <label>状态</label> |
| <select id="importDatasetToProjectStatus"> |
| <option value="active">激活</option> |
| <option value="inactive">禁用</option> |
| <option value="archived">归档</option> |
| </select> |
| </div> |
| <div class="form-group"> |
| <label>标签(逗号分隔)</label> |
| <input type="text" id="importDatasetToProjectTags" placeholder="例如: 标签1,标签2,标签3"> |
| </div> |
| <div class="form-group"> |
| <label>数据来源</label> |
| <input type="text" id="importDatasetToProjectSource" placeholder="数据来源"> |
| </div> |
| <div class="form-group"> |
| <label>来源URL</label> |
| <input type="url" id="importDatasetToProjectSourceUrl" placeholder="https://example.com"> |
| </div> |
| <div class="form-group"> |
| <label>标注者</label> |
| <select id="importDatasetToProjectAnnotatorId"> |
| ${userOptions} |
| </select> |
| <small style="color: #666; display: block; margin-top: 4px;"> |
| 指定负责标注该数据集的用户(可选) |
| </small> |
| </div> |
| </div> |
| <div class="form-group"> |
| <div id="importDatasetToProjectProgress" style="display: none;"> |
| <p style="color: #666;">正在导入,请稍候...</p> |
| </div> |
| <div id="importDatasetToProjectResult" style="display: none;"></div> |
| </div> |
| </form> |
| `; |
|
|
| 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) => ` |
| <div style="padding: 4px 0; border-bottom: 1px solid #f0f0f0;"> |
| <span style="font-weight: bold;">${index + 1}.</span> ${escapeHtml(file.name)} |
| </div> |
| `).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 { |
| |
| 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); |
|
|
| |
| 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 = `<p style="color: #2e7d32; font-weight: bold;">${successMsg}</p>`; |
|
|
| if (result.file_results && result.file_results.length > 0) { |
| resultHtml += '<details style="margin-top: 12px;"><summary style="cursor: pointer; font-weight: bold;">文件导入详情</summary>'; |
| resultHtml += '<div style="margin-top: 8px; max-height: 300px; overflow-y: auto;">'; |
| resultHtml += result.file_results.map(r => { |
| if (r.success) { |
| return `<div style="padding: 4px 0; border-bottom: 1px solid #f0f0f0;"> |
| <strong>${escapeHtml(r.filename)}</strong>: 成功导入 ${r.imported_count} 条,失败 ${r.failed_count} 条 |
| ${r.errors && r.errors.length > 0 ? `<div style="color: #d32f2f; font-size: 12px; margin-left: 20px;">${r.errors.slice(0, 3).map(e => escapeHtml(e)).join('<br>')}</div>` : ''} |
| </div>`; |
| } else { |
| return `<div style="padding: 4px 0; border-bottom: 1px solid #f0f0f0; color: #d32f2f;"> |
| <strong>${escapeHtml(r.filename)}</strong>: 导入失败 - ${escapeHtml(r.error || '未知错误')} |
| </div>`; |
| } |
| }).join(''); |
| resultHtml += '</div></details>'; |
| } |
|
|
| if (result.errors && result.errors.length > 0) { |
| resultHtml += `<details style="margin-top: 12px;"><summary style="cursor: pointer; color: #d32f2f; font-weight: bold;">错误详情 (显示前${Math.min(result.errors.length, 10)}个)</summary>`; |
| resultHtml += `<ul style="margin-top: 8px; padding-left: 20px; max-height: 200px; overflow-y: auto;">`; |
| resultHtml += result.errors.slice(0, 10).map(err => `<li style="color: #d32f2f; margin: 4px 0;">${escapeHtml(err)}</li>`).join(''); |
| resultHtml += '</ul></details>'; |
| } |
|
|
| 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); |
| } |
|
|
| |
| 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 || '未知错误')); |
| } |
| } |
|
|
| 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 = `<tr><td colspan="7" class="loading">${t('project.noConfigs')}</td></tr>`; |
| return; |
| } |
|
|
| tbody.innerHTML = configs.map((config, index) => ` |
| <tr data-config-id="${config.id}"> |
| <td style="text-align: center;"> |
| <div style="display: flex; flex-direction: column; gap: 4px; align-items: center;"> |
| <button |
| class="btn btn-sm btn-secondary" |
| onclick="moveConfigOrder(${currentProjectId}, ${config.id}, 'up')" |
| ${index === 0 ? 'disabled' : ''} |
| style="padding: 2px 6px; font-size: 10px; min-width: auto;" |
| title="${t('actions.moveUp')}" |
| >↑</button> |
| <span style="font-size: 12px; color: #666; min-width: 20px; text-align: center;">${index + 1}</span> |
| <button |
| class="btn btn-sm btn-secondary" |
| onclick="moveConfigOrder(${currentProjectId}, ${config.id}, 'down')" |
| ${index === configs.length - 1 ? 'disabled' : ''} |
| style="padding: 2px 6px; font-size: 10px; min-width: auto;" |
| title="${t('actions.moveDown')}" |
| >↓</button> |
| </div> |
| </td> |
| <td>${config.id}</td> |
| <td>${escapeHtml(config.name)}</td> |
| <td>${escapeHtml(config.annotation_type || config.type || '-')}</td> |
| <td>${escapeHtml(config.description || '-')}</td> |
| <td><span class="status-badge ${config.required ? 'active' : 'inactive'}"> |
| ${config.required ? t('common.yes') : t('common.no')} |
| </span></td> |
| <td> |
| <button class="btn btn-sm btn-danger" onclick="removeConfigFromProjectHandler(${currentProjectId}, ${config.id})">${t('actions.remove')}</button> |
| </td> |
| </tr> |
| `).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 = ` |
| <form id="addConfigToProjectForm"> |
| <div class="form-group"> |
| <label>${t('project.selectConfig')} *</label> |
| <select id="configSelectForProject" required> |
| <option value="">${t('project.selectConfigPlaceholder')}...</option> |
| ${availableConfigs.map(c => ` |
| <option value="${c.id}">${escapeHtml(c.name)} (ID: ${c.id}, ${t('config.configType')}: ${c.annotation_type || c.type || '-'})</option> |
| `).join('')} |
| </select> |
| </div> |
| </form> |
| `; |
|
|
| 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 = ` |
| <form id="importProjectForm"> |
| <div class="form-group"> |
| <label>选择JSONL文件 *</label> |
| <input type="file" id="importProjectFiles" accept=".jsonl" multiple required> |
| <small style="color: #666; display: block; margin-top: 4px;"> |
| 可以选择多个.jsonl格式的文件,每个文件将作为一个数据集导入 |
| </small> |
| <div id="importProjectFilesList" style="margin-top: 12px; display: none;"> |
| <h4 style="margin: 0 0 8px 0; font-size: 14px; font-weight: bold;">已选择的文件:</h4> |
| <div id="importProjectFilesPreview" style="max-height: 200px; overflow-y: auto; border: 1px solid #e0e0e0; padding: 8px; border-radius: 4px;"></div> |
| </div> |
| </div> |
| |
| <div style="border-top: 1px solid #e0e0e0; margin: 16px 0; padding-top: 16px;"> |
| <h4 style="margin: 0 0 12px 0; font-size: 14px; font-weight: bold;">导入模式</h4> |
| <div class="form-group" style="display: flex; gap: 24px; align-items: center; flex-wrap: nowrap;"> |
| <label style="display: inline-flex; align-items: center; gap: 6px; cursor: pointer; margin: 0; white-space: nowrap; flex-shrink: 0;"> |
| <input type="radio" name="importMode" value="new" checked style="margin: 0; width: auto; height: auto; flex-shrink: 0;"> |
| <span>创建新项目</span> |
| </label> |
| <label style="display: inline-flex; align-items: center; gap: 6px; cursor: pointer; margin: 0; white-space: nowrap; flex-shrink: 0;"> |
| <input type="radio" name="importMode" value="existing" style="margin: 0; width: auto; height: auto; flex-shrink: 0;"> |
| <span>导入到现有项目</span> |
| </label> |
| </div> |
| <div id="existingProjectSelect" style="display: none; margin-top: 12px;"> |
| <label>选择项目 *</label> |
| <select id="importProjectExistingId" class="form-control"> |
| <option value="">请选择项目...</option> |
| </select> |
| </div> |
| </div> |
| |
| <div id="newProjectInfoSection" style="border-top: 1px solid #e0e0e0; margin: 16px 0; padding-top: 16px;"> |
| <h4 style="margin: 0 0 12px 0; font-size: 14px; font-weight: bold;">项目信息(创建新项目时必填)</h4> |
| <div class="form-group"> |
| <label>项目名称 *</label> |
| <input type="text" id="importProjectName" placeholder="项目名称" required> |
| </div> |
| <div class="form-group"> |
| <label>描述 <span style="color: red;">*</span></label> |
| <textarea id="importProjectDescription" rows="2" placeholder="项目描述" required></textarea> |
| </div> |
| <div class="form-group"> |
| <label>版本</label> |
| <input type="text" id="importProjectVersion" placeholder="例如: 1.0"> |
| </div> |
| <div class="form-group"> |
| <label>状态</label> |
| <select id="importProjectStatus"> |
| <option value="active">激活</option> |
| <option value="inactive">禁用</option> |
| <option value="archived">归档</option> |
| </select> |
| </div> |
| <div class="form-group"> |
| <label>分类</label> |
| <input type="text" id="importProjectCategory" placeholder="项目分类"> |
| </div> |
| <div class="form-group"> |
| <label>标签(逗号分隔)</label> |
| <input type="text" id="importProjectTags" placeholder="例如: 标签1,标签2,标签3"> |
| </div> |
| <div class="form-group"> |
| <label>数据来源</label> |
| <input type="text" id="importProjectSource" placeholder="数据来源"> |
| </div> |
| <div class="form-group"> |
| <label>来源URL</label> |
| <input type="url" id="importProjectSourceUrl" placeholder="https://example.com"> |
| </div> |
| <div class="form-group"> |
| <label>评估目的 <span style="color: red;">*</span></label> |
| <textarea id="importProjectEvaluationPurpose" rows="2" placeholder="请输入项目的评估目的" required></textarea> |
| </div> |
| <div class="form-group"> |
| <label>完成时间 <span style="color: red;">*</span></label> |
| <input type="datetime-local" id="importProjectDeadline" required> |
| <small style="color: #666; display: block; margin-top: 4px;"> |
| 请填写到具体几点(例如:2024-12-31 18:00) |
| </small> |
| </div> |
| </div> |
| |
| <div style="border-top: 1px solid #e0e0e0; margin: 16px 0; padding-top: 16px;"> |
| <h4 style="margin: 0 0 12px 0; font-size: 14px; font-weight: bold;">数据集命名配置</h4> |
| <div class="form-group"> |
| <label>数据集名称前缀</label> |
| <input type="text" id="importProjectDatasetPrefix" placeholder="默认为项目名称"> |
| <small style="color: #666; display: block; margin-top: 4px;"> |
| 数据集名称格式:{前缀}_{文件名},如果不填写,将使用项目名称作为前缀 |
| </small> |
| </div> |
| <div id="importProjectDatasetNames" style="margin-top: 12px; display: none;"> |
| <h5 style="margin: 0 0 8px 0; font-size: 13px; font-weight: bold;">数据集名称预览(可编辑):</h5> |
| <div id="importProjectDatasetNamesList" style="max-height: 200px; overflow-y: auto; border: 1px solid #e0e0e0; padding: 8px; border-radius: 4px;"></div> |
| </div> |
| </div> |
| |
| <div class="form-group"> |
| <div id="importProjectProgress" style="display: none;"> |
| <p style="color: #666;">正在导入,请稍候...</p> |
| </div> |
| <div id="importProjectResult" style="display: none;"></div> |
| </div> |
| </form> |
| `; |
|
|
| 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) => ` |
| <div style="padding: 4px 0; border-bottom: 1px solid #f0f0f0;"> |
| <span style="font-weight: bold;">${index + 1}.</span> ${escapeHtml(file.name)} |
| </div> |
| `).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 ` |
| <div style="padding: 8px; border-bottom: 1px solid #f0f0f0; display: flex; align-items: center; gap: 8px;"> |
| <span style="flex: 0 0 120px; font-size: 12px; color: #666;">${escapeHtml(file.name)}:</span> |
| <input |
| type="text" |
| class="form-control" |
| data-file-index="${index}" |
| data-filename="${escapeHtml(file.name)}" |
| value="${escapeHtml(defaultName)}" |
| style="flex: 1;" |
| placeholder="数据集名称" |
| > |
| </div> |
| `; |
| }).join(''); |
| } |
|
|
| async function loadProjectsForSelect() { |
| try { |
| const projects = await apiGet('/projects?limit=1000'); |
| existingProjectIdSelect.innerHTML = '<option value="">请选择项目...</option>' + |
| projects.map(p => `<option value="${p.id}">${escapeHtml(p.name)}</option>`).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 { |
| |
| 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); |
| |
| 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)); |
| } |
|
|
| |
| 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 = `<p style="color: #2e7d32; font-weight: bold;">${successMsg}</p>`; |
|
|
| if (result.file_results && result.file_results.length > 0) { |
| resultHtml += '<details style="margin-top: 12px;"><summary style="cursor: pointer; font-weight: bold;">文件导入详情</summary>'; |
| resultHtml += '<div style="margin-top: 8px; max-height: 300px; overflow-y: auto;">'; |
| resultHtml += result.file_results.map(r => { |
| if (r.success) { |
| return `<div style="padding: 4px 0; border-bottom: 1px solid #f0f0f0;"> |
| <strong>${escapeHtml(r.filename)}</strong>: 成功导入 ${r.imported_count} 条,失败 ${r.failed_count} 条 |
| ${r.errors && r.errors.length > 0 ? `<div style="color: #d32f2f; font-size: 12px; margin-left: 20px;">${r.errors.slice(0, 3).map(e => escapeHtml(e)).join('<br>')}</div>` : ''} |
| </div>`; |
| } else { |
| return `<div style="padding: 4px 0; border-bottom: 1px solid #f0f0f0; color: #d32f2f;"> |
| <strong>${escapeHtml(r.filename)}</strong>: 导入失败 - ${escapeHtml(r.error || '未知错误')} |
| </div>`; |
| } |
| }).join(''); |
| resultHtml += '</div></details>'; |
| } |
|
|
| if (result.errors && result.errors.length > 0) { |
| resultHtml += `<details style="margin-top: 12px;"><summary style="cursor: pointer; color: #d32f2f; font-weight: bold;">错误详情 (显示前${Math.min(result.errors.length, 10)}个)</summary>`; |
| resultHtml += `<ul style="margin-top: 8px; padding-left: 20px; max-height: 200px; overflow-y: auto;">`; |
| resultHtml += result.errors.slice(0, 10).map(err => `<li style="color: #d32f2f; margin: 4px 0;">${escapeHtml(err)}</li>`).join(''); |
| resultHtml += '</ul></details>'; |
| } |
|
|
| resultDiv.innerHTML = resultHtml; |
|
|
| |
| if (result.project_id) { |
| setTimeout(() => { |
| loadProjects(); |
| if (importMode === 'existing') { |
| |
| if (currentProjectId === parseInt(result.project_id)) { |
| loadProjectDetail(result.project_id); |
| } |
| } |
| }, 1000); |
| } |
|
|
| |
| 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 || '未知错误')); |
| } |
| } |
|
|