// ==UserScript== // @name 数据导出 // @namespace https://docs.scriptcat.org/ // @version 0.2.0 // @description try to take over the world! // @author You // @match http://*/* // @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 = `
备份任务 全量采集
×
任务列表
进度: 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 => { tab.addEventListener('click', function() { document.querySelectorAll('.usc-tab').forEach(t => t.classList.remove('active')); this.classList.add('active'); document.querySelectorAll('.usc-tab-content').forEach(content => { content.style.display = content.id === `tab-${this.dataset.tab}` ? 'block' : 'none'; }); }); }); // 最小化/还原 let isMinimized = false; minimizeBtn.addEventListener('click', () => { if (isMinimized) { bodyDiv.style.display = 'block'; panel.style.width = '480px'; 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); // 方案A:通过 Vue 行组件数据精确匹配(绕过 DOM 文本 CSS 截断问题) for (const row of rows) { let rowName = ''; try { // Element UI 的 el-table__row 组件持有 row 数据 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; } } } // 方案B:通过表格 Vue 数据匹配行索引 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'); } } // 方案C:textContent 匹配(innerText 受 CSS 截断影响,textContent 返回完整文本) if (!entered) { addLog('Vue数据匹配未命中,尝试 textContent 匹配...', 'warn'); for (const row of rows) { const nameSpan = row.querySelector('td.organizationStyle span'); const nameTd = row.querySelector('td.organizationStyle'); const cellText = normalize(nameSpan ? nameSpan.textContent : nameTd ? nameTd.textContent : ''); addLog(`行textContent: "${cellText}"`, 'info'); if (cellText === targetName) { addLog(`textContent精确匹配命中`, 'success'); if (nameSpan) { await safeClick(nameSpan); entered = true; break; } if (nameTd) { await safeClick(nameTd); entered = true; break; } } } } // 方案D:尝试点击行内可点击元素,利用搜索结果唯一性(搜索精确名称时通常只返回一行) 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. 等待详情页加载 + 3秒 addLog('3. 等待详情页加载 + 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. 等待 2 分钟 addLog('8. 等待打包(120秒)...', 'info'); await new Promise(r => setTimeout(r, 120000)); // 9. 点击第一个可用下载按钮 addLog('9. 查找下载按钮...', 'info'); let downloadBtn = null; let attempt = 0; while (attempt < 10 && !downloadBtn) { const allBtns = document.querySelectorAll('.el-table__body-wrapper tbody button'); for (const btn of allBtns) { if (btn.innerText.replace(/\s+/g, '') === '下载') { const color = btn.style.color; if (color === 'rgb(73, 109, 255)' || color === '') { downloadBtn = btn; break; } } } 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'; } 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. 初始化 ================= if (projects.length === 0) { // 自动创建默认项目 projects.push({ id: 1, name: '默认项目', tasks: [], nextTaskId: 1 }); currentProjectId = 1; saveToStorage(); } renderProjectSelect(); updateProgress(); // 页面关闭前保存 window.addEventListener('beforeunload', saveToStorage); addLog('控制台已启动', 'success'); })();