// ==UserScript== // @name 数据导出增强版 // @namespace https://docs.scriptcat.org/ // @version 0.3.0 // @description 支持任务备份导出和全量采集题ID/序列名称,可自动翻页采集全部数据 // @author You // @match *://*/*project* // @grant none // @noframes // ==/UserScript== (function() { document.querySelectorAll('#ultimate-scraper-panel').forEach(panel => panel.remove()); // ================= 1. 本地存储 ================= const STORAGE_KEY = 'usc_projects_v3'; function saveToStorage() { try { const data = { projects, currentProjectId, mappingData }; localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); } catch(e) {} } function loadFromStorage() { try { const raw = localStorage.getItem(STORAGE_KEY); if (raw) { const data = JSON.parse(raw); projects = data.projects || []; currentProjectId = data.currentProjectId || (projects.length > 0 ? projects[0].id : null); mappingData = data.mappingData || []; } } catch(e) {} } // ================= 2. 网络 & DOM 拦截 ================= let latestDownloadFilename = ''; const originalOpen = XMLHttpRequest.prototype.open; const originalSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function(...args) { this._url = args[1]; return originalOpen.apply(this, args); }; XMLHttpRequest.prototype.send = function(...args) { this.addEventListener('load', function() { try { const disposition = this.getResponseHeader('Content-Disposition'); if (disposition && disposition.indexOf('attachment') !== -1) { const filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/; const matches = filenameRegex.exec(disposition); if (matches != null && matches[1]) { let filename = matches[1].replace(/['"]/g, ''); if (filename.startsWith("UTF-8''")) { filename = decodeURIComponent(filename.replace("UTF-8''", '')); } latestDownloadFilename = filename; } } } catch (e) {} }); return originalSend.apply(this, args); }; const originalFetch = window.fetch; window.fetch = async function(...args) { const response = await originalFetch.apply(this, args); try { const disposition = response.headers.get('Content-Disposition'); if (disposition && disposition.indexOf('attachment') !== -1) { const filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/; const matches = filenameRegex.exec(disposition); if (matches != null && matches[1]) { let filename = matches[1].replace(/['"]/g, ''); if (filename.startsWith("UTF-8''")) { filename = decodeURIComponent(filename.replace("UTF-8''", '')); } latestDownloadFilename = filename; } } } catch (e) {} return response; }; const originalCreateElement = document.createElement.bind(document); document.createElement = function(tagName, options) { const el = originalCreateElement(tagName, options); if (tagName.toLowerCase() === 'a') { const originalSetAttribute = el.setAttribute.bind(el); el.setAttribute = function(name, value) { if (name === 'download') { latestDownloadFilename = value; } else if (name === 'href' && !latestDownloadFilename) { try { const url = new URL(value, window.location.origin); const fname = url.pathname.split('/').pop(); if (fname && fname.includes('.')) latestDownloadFilename = fname; } catch (e) {} } return originalSetAttribute(name, value); }; if (el.getAttribute('download')) { latestDownloadFilename = el.getAttribute('download'); } } return el; }; const originalOpenWindow = window.open; window.open = function(url, ...args) { if (url && typeof url === 'string') { try { const path = new URL(url, window.location.origin).pathname; const fname = path.split('/').pop(); if (fname && fname.includes('.')) latestDownloadFilename = fname; } catch (e) {} } return originalOpenWindow.call(window, url, ...args); }; // ================= 3. 辅助函数 ================= function waitForElement(selector, timeout = 10000) { return new Promise((resolve, reject) => { const el = document.querySelector(selector); if (el) return resolve(el); const observer = new MutationObserver(() => { const el = document.querySelector(selector); if (el) { observer.disconnect(); resolve(el); } }); observer.observe(document.body, { childList: true, subtree: true }); setTimeout(() => { observer.disconnect(); reject(new Error(`未找到元素: ${selector}`)); }, timeout); }); } async function clickButtonByText(text, parentSelector = 'body', timeout = 5000) { await waitForElement(`${parentSelector} button, ${parentSelector} label`, timeout); const allBtns = document.querySelectorAll(`${parentSelector} button, ${parentSelector} label`); const normalizedText = text.replace(/\s+/g, ''); for (const btn of allBtns) { const btnText = btn.innerText.replace(/\s+/g, ''); if (btnText.includes(normalizedText)) { btn.click(); await new Promise(r => setTimeout(r, 300)); return; } } throw new Error(`未找到包含文字 "${text}" 的按钮`); } async function clickSpecificExportButton() { await waitForElement('button.el-button', 5000); const buttons = document.querySelectorAll('button.el-button'); for (const btn of buttons) { if (btn.innerText.replace(/\s+/g, '').includes('导出') && btn.querySelector('svg use[*|href="#icondaochu"]')) { btn.scrollIntoView({ behavior: 'smooth', block: 'center' }); await new Promise(r => setTimeout(r, 300)); btn.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window })); await new Promise(r => setTimeout(r, 300)); return; } } throw new Error('未找到带图标的导出按钮'); } function waitForLoadingToDisappear() { return new Promise((resolve) => { const checkInterval = setInterval(() => { const loaders = document.querySelectorAll('.el-loading-mask, .el-loading-spinner'); let isLoading = false; loaders.forEach(l => { if (l.offsetParent !== null) isLoading = true; }); if (!isLoading) { clearInterval(checkInterval); resolve(); } }, 500); }); } function setVueInputValue(inputEl, value) { const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set; nativeInputValueSetter.call(inputEl, value); inputEl.dispatchEvent(new Event('input', { bubbles: true })); inputEl.dispatchEvent(new Event('change', { bubbles: true })); } async function safeClick(el) { el.click(); await new Promise(r => setTimeout(r, 300)); } async function goBackToParentTask() { await waitForElement('div#tab-0[role="tab"]', 8000); const clickTarget = document.querySelector('#tab-0 > span > svg > use') || document.querySelector('#tab-0 > span.task_name') || document.querySelector('div#tab-0[role="tab"] .task_name'); if (!clickTarget) throw new Error('未找到返回按钮'); clickTarget.scrollIntoView({ behavior: 'smooth', block: 'center' }); await new Promise(r => setTimeout(r, 300)); ['mousedown', 'mouseup', 'click'].forEach(type => { clickTarget.dispatchEvent(new MouseEvent(type, { bubbles: true, cancelable: true, view: window })); }); await new Promise(r => setTimeout(r, 2500)); } function decodeFilename(name) { if (!name) return ''; try { return decodeURIComponent(name); } catch (e) { return name; } } function acquireGlobalDownloadLock(lockName, ttl = 2000) { const lockStore = window.__uscDownloadLocks || (window.__uscDownloadLocks = {}); const now = Date.now(); if (lockStore[lockName] && now - lockStore[lockName] < ttl) { return false; } lockStore[lockName] = now; return true; } // ================= 4. 数据模型 ================= let projects = []; // [{ id, name, tasks: [], nextTaskId }] let currentProjectId = null; let mappingData = []; // [[taskName, fileName, projectName], ...] loadFromStorage(); function getCurrentProject() { return projects.find(p => p.id === currentProjectId) || null; } // ================= 5. UI 面板 ================= const panelHTML = `
备份任务 全量采集 数据看板 题ID采集 API导出
×
任务列表
进度: 0/0 待命
系统就绪
`; document.body.insertAdjacentHTML('beforeend', panelHTML); // ================= 6. DOM 引用与事件绑定 ================= const panel = document.getElementById('ultimate-scraper-panel'); const header = document.getElementById('usc-header'); const minimizeBtn = document.getElementById('usc-minimize'); const bodyDiv = document.getElementById('usc-body'); const logArea = document.getElementById('usc-log-area'); const taskListDOM = document.getElementById('usc-task-list'); const startBtn = document.getElementById('usc-start'); const projectSelect = document.getElementById('usc-project-select'); // 标签切换 document.querySelectorAll('.usc-tab').forEach(tab => { // 添加hover效果 tab.addEventListener('mouseenter', function() { if (!this.classList.contains('active')) { this.style.background = 'rgba(255,255,255,0.15)'; } }); tab.addEventListener('mouseleave', function() { if (!this.classList.contains('active')) { this.style.background = 'transparent'; } }); // 点击切换 tab.addEventListener('click', function() { // 移除所有active样式 document.querySelectorAll('.usc-tab').forEach(t => { t.classList.remove('active'); t.style.background = 'transparent'; t.style.boxShadow = 'none'; }); // 添加当前active样式 this.classList.add('active'); this.style.background = 'rgba(255,255,255,0.25)'; this.style.boxShadow = '0 2px 8px rgba(0,0,0,0.1)'; // 切换内容显示 document.querySelectorAll('.usc-tab-content').forEach(content => { content.style.display = content.id === `tab-${this.dataset.tab}` ? 'block' : 'none'; }); // 刷新任务下拉列表 if (this.dataset.tab === 'topic') { refreshTopicTaskCheckboxList(); } // 刷新数据看板 if (this.dataset.tab === 'view') { renderViewList(); } // API导出 - 尝试自动填充 access-key if (this.dataset.tab === 'api') { const akInput = document.getElementById('api-ak'); if (akInput && !akInput.value) { // 试着从页面已有的请求中找 access-key const scripts = document.querySelectorAll('script'); for (const s of scripts) { const m = s.textContent.match(/access-key["']?\s*:\s*["']([^"']+)/); if (m) { akInput.value = m[1]; break; } } } } }); }); // 最小化/还原 let isMinimized = false; minimizeBtn.addEventListener('click', () => { if (isMinimized) { bodyDiv.style.display = 'block'; panel.style.width = '500px'; panel.style.height = ''; minimizeBtn.innerText = '–'; isMinimized = false; } else { bodyDiv.style.display = 'none'; panel.style.width = '200px'; minimizeBtn.innerText = '□'; isMinimized = true; } }); // 关闭 document.getElementById('usc-close').addEventListener('click', () => panel.remove()); // 拖动 let isDragging = false, dragOffsetX, dragOffsetY; header.addEventListener('mousedown', function(e) { if (e.target.id === 'usc-minimize' || e.target.id === 'usc-close' || e.target.classList.contains('usc-tab')) return; isDragging = true; const rect = panel.getBoundingClientRect(); dragOffsetX = e.clientX - rect.left; dragOffsetY = e.clientY - rect.top; panel.style.transition = 'none'; e.preventDefault(); }); document.addEventListener('mousemove', function(e) { if (!isDragging) return; panel.style.left = Math.min(Math.max(0, e.clientX - dragOffsetX), window.innerWidth - panel.offsetWidth) + 'px'; panel.style.top = Math.min(Math.max(0, e.clientY - dragOffsetY), window.innerHeight - 50) + 'px'; panel.style.right = 'auto'; }); document.addEventListener('mouseup', function() { if (isDragging) { isDragging = false; panel.style.transition = ''; } }); // 日志输出 function addLog(msg, type = 'info') { const colors = { info: '#cccccc', success: '#4EC9B0', error: '#F56C6C', warn: '#E6A23C' }; logArea.innerHTML += `
${new Date().toLocaleTimeString()} - ${msg}
`; logArea.scrollTop = logArea.scrollHeight; } // ================= 7. 备份任务核心功能(保持原有逻辑) ================= function updateProgress() { const project = getCurrentProject(); const total = project ? project.tasks.length : 0; const successCount = project ? project.tasks.filter(t => t.status === 'success').length : 0; document.getElementById('usc-progress-bar').style.width = total ? ((successCount / total) * 100).toFixed(0) + '%' : '0%'; document.getElementById('usc-progress-text').innerText = `进度: ${successCount}/${total}`; } function renderProjectSelect() { projectSelect.innerHTML = ''; projects.forEach(p => { const option = document.createElement('option'); option.value = p.id; option.textContent = p.name; if (p.id === currentProjectId) option.selected = true; projectSelect.appendChild(option); }); if (!currentProjectId && projects.length > 0) { currentProjectId = projects[0].id; projectSelect.value = currentProjectId; } renderTaskList(); updateProgress(); } function renderTaskList() { const project = getCurrentProject(); taskListDOM.innerHTML = ''; if (!project) return; project.tasks.forEach(task => { const statusStyles = { pending: 'color: #606266;', running: 'color: #E6A23C; font-weight: bold;', success: 'color: #67C23A; text-decoration: line-through;', error: 'color: #F56C6C;' }; const statusTexts = { pending: '', running: '执行中...', success: '✅ 已完成', error: '❌ 失败' }; const toggleBtn = (task.status === 'success' || task.status === 'error') ? `` : ``; taskListDOM.insertAdjacentHTML('beforeend', `
${task.name} ${statusTexts[task.status]} ${toggleBtn}
`); }); // 绑定状态切换事件 document.querySelectorAll('.usc-toggle-btn').forEach(btn => { btn.addEventListener('click', function(e) { e.stopPropagation(); const id = Number(this.dataset.id); const project = getCurrentProject(); if (!project) return; const task = project.tasks.find(t => t.id === id); if (!task) return; if (task.status === 'success' || task.status === 'error') { task.status = 'pending'; mappingData = mappingData.filter(row => row[0] !== task.name); } else { task.status = 'success'; if (!task.fileName) task.fileName = `手动标记_${Date.now()}.zip`; if (!mappingData.some(row => row[0] === task.name)) { mappingData.push([task.name, task.fileName, project.name]); } } renderTaskList(); updateProgress(); saveToStorage(); }); }); updateProgress(); } // 项目选择 projectSelect.addEventListener('change', () => { currentProjectId = Number(projectSelect.value); renderTaskList(); updateProgress(); saveToStorage(); }); // 新建项目 document.getElementById('usc-add-project-btn').addEventListener('click', () => { const name = prompt('请输入项目名称:'); if (!name) return; const newId = projects.length > 0 ? Math.max(...projects.map(p => p.id)) + 1 : 1; projects.push({ id: newId, name, tasks: [], nextTaskId: 1 }); currentProjectId = newId; renderProjectSelect(); saveToStorage(); }); // 删除项目 document.getElementById('usc-del-project-btn').addEventListener('click', () => { if (!confirm('确定删除当前项目及其所有任务吗?')) return; projects = projects.filter(p => p.id !== currentProjectId); currentProjectId = projects.length > 0 ? projects[0].id : null; renderProjectSelect(); saveToStorage(); }); // 添加任务 document.getElementById('usc-add-task-btn').addEventListener('click', () => { const project = getCurrentProject(); if (!project) return alert('请先创建/选择一个项目'); const input = document.getElementById('usc-new-task-input'); const name = input.value.trim(); if (!name) return; project.tasks.push({ id: project.nextTaskId++, name, status: 'pending', fileName: '' }); input.value = ''; renderTaskList(); saveToStorage(); addLog(`已添加任务: ${name}`, 'success'); }); // 导入 CSV document.getElementById('usc-import-csv-btn').addEventListener('click', () => { document.getElementById('usc-csv-upload').click(); }); document.getElementById('usc-csv-upload').addEventListener('change', function(e) { const project = getCurrentProject(); if (!project) return alert('请先选择一个项目'); const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = function(evt) { const text = evt.target.result.replace(/^\uFEFF/, ''); const lines = text.split(/\r?\n/).filter(line => line.trim() !== ''); let added = 0; for (let i = 1; i < lines.length; i++) { const cols = lines[i].split(','); const name = cols[0].replace(/"/g, '').trim(); if (name) { project.tasks.push({ id: project.nextTaskId++, name, status: 'pending', fileName: '' }); added++; } } if (added) { renderTaskList(); saveToStorage(); addLog(`导入 ${added} 个任务`, 'success'); } else alert('CSV解析失败'); }; reader.readAsText(file); }); // 全选/反选 document.getElementById('usc-select-all').addEventListener('click', () => { document.querySelectorAll('.usc-task-check:not(:disabled)').forEach(cb => cb.checked = true); }); document.getElementById('usc-select-reverse').addEventListener('click', () => { document.querySelectorAll('.usc-task-check:not(:disabled)').forEach(cb => cb.checked = !cb.checked); }); // 执行备份(原有逻辑保留,但为了代码简洁省略详细注释,保持原有功能) startBtn.addEventListener('click', async () => { const project = getCurrentProject(); if (!project) return alert('请选择一个项目'); const checkedBoxes = document.querySelectorAll('.usc-task-check:checked'); if (checkedBoxes.length === 0) return alert('请勾选任务'); startBtn.disabled = true; startBtn.innerText = '⏳ 执行中...'; const selectedTaskIds = Array.from(checkedBoxes).map(cb => Number(cb.dataset.id)); const tasksToRun = project.tasks.filter(t => selectedTaskIds.includes(t.id) && t.status !== 'success'); let successCount = project.tasks.filter(t => t.status === 'success').length; for (const task of tasksToRun) { let hasReturnedToParent = false; task.status = 'running'; renderTaskList(); document.getElementById('usc-status').innerText = '处理中'; addLog(`---------- 开始处理: ${task.name} ----------`, 'info'); try { // 1. 搜索 addLog('1. 搜索任务...', 'info'); const searchInput = await waitForElement('.search_list input.el-input__inner'); setVueInputValue(searchInput, task.name); await clickButtonByText('搜索', '.search_list'); await new Promise(r => setTimeout(r, 2500)); // 2. 进入详情 addLog('2. 点击进入任务详情...', 'info'); const rows = document.querySelectorAll('.el-table__body-wrapper tbody tr.el-table__row'); let entered = false; const normalize = s => (s || '').replace(/[\s\u00A0\u3000]+/g, '').trim(); const targetName = normalize(task.name); for (const row of rows) { let rowName = ''; try { const vue = row.__vue__; if (vue && vue.row) { rowName = normalize(vue.row.task_name || vue.row.name || ''); } } catch(e) {} if (rowName === targetName) { addLog(`Vue行数据匹配命中: "${rowName}"`, 'success'); const nameSpan = row.querySelector('td.organizationStyle span'); const nameTd = row.querySelector('td.organizationStyle'); if (nameSpan) { await safeClick(nameSpan); entered = true; break; } if (nameTd) { await safeClick(nameTd); entered = true; break; } } } if (!entered) { addLog('尝试表格数据匹配...', 'warn'); try { const tableEl = document.querySelector('.el-table'); if (tableEl && tableEl.__vue__) { let vm = tableEl.__vue__; while (vm) { if (vm.$data && vm.$data.tableOptions && Array.isArray(vm.$data.tableOptions.data)) { const tableData = vm.$data.tableOptions.data; for (let i = 0; i < tableData.length; i++) { const rd = tableData[i]; const rowName = normalize(rd.task_name || rd.name || ''); if (rowName === targetName && i < rows.length) { addLog(`表格数据匹配命中(索引${i}): "${rowName}"`, 'success'); const row = rows[i]; const nameSpan = row.querySelector('td.organizationStyle span'); const nameTd = row.querySelector('td.organizationStyle'); if (nameSpan) { await safeClick(nameSpan); entered = true; break; } if (nameTd) { await safeClick(nameTd); entered = true; break; } } } break; } vm = vm.$parent; } } } catch(e) { addLog(`表格数据匹配异常: ${e.message}`, 'warn'); } } if (!entered && rows.length === 1) { addLog('仅一行搜索结果,直接点击', 'warn'); const row = rows[0]; const nameSpan = row.querySelector('td.organizationStyle span'); const nameTd = row.querySelector('td.organizationStyle'); if (nameSpan) { await safeClick(nameSpan); entered = true; } else if (nameTd) { await safeClick(nameTd); entered = true; } } if (!entered) throw new Error('搜索结果中未找到该任务'); // 3. 等待详情页加载 addLog('3. 等待详情页加载...', 'info'); try { await waitForElement('div#tab-0', 8000); } catch(e) { throw new Error('未成功进入任务详情页'); } await new Promise(r => setTimeout(r, 3000)); // 4. 点击导出按钮 addLog('4. 点击导出按钮...', 'info'); await clickSpecificExportButton(); await new Promise(r => setTimeout(r, 2000)); // 5. 选择“原文件带JSON” addLog('5. 选择原文件带JSON...', 'info'); const radioLabels = document.querySelectorAll('.drawer_box .el-radio-group label'); let radioClicked = false; for (const label of radioLabels) { if (label.innerText.replace(/\s+/g, '').includes('原文件带JSON')) { await safeClick(label); radioClicked = true; break; } } if (!radioClicked) throw new Error('未找到该选项'); await new Promise(r => setTimeout(r, 500)); // 6. 确认导出 addLog('6. 确认导出...', 'info'); await clickButtonByText('导出', '.drawer__footer'); await new Promise(r => setTimeout(r, 2000)); await waitForLoadingToDisappear(); await new Promise(r => setTimeout(r, 2000)); // 7. 等待下载管理页面 addLog('7. 等待进入下载管理页面...', 'info'); try { await waitForElement('.el-table__header', 15000); } catch(e) { throw new Error('未检测到下载管理页面'); } // 8. 监听打包状态 addLog('8. 等待打包完成...', 'info'); const MAX_WAIT_MS = 300000; const POLL_INTERVAL_MS = 3000; const waitStart = Date.now(); let packDone = false; function getFirstRowStatus() { const firstRow = document.querySelector('.el-table__body-wrapper tbody tr.el-table__row'); if (!firstRow) return ''; const allSpans = firstRow.querySelectorAll('.cell span'); for (const sp of allSpans) { const t = sp.innerText.trim(); if (t === '已完成' || t === '处理中') return t; } const svgUse = firstRow.querySelector('svg use'); if (svgUse) { const href = svgUse.getAttribute('xlink:href') || svgUse.getAttribute('href') || ''; if (href.includes('iconyitongguo')) return '已完成'; if (href.includes('iconjinhangzhong')) return '处理中'; } return ''; } while (Date.now() - waitStart < MAX_WAIT_MS) { const status = getFirstRowStatus(); addLog(`第一行状态: ${status || '未知'}`, 'info'); if (status === '已完成') { packDone = true; break; } const elapsed = Math.round((Date.now() - waitStart) / 1000); addLog(`打包中... 已等待${elapsed}秒`, 'info'); await new Promise(r => setTimeout(r, POLL_INTERVAL_MS)); } if (!packDone) { addLog('打包超时(5分钟),尝试继续下载...', 'warn'); } else { addLog('打包已完成!', 'success'); } await new Promise(r => setTimeout(r, 1000)); // 9. 点击第一行的下载按钮 addLog('9. 查找下载按钮...', 'info'); let downloadBtn = null; let attempt = 0; while (attempt < 10 && !downloadBtn) { const firstRow = document.querySelector('.el-table__body-wrapper tbody tr.el-table__row'); if (firstRow) { const btn = firstRow.querySelector('button'); if (btn && btn.innerText.replace(/\s+/g, '') === '下载') { const color = btn.style.color; if (color && color !== 'rgb(192, 196, 200)') { downloadBtn = btn; } } } if (!downloadBtn) { await new Promise(r => setTimeout(r, 2000)); attempt++; } } if (!downloadBtn) throw new Error('未找到可用下载按钮'); downloadBtn.scrollIntoView({ behavior: 'smooth', block: 'center' }); await new Promise(r => setTimeout(r, 300)); await safeClick(downloadBtn); addLog('下载按钮已触发', 'success'); // 10. 捕获文件名 addLog('10. 捕获文件名...', 'info'); await new Promise(r => setTimeout(r, 2000)); if (!latestDownloadFilename) { const allLinks = document.querySelectorAll('a[download]'); for (const link of allLinks) { const d = link.getAttribute('download'); if (d) { latestDownloadFilename = d; break; } } } let finalFilename = decodeFilename(latestDownloadFilename) || `未拦截到文件名_${Date.now()}.zip`; task.fileName = finalFilename; mappingData.push([task.name, finalFilename, project.name]); // 11. 返回上级 addLog('11. 返回上级...', 'info'); await goBackToParentTask(); hasReturnedToParent = true; task.status = 'success'; successCount++; addLog(`✅ 完成: ${task.name} -> ${finalFilename}`, 'success'); saveToStorage(); } catch (err) { task.status = 'error'; addLog(`❌ 失败: ${task.name} | ${err.message}`, 'error'); saveToStorage(); } renderTaskList(); updateProgress(); try { if (!hasReturnedToParent) { await goBackToParentTask(); } } catch (e) {} } document.getElementById('usc-status').innerText = '批次完成'; document.getElementById('usc-status').style.color = '#67C23A'; startBtn.disabled = false; startBtn.innerText = '▶ 执行选中任务'; addLog('🎉 当前勾选任务执行完毕!', 'success'); saveToStorage(); }); // 导出映射表 document.getElementById('usc-export-map').addEventListener('click', () => { const project = getCurrentProject(); if (!project) return alert('无项目'); const data = mappingData.filter(row => row[2] === project.name); if (data.length === 0) return alert('无映射数据'); let csvContent = "\uFEFF任务名称,压缩包名称\n"; data.forEach(row => csvContent += `"${row[0]}","${row[1]}"\n`); const link = document.createElement('a'); link.href = URL.createObjectURL(new Blob([csvContent], { type: 'text/csv;charset=utf-8;' })); link.download = `${project.name}_映射表.csv`; document.body.appendChild(link); link.click(); document.body.removeChild(link); }); // 映射表弹窗 const mapDialog = document.getElementById('usc-map-dialog'); const mapTbody = document.getElementById('usc-map-tbody'); const mapEmpty = document.getElementById('usc-map-empty'); function getProjectMappingIndices() { const project = getCurrentProject(); if (!project) return []; const indices = []; mappingData.forEach((row, i) => { if (row[2] === project.name) indices.push(i); }); return indices; } function renderMapTable() { const project = getCurrentProject(); const data = project ? mappingData.filter(row => row[2] === project.name) : []; mapTbody.innerHTML = ''; mapEmpty.style.display = data.length === 0 ? 'block' : 'none'; data.forEach((row, idx) => { const tr = document.createElement('tr'); tr.style.borderBottom = '1px solid #ebeef5'; tr.innerHTML = ` ${idx + 1} ${row[0]} ${row[1]} `; mapTbody.appendChild(tr); }); // 双击单元格编辑(简化版) mapTbody.querySelectorAll('.usc-map-cell').forEach(td => { td.addEventListener('dblclick', function() { if (this.querySelector('input')) return; const field = this.dataset.field; const idx = Number(this.dataset.idx); const oldVal = field === 'index' ? String(idx + 1) : this.innerText; const input = document.createElement('input'); input.type = 'text'; input.value = oldVal; input.style.cssText = 'width: 100%; padding: 2px 4px; font-size: 13px; border: 1px solid #409EFF; border-radius: 3px; outline: none; box-sizing: border-box;'; this.innerText = ''; this.appendChild(input); input.focus(); input.select(); const commitEdit = () => { const globalIndices = getProjectMappingIndices(); const newVal = input.value.trim(); if (field === 'index') { const newIdx = parseInt(newVal) - 1; if (isNaN(newIdx) || newIdx < 0 || newIdx >= data.length || newIdx === idx) { renderMapTable(); return; } const gIdx = globalIndices[idx]; const row = mappingData.splice(gIdx, 1)[0]; const newGIdx = globalIndices[newIdx] !== undefined ? globalIndices[newIdx] : globalIndices[globalIndices.length - 1] + 1; mappingData.splice(newGIdx, 0, row); } else if (field === 'task') { if (newVal && newVal !== oldVal) mappingData[globalIndices[idx]][0] = newVal; } else if (field === 'file') { if (newVal && newVal !== oldVal) mappingData[globalIndices[idx]][1] = newVal; } saveToStorage(); renderMapTable(); }; input.addEventListener('blur', commitEdit); input.addEventListener('keydown', (e) => { if (e.key === 'Enter') { input.blur(); } if (e.key === 'Escape') { input.removeEventListener('blur', commitEdit); renderMapTable(); } }); }); }); // 编辑按钮 mapTbody.querySelectorAll('.usc-map-edit-btn').forEach(btn => { btn.addEventListener('click', function() { const idx = Number(this.dataset.idx); const row = mapTbody.children[idx]; if (!row || row.dataset.editing === 'true') return; row.dataset.editing = 'true'; const globalIndices = getProjectMappingIndices(); const gIdx = globalIndices[idx]; const mapRow = mappingData[gIdx]; const cells = row.querySelectorAll('.usc-map-cell'); const indexInput = document.createElement('input'); indexInput.type = 'number'; indexInput.value = idx + 1; indexInput.min = 1; indexInput.max = data.length; indexInput.style.cssText = 'width: 50px; padding: 2px 4px; font-size: 13px; border: 1px solid #409EFF; border-radius: 3px;'; cells[0].innerText = ''; cells[0].appendChild(indexInput); const taskInput = document.createElement('input'); taskInput.type = 'text'; taskInput.value = mapRow[0]; taskInput.style.cssText = 'width: 100%; padding: 2px 4px; font-size: 13px; border: 1px solid #409EFF; border-radius: 3px; box-sizing: border-box;'; cells[1].innerText = ''; cells[1].appendChild(taskInput); const fileInput = document.createElement('input'); fileInput.type = 'text'; fileInput.value = mapRow[1]; fileInput.style.cssText = 'width: 100%; padding: 2px 4px; font-size: 13px; border: 1px solid #409EFF; border-radius: 3px; box-sizing: border-box;'; cells[2].innerText = ''; cells[2].appendChild(fileInput); const opTd = row.querySelector('td:last-child'); opTd.innerHTML = ` `; opTd.querySelector('.usc-map-save-btn').addEventListener('click', function() { const newIdx = parseInt(indexInput.value) - 1; const newTask = taskInput.value.trim(); const newFile = fileInput.value.trim(); if (!newTask || !newFile) return alert('任务名称和压缩包名称不能为空'); mappingData[gIdx][0] = newTask; mappingData[gIdx][1] = newFile; if (!isNaN(newIdx) && newIdx >= 0 && newIdx < data.length && newIdx !== idx) { const movedRow = mappingData.splice(gIdx, 1)[0]; const recalcIndices = getProjectMappingIndices(); const targetGIdx = newIdx < recalcIndices.length ? recalcIndices[newIdx] : recalcIndices[recalcIndices.length - 1] + 1; mappingData.splice(targetGIdx, 0, movedRow); } saveToStorage(); renderMapTable(); }); opTd.querySelector('.usc-map-cancel-btn').addEventListener('click', () => renderMapTable()); taskInput.focus(); }); }); // 删除事件 mapTbody.querySelectorAll('.usc-map-del-btn').forEach(btn => { btn.addEventListener('click', function() { const project = getCurrentProject(); if (!project) return; const delIdx = Number(this.dataset.idx); const projectData = mappingData.filter(row => row[2] === project.name); if (delIdx < 0 || delIdx >= projectData.length) return; const delRow = projectData[delIdx]; const globalIdx = mappingData.indexOf(delRow); if (globalIdx !== -1) { mappingData.splice(globalIdx, 1); saveToStorage(); renderMapTable(); } }); }); } document.getElementById('usc-view-map').addEventListener('click', () => { if (!getCurrentProject()) return alert('请先选择一个项目'); renderMapTable(); mapDialog.style.display = 'flex'; }); document.getElementById('usc-map-dialog-close').addEventListener('click', () => { mapDialog.style.display = 'none'; }); mapDialog.addEventListener('click', (e) => { if (e.target === mapDialog) mapDialog.style.display = 'none'; }); document.getElementById('usc-map-add-btn').addEventListener('click', () => { const project = getCurrentProject(); if (!project) return; const taskInput = document.getElementById('usc-map-add-task'); const fileInput = document.getElementById('usc-map-add-file'); const taskName = taskInput.value.trim(); const fileName = fileInput.value.trim(); if (!taskName) return alert('请输入任务名称'); if (!fileName) return alert('请输入压缩包名称'); mappingData.push([taskName, fileName, project.name]); taskInput.value = ''; fileInput.value = ''; saveToStorage(); renderMapTable(); }); // ================= 8. 全量采集器(任务列表)原有功能保留 ================= let collectAllData = []; let isCollecting = false; let isCollectDownloading = false; function getVueTableData() { const tableEl = document.querySelector('.el-table'); if (!tableEl || !tableEl.__vue__) return []; let vm = tableEl.__vue__; while (vm.$parent) { if (vm.$data && vm.$data.tableOptions && Array.isArray(vm.$data.tableOptions.data)) { return vm.$data.tableOptions.data; } vm = vm.$parent; } return []; } function collectClickAndWait(element) { return new Promise((resolve) => { element.click(); setTimeout(resolve, 1500); }); } document.getElementById('collect-start').addEventListener('click', async function() { if (isCollecting) return; isCollecting = true; collectAllData = []; this.disabled = true; this.innerText = '正在采集中...'; document.getElementById('collect-download').style.display = 'none'; const statusEl = document.getElementById('collect-status'); const progressBar = document.getElementById('collect-progress-bar'); const progressText = document.getElementById('collect-progress-text'); const dataCount = document.getElementById('collect-data-count'); statusEl.innerText = '准备翻至第一页...'; statusEl.style.color = '#E6A23C'; try { const totalText = document.querySelector('.el-pagination__total').innerText; const totalItems = parseInt(totalText.match(/\d+/)[0]); const pageSize = 20; const totalPages = Math.ceil(totalItems / pageSize) || 1; const firstPageBtn = document.querySelector('.el-pager li.number'); if (firstPageBtn) await collectClickAndWait(firstPageBtn); statusEl.innerText = '正在采集...'; statusEl.style.color = '#409EFF'; for (let page = 1; page <= totalPages; page++) { const pageData = getVueTableData(); if (pageData.length > 0) collectAllData = collectAllData.concat(pageData); const percent = (page / totalPages * 100).toFixed(0) + '%'; progressBar.style.width = percent; progressText.innerText = `进度: ${page}/${totalPages}`; dataCount.innerText = `已采集: ${collectAllData.length} 条`; const nextBtn = document.querySelector('.el-pagination .btn-next:not([disabled])'); if (nextBtn && page < totalPages) { await collectClickAndWait(nextBtn); } else { break; } } // 去重 const uniqueMap = new Map(); collectAllData.forEach(item => { if (item.task_key && !uniqueMap.has(item.task_key)) { uniqueMap.set(item.task_key, item); } }); collectAllData = Array.from(uniqueMap.values()); statusEl.innerText = '采集完成!'; statusEl.style.color = '#67C23A'; dataCount.innerText = `去重后: ${collectAllData.length} 条`; document.getElementById('collect-download').style.display = 'block'; // 刷新数据看板 addLog('全量采集完成,可切换到「数据看板」标签查看', 'success'); } catch (err) { statusEl.innerText = '采集出错'; statusEl.style.color = '#F56C6C'; } finally { this.disabled = false; this.innerText = '重新采集'; isCollecting = false; } }); document.getElementById('collect-download').addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); if (isCollectDownloading) return; if (!acquireGlobalDownloadLock('collect-download')) return; if (collectAllData.length === 0) return alert('没有数据!'); isCollectDownloading = true; const downloadBtn = e.currentTarget; downloadBtn.disabled = true; const headers = ['任务名称', '任务key', '标注类型', '总题数', '标注进度', '审核进度', '质检进度', '验收进度', '创建人', '创建时间', '任务状态']; let csvContent = "\uFEFF" + headers.join(',') + '\n'; const formatVal = (val) => { if (val === null || val === undefined) val = ''; val = String(val); if (val.includes(',') || val.includes('"') || val.includes('\n')) { return '"' + val.replace(/"/g, '""') + '"'; } return val; }; collectAllData.forEach(item => { const row = [ item.task_name, item.task_key, item.task_type, item.total_num, item.label_progress, item.check_progress, item.inspect_progress, item.accept_progress, item.creator, item.created_at, item.online_label || (item.switch ? '上线' : '下线') ].map(formatVal); csvContent += row.join(',') + '\n'; }); const link = document.createElement('a'); const objectUrl = URL.createObjectURL(new Blob([csvContent], { type: 'text/csv;charset=utf-8;' })); link.href = objectUrl; link.download = '全量完整任务数据.csv'; document.body.appendChild(link); link.click(); document.body.removeChild(link); setTimeout(() => { URL.revokeObjectURL(objectUrl); downloadBtn.disabled = false; isCollectDownloading = false; }, 300); }); // ================= 9. 题ID采集器(新增功能) ================= let topicCollectorData = []; let isTopicCollecting = false; let isTopicDownloading = false; function refreshTopicTaskCheckboxList() { const project = getCurrentProject(); const container = document.getElementById('topic-task-checkbox-list'); if (!container) return; if (!project || !project.tasks || project.tasks.length === 0) { container.innerHTML = '
暂无任务,请先在备份任务中添加
'; return; } container.innerHTML = ''; project.tasks.forEach(task => { const div = document.createElement('div'); div.style.marginBottom = '8px'; div.innerHTML = ` `; container.appendChild(div); }); } function getSelectedTopicTasks() { const checkboxes = document.querySelectorAll('.topic-task-checkbox:checked'); return Array.from(checkboxes).map(cb => cb.value); } // 从详情页表格中采集题ID和序列名称(自动翻页) async function collectTopicAndSequence(taskName, onProgress) { const collected = []; // 等待数据详情表格加载 await waitForElement('.dataDetail .el-table', 10000); // 获取分页信息 const getPaginationInfo = () => { const totalSpan = document.querySelector('.el-pagination__total'); let total = 0; if (totalSpan) { const match = totalSpan.innerText.match(/\d+/); if (match) total = parseInt(match[0], 10); } const pageSize = 20; // 默认每页20条 const totalPages = Math.ceil(total / pageSize) || 1; return { total, totalPages, pageSize }; }; // 从当前表格提取数据 const extractCurrentPageData = () => { const rows = document.querySelectorAll('.el-table__body-wrapper tbody tr.el-table__row'); const pageData = []; for (const row of rows) { // 题ID(第2列) const topicIdCell = row.querySelector('td:nth-child(2) .cell'); const topicId = topicIdCell ? topicIdCell.innerText.trim() : ''; // 序列名称(第3列) const seqNameCell = row.querySelector('td:nth-child(3) .cell'); const seqName = seqNameCell ? seqNameCell.innerText.trim() : ''; // 工序状态(第4-7列) const s1 = row.querySelector('td:nth-child(4) .cell'); const s2 = row.querySelector('td:nth-child(5) .cell'); const s3 = row.querySelector('td:nth-child(6) .cell'); const s4 = row.querySelector('td:nth-child(7) .cell'); if (topicId) { pageData.push({ 题ID: topicId, 序列名称: seqName, 标注工序: s1 ? s1.innerText.trim() : '', 审核工序: s2 ? s2.innerText.trim() : '', 质检工序: s3 ? s3.innerText.trim() : '', 验收工序: s4 ? s4.innerText.trim() : '' }); } } return pageData; }; // 点击翻页 const goToNextPage = async () => { const nextBtn = document.querySelector('.el-pagination .btn-next:not([disabled])'); if (nextBtn) { nextBtn.click(); await new Promise(r => setTimeout(r, 2000)); await waitForLoadingToDisappear(); await new Promise(r => setTimeout(r, 1000)); return true; } return false; }; // 确保在第一页 const firstPageBtn = document.querySelector('.el-pager li.number'); if (firstPageBtn && !firstPageBtn.classList.contains('active')) { firstPageBtn.click(); await new Promise(r => setTimeout(r, 2000)); await waitForLoadingToDisappear(); } let { totalPages } = getPaginationInfo(); if (totalPages === 0) totalPages = 1; for (let page = 1; page <= totalPages; page++) { const pageData = extractCurrentPageData(); collected.push(...pageData); if (onProgress) { onProgress(page, totalPages, collected.length); } if (page < totalPages) { const hasNext = await goToNextPage(); if (!hasNext) break; // 重新获取总页数(可能动态变化) const newInfo = getPaginationInfo(); if (newInfo.totalPages !== totalPages) { totalPages = newInfo.totalPages; } } } return collected; } // 进入任务详情并采集 async function enterTaskAndCollect(taskName, onStatus) { onStatus('正在搜索任务...'); // 搜索任务 const searchInput = await waitForElement('.search_list input.el-input__inner'); setVueInputValue(searchInput, taskName); await clickButtonByText('搜索', '.search_list'); await new Promise(r => setTimeout(r, 2500)); onStatus('正在查找任务...'); const rows = document.querySelectorAll('.el-table__body-wrapper tbody tr.el-table__row'); let entered = false; const normalize = s => (s || '').replace(/[\s\u00A0\u3000]+/g, '').trim(); const targetName = normalize(taskName); // 匹配任务行 for (const row of rows) { let rowName = ''; try { const vue = row.__vue__; if (vue && vue.row) { rowName = normalize(vue.row.task_name || vue.row.name || ''); } } catch(e) {} if (rowName === targetName) { const nameSpan = row.querySelector('td.organizationStyle span'); const nameTd = row.querySelector('td.organizationStyle'); if (nameSpan) { await safeClick(nameSpan); entered = true; break; } if (nameTd) { await safeClick(nameTd); entered = true; break; } } } if (!entered && rows.length === 1) { const row = rows[0]; const nameSpan = row.querySelector('td.organizationStyle span'); const nameTd = row.querySelector('td.organizationStyle'); if (nameSpan) { await safeClick(nameSpan); entered = true; } else if (nameTd) { await safeClick(nameTd); entered = true; } } if (!entered) { throw new Error('未找到任务入口'); } onStatus('进入任务详情页...'); try { await waitForElement('div#tab-0', 8000); } catch(e) { throw new Error('未成功进入任务详情页'); } await new Promise(r => setTimeout(r, 2000)); // 切换到“数据详情”标签页 onStatus('切换到数据详情标签...'); const dataDetailTab = document.querySelector('#tab-first'); if (dataDetailTab && !dataDetailTab.classList.contains('is-active')) { dataDetailTab.click(); await new Promise(r => setTimeout(r, 1500)); } onStatus('开始采集题ID和序列名称...'); const collected = await collectTopicAndSequence(taskName, (page, total, count) => { onStatus(`采集进度: ${page}/${total} 页, 已采集 ${count} 条`); }); onStatus('采集完成,准备返回...'); await goBackToParentTask(); return collected; } document.getElementById('topic-start').addEventListener('click', async function() { if (isTopicCollecting) return; const selectedTasks = getSelectedTopicTasks(); if (selectedTasks.length === 0) { alert('请至少选择一个任务'); return; } isTopicCollecting = true; this.disabled = true; this.innerText = '批量采集中...'; document.getElementById('topic-download').style.display = 'none'; topicCollectorData = []; const statusEl = document.getElementById('topic-status'); const progressBar = document.getElementById('topic-progress-bar'); const progressText = document.getElementById('topic-progress-text'); const dataCount = document.getElementById('topic-data-count'); statusEl.innerText = '准备开始批量采集...'; statusEl.style.color = '#E6A23C'; let totalCollected = 0; let successCount = 0; let failCount = 0; for (let i = 0; i < selectedTasks.length; i++) { const taskName = selectedTasks[i]; const taskProgress = (i / selectedTasks.length) * 100; progressBar.style.width = `${taskProgress}%`; progressText.innerText = `进度: ${i}/${selectedTasks.length} 个任务`; statusEl.innerText = `[${i+1}/${selectedTasks.length}] 正在采集: ${taskName}`; addLog(`---------- 开始采集任务: ${taskName} ----------`, 'info'); try { const onStatus = (msg) => { statusEl.innerText = `[${i+1}/${selectedTasks.length}] ${taskName}: ${msg}`; }; const collected = await enterTaskAndCollect(taskName, onStatus); topicCollectorData.push({ taskName: taskName, data: collected }); totalCollected += collected.length; successCount++; addLog(`✅ 任务“${taskName}”采集完成,共 ${collected.length} 条数据`, 'success'); } catch (err) { failCount++; addLog(`❌ 任务“${taskName}”采集失败: ${err.message}`, 'error'); } // 任务之间等待一下 if (i < selectedTasks.length - 1) { await new Promise(r => setTimeout(r, 2000)); } } // 完成 progressBar.style.width = '100%'; progressText.innerText = `进度: ${selectedTasks.length}/${selectedTasks.length} 个任务`; statusEl.innerText = `批量采集完成!成功: ${successCount}, 失败: ${failCount}, 总数据: ${totalCollected} 条`; statusEl.style.color = successCount > 0 ? '#67C23A' : '#F56C6C'; dataCount.innerText = `已采集: ${totalCollected} 条 (来自 ${successCount} 个任务)`; if (totalCollected > 0) { document.getElementById('topic-download').style.display = 'block'; } this.disabled = false; this.innerText = '开始采集题ID/序列名'; isTopicCollecting = false; addLog(`🎉 批量采集完成!成功: ${successCount}, 失败: ${failCount}`, successCount > 0 ? 'success' : 'warn'); }); document.getElementById('topic-download').addEventListener('click', function(e) { e.preventDefault(); if (isTopicDownloading) return; if (!acquireGlobalDownloadLock('topic-download')) return; if (topicCollectorData.length === 0) { alert('没有数据可导出,请先执行采集'); return; } isTopicDownloading = true; const btn = this; btn.disabled = true; const headers = ['任务名称', '题ID', '序列名称', '标注工序', '审核工序', '质检工序', '验收工序']; let csvContent = "\uFEFF" + headers.join(',') + '\n'; const formatVal = (val) => { if (val === null || val === undefined) val = ''; val = String(val); if (val.includes(',') || val.includes('"') || val.includes('\n')) { return '"' + val.replace(/"/g, '""') + '"'; } return val; }; topicCollectorData.forEach(taskItem => { taskItem.data.forEach(item => { csvContent += `${formatVal(taskItem.taskName)},${formatVal(item.题ID)},${formatVal(item.序列名称)},${formatVal(item.标注工序)},${formatVal(item.审核工序)},${formatVal(item.质检工序)},${formatVal(item.验收工序)}\n`; }); }); const link = document.createElement('a'); const objectUrl = URL.createObjectURL(new Blob([csvContent], { type: 'text/csv;charset=utf-8;' })); link.href = objectUrl; const now = new Date(); const timestamp = `${now.getFullYear()}${(now.getMonth()+1).toString().padStart(2,'0')}${now.getDate().toString().padStart(2,'0')}_${now.getHours().toString().padStart(2,'0')}${now.getMinutes().toString().padStart(2,'0')}${now.getSeconds().toString().padStart(2,'0')}`; link.download = `题ID_序列名_批量_${timestamp}.csv`; document.body.appendChild(link); link.click(); document.body.removeChild(link); setTimeout(() => { URL.revokeObjectURL(objectUrl); btn.disabled = false; isTopicDownloading = false; }, 300); addLog(`📥 已导出 ${topicCollectorData.reduce((sum, t) => sum + t.data.length, 0)} 条数据,来自 ${topicCollectorData.length} 个任务`, 'success'); }); // ================= API 数据导出 ================= function apiLog(msg, type = 'info') { const el = document.getElementById('api-log'); if (!el) return; const colors = { info: '#cccccc', success: '#4EC9B0', error: '#F56C6C', warn: '#E6A23C' }; el.innerHTML += `
${new Date().toLocaleTimeString()} - ${msg}
`; el.scrollTop = el.scrollHeight; } // 采集任务列表(从当前页面Vue表格读取) document.getElementById('api-collect').addEventListener('click', function() { const tableData = getVueTableData(); if (!tableData || tableData.length === 0) { apiLog('⚠️ 未从页面获取到任务数据,请确认在当前项目列表页', 'warn'); return; } const container = document.getElementById('api-task-list'); if (!container) return; container.innerHTML = ''; tableData.forEach(item => { const name = item.task_name || ''; const key = item.task_key || ''; if (!key) return; const div = document.createElement('div'); div.style.marginBottom = '6px'; div.innerHTML = ` `; container.appendChild(div); }); apiLog('✅ 采集到 ' + tableData.length + ' 个任务(已自动全选)', 'success'); }); // 全选/清空 document.getElementById('api-select-all').addEventListener('click', () => { document.querySelectorAll('.api-task-cb').forEach(cb => cb.checked = true); }); document.getElementById('api-unselect-all').addEventListener('click', () => { document.querySelectorAll('.api-task-cb').forEach(cb => cb.checked = false); }); document.getElementById('api-test').addEventListener('click', async function() { const ak = document.getElementById('api-ak').value.trim(); if (!ak) { apiLog('请先填写 Access Key', 'error'); return; } const base = location.origin; apiLog('正在测试连接...', 'info'); try { const r = await fetch(base + '/v2/users/info', { method: 'POST', headers: { 'access-key': ak, 'Content-Type': 'application/json;charset=UTF-8' }, body: '{}' }); const t = await r.text(); if (t[0] === '<') { apiLog('❌ 返回了 HTML 页面(未登录或 access-key 错误)', 'error'); return; } const j = JSON.parse(t); if (j.code && j.code !== '') { apiLog('❌ ' + (j.msg || j.code), 'error'); } else { apiLog('✅ ' + j.data.nickname + ' (uid=' + j.data.user_id + ')', 'success'); } } catch(e) { apiLog('❌ 网络错误: ' + e.message, 'error'); } }); document.getElementById('api-start').addEventListener('click', async function() { const btn = this; btn.disabled = true; btn.textContent = '⏳ 执行中...'; const base = location.origin; const ak = document.getElementById('api-ak').value.trim(); const checked = document.querySelectorAll('.api-task-cb:checked'); const keys = Array.from(checked).map(cb => cb.dataset.key).filter(Boolean); if (!ak) { apiLog('请填写 Access Key', 'error'); btn.disabled = false; btn.textContent = '▶ 开始导出'; return; } if (keys.length === 0) { apiLog('请先勾选要导出的任务(点击「采集任务列表」获取)', 'error'); btn.disabled = false; btn.textContent = '▶ 开始导出'; return; } const bar = document.getElementById('api-bar'); const progText = document.getElementById('api-progress'); const statusEl = document.getElementById('api-status'); function h(ex) { return { 'access-key': ak, 'Accept': 'application/json, */*', 'Content-Type': 'application/json;charset=UTF-8', ...ex } } async function api(url, opt) { const r = await fetch(url, { ...opt, headers: { ...h(), ...(opt ? opt.headers : {}) } }); const t = await r.text(); if (t[0] === '<') throw new Error('服务器返回了 HTML 页面(可能未登录或 access-key 错误)'); const j = JSON.parse(t); if (j.code && j.code !== '') throw new Error(j.msg || j.code); return j; } let successCount = 0, failCount = 0; for (let ki = 0; ki < keys.length; ki++) { const TK = keys[ki]; const overallPct = (ki / keys.length) * 100; bar.style.width = overallPct + '%'; progText.textContent = `进度: ${ki}/${keys.length}`; statusEl.textContent = `处理中: ${TK}`; apiLog(`========== [${ki + 1}/${keys.length}] ${TK} ==========`, 'info'); try { // 1. 登录验证 apiLog('[1/5] 验证登录...', 'info'); await api(base + '/v2/users/info', { method: 'POST', body: '{}' }); apiLog(' ✓ 登录成功', 'success'); // 2. 统计 apiLog('[2/5] 获取任务统计...', 'info'); const c = await api(base + '/v2/reports/' + TK + '/check-info'); const s = c.data; apiLog(' ✓ 预期包数: ' + s.package_total_num + ' | 标注: ' + s.label_num + ' | 质检: ' + s.check_num, 'success'); // 3. 获取包列表 apiLog('[3/5] 获取包列表...', 'info'); let all = [], pg = 1, tot = 0; while (true) { const d = await api(base + '/v2/packages?task_batch_key=' + TK + '&work_type=4&page=' + pg + '&page_num=20&get_result_package=1'); const items = d.data.items || []; tot = d.data.meta ? d.data.meta.total_num || 0 : 0; all = all.concat(items); if (items.length < 20 || all.length >= tot) break; pg++; } apiLog(' ✓ ' + all.length + ' 个包', 'success'); // 获取推送时间 let pushTime = ''; if (all.length > 0) { try { const dsMatch = all[0].config_url.match(/\/default\/([^/]+)\//); const dsId = dsMatch ? dsMatch[1] : ''; if (dsId) { const pt = await api(base + '/v2/datasets/' + dsId + '/push-tasks?expand=user&dataset_id=' + dsId + '&page_num=50&page=1'); const pushes = (pt.data.items || []).filter(t => t.task_key === TK); if (pushes.length > 0) { const d = new Date(pushes[0].created_at * 1000); pushTime = d.getDate() + '/' + (d.getMonth() + 1) + '/' + d.getFullYear() + ' ' + String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0') + ':' + String(d.getSeconds()).padStart(2, '0'); } } } catch(e) { apiLog(' ⚠️ 推送时间获取失败: ' + e.message.substring(0, 50), 'warn'); } } // 4. 逐包获取 tasks apiLog('[4/5] 逐包获取 tasks(含真实 task_id)...', 'info'); const recs = []; for (let i = 0; i < all.length; i++) { const p = all[i]; try { const d = await api(base + '/v2/task-batches/' + TK + '/packages/' + p.package_id + '/tasks?task_id=&file_name=&seg_dir_name=&is_issues=&work_type=1&mark_status='); const items = d.data.items || []; const a = p.label_info ? p.label_info.user_id : null; const q = p.check_info ? p.check_info.user_id : null; for (const t of items) { const ts = parseFloat(t.file_name.replace('.pcd', '')); recs.push({ task_id: t.task_id, a, q, file_name: t.file_name, _t: ts }); } } catch(e) { apiLog(' ⚠️ 包 ' + p.package_id + ' 失败: ' + e.message.substring(0, 50), 'warn'); } // 更新子进度(50~85) const subPct = 50 + Math.round((i + 1) / all.length * 35); bar.style.width = (overallPct + (subPct / keys.length)) + '%'; } apiLog(' ✓ 累计 ' + recs.length + ' 条文件记录', 'success'); if (recs.length === 0) { apiLog('❌ 没有获取到任何文件记录', 'error'); failCount++; continue; } // 5. 整理导出 apiLog('[5/5] 整理数据并导出...', 'info'); recs.sort((a, b) => (a.a || 0) - (b.a || 0) || a._t - b._t); const out = recs.map(r => ({ task_id: r.task_id, annotator: r.a, annotation_qa: r.q, file_name: r.file_name, annotated_info: pushTime })); // 分组统计 const grp = {}; for (const r of out) { const k = r.annotator + '-' + r.annotation_qa; grp[k] = (grp[k] || 0) + 1; } let detail = ''; for (const [k, v] of Object.entries(grp)) { detail += ' annotator=' + k.split('-')[0] + ' qa=' + k.split('-')[1] + ' → ' + v + '条;'; } // 下载 const blob = new Blob([JSON.stringify(out, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = TK + '_api_export.json'; a.click(); URL.revokeObjectURL(url); successCount++; apiLog('✅ ' + TK + ' 导出完成: ' + out.length + ' 条 | ' + Object.keys(grp).length + ' 组 | ' + detail, 'success'); } catch(e) { failCount++; apiLog('❌ ' + TK + ' 失败: ' + e.message, 'error'); } } bar.style.width = '100%'; progText.textContent = `进度: ${keys.length}/${keys.length}`; statusEl.textContent = successCount > 0 ? '✅ 全部完成' : '❌ 全部失败'; statusEl.style.color = successCount > 0 ? '#67C23A' : '#F56C6C'; btn.disabled = false; btn.textContent = '▶ 开始导出'; apiLog(`🎉 全部完成: 成功 ${successCount}, 失败 ${failCount}`, successCount > 0 ? 'success' : 'warn'); }); // ================= 数据看板 - 刷新数据 ================= const viewRefreshBtn = document.getElementById('view-refresh'); if (viewRefreshBtn) { viewRefreshBtn.addEventListener('click', function() { renderViewList(); addLog('数据看板已刷新', 'info'); }); } // 数据看板 - 全选 const viewSelectAllBtn = document.getElementById('view-select-all'); if (viewSelectAllBtn) { viewSelectAllBtn.addEventListener('click', function() { viewSelectedTasks.clear(); collectAllData.forEach((item, idx) => { viewSelectedTasks.add(item.task_key || idx); }); renderViewList(); }); } // 数据看板 - 清空 const viewUnselectAllBtn = document.getElementById('view-unselect-all'); if (viewUnselectAllBtn) { viewUnselectAllBtn.addEventListener('click', function() { viewSelectedTasks.clear(); renderViewList(); }); } // ================= 10. 数据看板功能 ================= let viewSelectedTasks = new Set(); function safeNum(val) { const n = parseFloat(val); return isNaN(n) ? 0 : n; } function renderViewList() { const listEl = document.getElementById('view-list'); if (!listEl) return; if (!collectAllData || collectAllData.length === 0) { listEl.innerHTML = '
请先在“全量采集”标签页采集数据
'; updateViewStats(); return; } listEl.innerHTML = ''; collectAllData.forEach((item, idx) => { const isSelected = viewSelectedTasks.has(item.task_key || idx); const row = document.createElement('div'); row.style.cssText = 'display: grid; grid-template-columns: 30px 1fr 60px 60px 60px 60px 60px; gap: 4px; padding: 6px 4px; border-bottom: 1px dashed #ebeef5; align-items: center; font-size: 12px;'; row.innerHTML = `
${item.task_name || '-'}
${safeNum(item.total_num)}
${safeNum(item.label_progress)}
${safeNum(item.check_progress)}
${safeNum(item.inspect_progress)}
${safeNum(item.accept_progress)}
`; listEl.appendChild(row); }); // 绑定复选框事件 listEl.querySelectorAll('.view-task-check').forEach(cb => { cb.addEventListener('change', function() { const key = this.dataset.key; if (this.checked) { viewSelectedTasks.add(key); } else { viewSelectedTasks.delete(key); } updateViewStats(); }); }); updateViewStats(); } function updateViewStats() { const totalNumEl = document.getElementById('view-total-num'); const labelProgressEl = document.getElementById('view-label-progress'); const checkProgressEl = document.getElementById('view-check-progress'); const inspectProgressEl = document.getElementById('view-inspect-progress'); const acceptProgressEl = document.getElementById('view-accept-progress'); if (!totalNumEl) return; let totalNum = 0; let totalLabel = 0; let totalCheck = 0; let totalInspect = 0; let totalAccept = 0; collectAllData.forEach((item, idx) => { const key = item.task_key || idx; if (viewSelectedTasks.size === 0 || viewSelectedTasks.has(key)) { totalNum += safeNum(item.total_num); totalLabel += safeNum(item.label_progress); totalCheck += safeNum(item.check_progress); totalInspect += safeNum(item.inspect_progress); totalAccept += safeNum(item.accept_progress); } }); totalNumEl.textContent = totalNum; labelProgressEl.textContent = totalLabel; checkProgressEl.textContent = totalCheck; inspectProgressEl.textContent = totalInspect; acceptProgressEl.textContent = totalAccept; } // ================= 11. 初始化 ================= if (projects.length === 0) { projects.push({ id: 1, name: '默认项目', tasks: [], nextTaskId: 1 }); currentProjectId = 1; saveToStorage(); } renderProjectSelect(); updateProgress(); refreshTopicTaskCheckboxList(); window.addEventListener('beforeunload', saveToStorage); addLog('控制台已启动,新增“数据看板”标签页', 'success'); })();