// ==UserScript==
// @name 数据导出终极版
// @namespace https://docs.scriptcat.org/
// @version 1.0.0
// @description 融合备份导出、全量采集、用户绩效采集的一体化工具
// @author You
// @match *://*/*
// @grant none
// @noframes
// ==/UserScript==
(function() {
'use strict';
document.querySelectorAll('#ultimate-scraper-panel').forEach(panel => panel.remove());
// ================= 1. 本地存储 =================
const STORAGE_KEY = 'usc_projects_v4';
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 = [];
let currentProjectId = null;
let mappingData = [];
loadFromStorage();
function getCurrentProject() {
return projects.find(p => p.id === currentProjectId) || null;
}
// ================= 5. UI 面板 =================
const panelHTML = `
状态:等待开始
进度: 0/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 = '520px'; 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'); });
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');
for (const task of tasksToRun) {
let hasReturnedToParent = false; task.status = 'running'; renderTaskList(); document.getElementById('usc-status').innerText = '处理中';
addLog(`---------- 开始处理: ${task.name} ----------`, 'info');
try {
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));
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行数据匹配命中`, 'success'); const nameSpan = row.querySelector('td.organizationStyle span'); if (nameSpan) { await safeClick(nameSpan); entered = true; break; } } }
if (!entered) { for (const row of rows) { const nameSpan = row.querySelector('td.organizationStyle span'); const cellText = normalize(nameSpan ? nameSpan.textContent : ''); if (cellText === targetName) { await safeClick(nameSpan); entered = true; break; } } }
if (!entered && rows.length === 1) { const nameSpan = rows[0].querySelector('td.organizationStyle span'); if (nameSpan) { await safeClick(nameSpan); entered = true; } }
if (!entered) throw new Error('搜索结果中未找到该任务');
addLog('3. 等待详情页加载...', 'info'); try { await waitForElement('div#tab-0', 8000); } catch(e) { throw new Error('未成功进入任务详情页'); } await new Promise(r => setTimeout(r, 3000));
addLog('4. 点击导出按钮...', 'info'); await clickSpecificExportButton(); await new Promise(r => setTimeout(r, 2000));
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));
addLog('6. 确认导出...', 'info'); await clickButtonByText('导出', '.drawer__footer'); await new Promise(r => setTimeout(r, 2000)); await waitForLoadingToDisappear(); await new Promise(r => setTimeout(r, 2000));
addLog('7. 等待进入下载管理页面...', 'info'); try { await waitForElement('.el-table__header', 15000); } catch(e) { throw new Error('未检测到下载管理页面'); }
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(); if (status === '已完成') { packDone = true; break; } await new Promise(r => setTimeout(r, POLL_INTERVAL_MS)); }
if (!packDone) addLog('打包超时,尝试继续下载...', 'warn'); else addLog('打包已完成!', 'success'); await new Promise(r => setTimeout(r, 1000));
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, '') === '下载' && btn.style.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');
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]);
addLog('11. 返回上级...', 'info'); await goBackToParentTask(); hasReturnedToParent = true;
task.status = 'success'; 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-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 || !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); }); }
function updatePerfTaskSelect() { const select = document.getElementById('perf-task-select'); if (!select) return; select.innerHTML = ''; if (collectAllData.length === 0) { select.innerHTML = ''; return; } collectAllData.forEach(item => { const option = document.createElement('option'); option.value = item.task_name; option.textContent = item.task_name; select.appendChild(option); }); }
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'; updatePerfTaskSelect();
} 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. 用户绩效核心逻辑 =================
let perfIsRunning = false;
let perfCollectedData = [['用户账号', '用户昵称', '累计标签数', '人工标注标签数', '智能标注标签数', '题包数量', '角点属性', '车位', '车位线', '轮挡', '减速带']];
function perfAddLog(msg, type = 'info') { const colors = { info: '#cccccc', success: '#4EC9B0', error: '#F56C6C', warn: '#E6A23C' }; const logArea = document.getElementById('perf-log-area'); if(!logArea) return; logArea.innerHTML += `${new Date().toLocaleTimeString()} - ${msg}
`; logArea.scrollTop = logArea.scrollHeight; }
function getPureTagName(element) { let tagName = ''; element.childNodes.forEach(node => { if (node.nodeType === Node.TEXT_NODE) tagName += node.textContent; }); return tagName.replace(/\s+/g, ''); }
function collectCurrentPageData() { const rows = document.querySelectorAll('li.tBody'); const userData = {}; rows.forEach(row => { const spans = row.querySelectorAll('div.tSpan'); if (spans.length >= 4) { let tag = getPureTagName(spans[0]); const count = spans[1].innerText.trim(); if (tag.includes('情景标签')) tag = '题包数量'; if (tag.includes('无需标注')) return; userData[tag] = count; } }); return userData; }
function collectMainListData(account) { const rows = document.querySelectorAll('li.tBody'); const mainData = { '用户账号': account }; rows.forEach(row => { const accountSpan = row.querySelector('div.tSpan[style*="color: rgb(132, 94, 238)"]'); if (accountSpan && accountSpan.innerText.trim() === account) { const spans = row.querySelectorAll('div.tSpan'); const headers = document.querySelectorAll('li.tHead div.tSpan'); let nicknameIdx = -1, totalIdx = -1, manualIdx = -1, smartIdx = -1; headers.forEach((h, idx) => { const txt = h.innerText.trim(); if (txt === '用户昵称') nicknameIdx = idx; if (txt === '累计标签数') totalIdx = idx; if (txt === '人工标注标签数') manualIdx = idx; if (txt === '智能标注标签数') smartIdx = idx; }); if (nicknameIdx !== -1 && spans[nicknameIdx]) mainData['用户昵称'] = spans[nicknameIdx].innerText.trim(); if (totalIdx !== -1 && spans[totalIdx]) mainData['累计标签数'] = spans[totalIdx].innerText.trim(); if (manualIdx !== -1 && spans[manualIdx]) mainData['人工标注标签数'] = spans[manualIdx].innerText.trim(); if (smartIdx !== -1 && spans[smartIdx]) mainData['智能标注标签数'] = spans[smartIdx].innerText.trim(); } }); return mainData; }
function perfExportCSV(taskName) { const fileName = `${taskName}_用户绩效.csv`; perfAddLog(`正在导出文件: ${fileName}`, 'success'); let csvContent = ''; perfCollectedData.forEach(row => { csvContent += row.map(field => `"${field || '0'}"`).join(',') + '\n'; }); const BOM = '\uFEFF'; const blob = new Blob([BOM + csvContent], { type: 'text/csv;charset=utf-8;' }); const link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = fileName; link.click(); URL.revokeObjectURL(link.href); }
// 专门用于等待用户列表加载的辅助函数
async function waitForUserListLoad(timeout = 5000) {
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
const el = document.querySelector('li.tBody div.tSpan[style*="color: rgb(132, 94, 238)"]');
if (el) return true;
await new Promise(r => setTimeout(r, 200));
}
return false;
}
// 绩效采集主流程
document.getElementById('perf-start-btn').addEventListener('click', async function() {
if (perfIsRunning) return;
const selectedTask = document.getElementById('perf-task-select').value;
if (!selectedTask) return alert('请选择一个任务');
perfIsRunning = true; this.disabled = true; document.getElementById('perf-stop-btn').style.display = 'block';
document.getElementById('perf-status').innerText = '采集中'; document.getElementById('perf-status').style.color = '#845eee';
perfCollectedData = [['用户账号', '用户昵称', '累计标签数', '人工标注标签数', '智能标注标签数', '题包数量', '角点属性', '车位', '车位线', '轮挡', '减速带']];
try {
perfAddLog(`1. 搜索任务: ${selectedTask}`, 'info');
const searchInput = await waitForElement('.search_list input.el-input__inner');
setVueInputValue(searchInput, selectedTask);
await clickButtonByText('搜索', '.search_list');
await new Promise(r => setTimeout(r, 2500));
perfAddLog('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(selectedTask);
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'); if (nameSpan) { await safeClick(nameSpan); entered = true; break; } } }
if (!entered && rows.length === 1) { const nameSpan = rows[0].querySelector('td.organizationStyle span'); if (nameSpan) { await safeClick(nameSpan); entered = true; } }
if (!entered) throw new Error('未找到该任务,无法进入详情');
perfAddLog('3. 切换到"绩效管理"Tab...', 'info');
const perfTab = await waitForElement('#tab-sixed', 5000);
await safeClick(perfTab);
await new Promise(r => setTimeout(r, 1500));
perfAddLog('4. 切换到"用户绩效"子Tab...', 'info');
const userPerfLi = await waitForElement('ul.record_ul li:nth-child(2)', 5000);
await safeClick(userPerfLi);
await new Promise(r => setTimeout(r, 1500));
perfAddLog('5. 获取用户列表...', 'info');
// 使用精准等待,确保列表完全加载
const isListLoaded = await waitForUserListLoad();
if (!isListLoaded) throw new Error('用户列表加载超时');
const accountElements = document.querySelectorAll('li.tBody div.tSpan[style*="color: rgb(132, 94, 238)"]');
const perfUserAccounts = Array.from(accountElements).map(el => el.innerText.trim()).filter(Boolean);
if(perfUserAccounts.length === 0) throw new Error('未找到用户列表');
perfAddLog(`找到 ${perfUserAccounts.length} 个用户`, 'success');
for (let i = 0; i < perfUserAccounts.length; i++) {
if (!perfIsRunning) break;
const account = perfUserAccounts[i];
perfAddLog(`(${i + 1}/${perfUserAccounts.length}) 正在处理: ${account}`, 'info');
const percent = ((i / perfUserAccounts.length) * 100).toFixed(0) + '%';
document.getElementById('perf-progress-bar').style.width = percent;
document.getElementById('perf-progress-text').innerText = `进度: ${i}/${perfUserAccounts.length}`;
// 重新获取元素防止DOM刷新丢失引用,加入短暂重试
let targetEl = null;
let retryCount = 0;
while(!targetEl && retryCount < 5) {
const currentAccEls = document.querySelectorAll('li.tBody div.tSpan[style*="color: rgb(132, 94, 238)"]');
currentAccEls.forEach(el => { if (el.innerText.trim() === account) targetEl = el; });
if(!targetEl) await new Promise(r => setTimeout(r, 400)); // 等待0.4秒重试
retryCount++;
}
if (targetEl) {
const mainData = collectMainListData(account);
await safeClick(targetEl);
await waitForElement('li.tBody', 5000);
await new Promise(r => setTimeout(r, 1000));
const detailData = collectCurrentPageData();
const rowArray = [
mainData['用户账号'] || '', mainData['用户昵称'] || '',
mainData['累计标签数'] || '0', mainData['人工标注标签数'] || '0', mainData['智能标注标签数'] || '0',
detailData['题包数量'] || '0',
(Object.keys(detailData).find(k => k.includes('角点属性')) ? detailData[Object.keys(detailData).find(k => k.includes('角点属性'))] : '0'),
(Object.keys(detailData).find(k => k.includes('车位') && !k.includes('线')) ? detailData[Object.keys(detailData).find(k => k.includes('车位') && !k.includes('线'))] : '0'),
(Object.keys(detailData).find(k => k.includes('车位线')) ? detailData[Object.keys(detailData).find(k => k.includes('车位线'))] : '0'),
(Object.keys(detailData).find(k => k.includes('轮挡')) ? detailData[Object.keys(detailData).find(k => k.includes('轮挡'))] : '0'),
(Object.keys(detailData).find(k => k.includes('减速带')) ? detailData[Object.keys(detailData).find(k => k.includes('减速带'))] : '0')
];
perfCollectedData.push(rowArray);
// 返回用户绩效列表
history.back();
// 核心修复:使用精准等待,确保返回后用户行完全渲染
await waitForUserListLoad();
await new Promise(r => setTimeout(r, 500)); // 额外保险等待
} else {
perfAddLog(`未找到账号: ${account} (可能页面未完全加载)`, 'warn');
}
}
document.getElementById('perf-progress-bar').style.width = '100%';
document.getElementById('perf-progress-text').innerText = `进度: ${perfUserAccounts.length}/${perfUserAccounts.length}`;
document.getElementById('perf-status').innerText = '完成'; document.getElementById('perf-status').style.color = '#67C23A';
if (perfCollectedData.length > 1) perfExportCSV(selectedTask);
} catch (error) {
perfAddLog(`流程出错: ${error.message}`, 'error');
document.getElementById('perf-status').innerText = '出错'; document.getElementById('perf-status').style.color = '#F56C6C';
} finally {
this.disabled = false; document.getElementById('perf-stop-btn').style.display = 'none'; perfIsRunning = false;
}
});
document.getElementById('perf-stop-btn').addEventListener('click', function() { perfIsRunning = false; perfAddLog('已手动停止采集', 'warn'); });
// ================= 10. 初始化 =================
if (projects.length === 0) { projects.push({ id: 1, name: '默认项目', tasks: [], nextTaskId: 1 }); currentProjectId = 1; saveToStorage(); }
renderProjectSelect(); updateProgress();
window.addEventListener('beforeunload', saveToStorage);
addLog('控制台已启动', 'success');
})();