// ==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);
});
})();