// ==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() {
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
已采集: 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');
})();