// ==UserScript== // @name 任务导出 // @namespace https://docs.scriptcat.org/ // @version 0.1.0 // @description try to take over the world! // @author You // @match http://*/* // @grant none // @noframes // ==/UserScript== (function() { // 防止重复注入 if (document.getElementById('custom-scraper-panel')) { document.getElementById('custom-scraper-panel').remove(); } // 1. 创建悬浮面板 UI const panelHTML = `
🚀 全量数据采集器 ×
状态: 等待开始
进度: 0/0 已采集: 0 条
`; document.body.insertAdjacentHTML('beforeend', panelHTML); // 面板拖拽逻辑 const panel = document.getElementById('custom-scraper-panel'); const header = panel.querySelector('div[style*="cursor: move"]'); let isDragging = false, startX, startY, initialLeft, initialTop; header.addEventListener('mousedown', (e) => { isDragging = true; startX = e.clientX; startY = e.clientY; initialLeft = panel.offsetLeft; initialTop = panel.offsetTop; }); document.addEventListener('mousemove', (e) => { if (!isDragging) return; panel.style.left = initialLeft + e.clientX - startX + 'px'; panel.style.top = initialTop + e.clientY - startY + 'px'; panel.style.right = 'auto'; panel.style.bottom = 'auto'; }); document.addEventListener('mouseup', () => isDragging = false); // 关闭按钮 document.getElementById('scraper-close').addEventListener('click', () => panel.remove()); // 2. 核心逻辑变量 let allData = []; let isCollecting = false; // 获取 Vue 实例中的数据 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 clickAndWait(element) { return new Promise((resolve) => { element.click(); // 给予 1.5 秒等待接口返回和 DOM 渲染 setTimeout(resolve, 1500); }); } // 3. 采集主流程 document.getElementById('scraper-start').addEventListener('click', async function() { if (isCollecting) return; isCollecting = true; allData = []; this.disabled = true; this.style.background = '#a0cfff'; this.innerText = '正在采集中...'; document.getElementById('scraper-download').style.display = 'none'; const statusEl = document.getElementById('scraper-status'); const progressBar = document.getElementById('scraper-progress-bar'); const progressText = document.getElementById('scraper-progress-text'); const dataCount = document.getElementById('scraper-data-count'); statusEl.innerText = '准备翻至第一页...'; statusEl.style.color = '#E6A23C'; // 获取总条数计算总页数 const totalText = document.querySelector('.el-pagination__total').innerText; const totalItems = parseInt(totalText.match(/\d+/)[0]); const pageSize = 20; // 默认每页条数,通常 Element UI 是 20 const totalPages = Math.ceil(totalItems / pageSize) || 1; // 先点击第一页确保从头开始 const firstPageBtn = document.querySelector('.el-pager li.number'); if (firstPageBtn) { await clickAndWait(firstPageBtn); } statusEl.innerText = '正在采集...'; statusEl.style.color = '#409EFF'; for (let currentPage = 1; currentPage <= totalPages; currentPage++) { // 1. 抓取当前页数据 const pageData = getVueTableData(); if (pageData.length > 0) { allData = allData.concat(pageData); } // 更新 UI const progressPercent = (currentPage / totalPages * 100).toFixed(0) + '%'; progressBar.style.width = progressPercent; progressText.innerText = `进度: ${currentPage}/${totalPages}`; dataCount.innerText = `已采集: ${allData.length} 条`; // 2. 尝试点击下一页 const nextBtn = document.querySelector('.el-pagination .btn-next:not([disabled])'); if (nextBtn && currentPage < totalPages) { await clickAndWait(nextBtn); } else { break; // 没有下一页了,退出循环 } } // 4. 采集完成,去重处理 (以 task_key 为唯一标识) const uniqueDataMap = new Map(); allData.forEach(item => { if (item.task_key && !uniqueDataMap.has(item.task_key)) { uniqueDataMap.set(item.task_key, item); } }); allData = Array.from(uniqueDataMap.values()); statusEl.innerText = '采集完成!'; statusEl.style.color = '#67C23A'; dataCount.innerText = `去重后: ${allData.length} 条`; this.disabled = false; this.style.background = '#409EFF'; this.innerText = '重新采集'; document.getElementById('scraper-download').style.display = 'block'; isCollecting = false; }); // 5. 导出 CSV 逻辑 document.getElementById('scraper-download').addEventListener('click', function() { if (allData.length === 0) return alert('没有数据可导出!'); 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; }; allData.forEach(item => { let 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'); link.href = URL.createObjectURL(new Blob([csvContent], { type: 'text/csv;charset=utf-8;' })); link.download = '全量完整任务数据.csv'; document.body.appendChild(link); link.click(); document.body.removeChild(link); }); })();