NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name 信息统计
// @namespace http://tampermonkey.net/
// @version 2025-10-23
// @description try to take over the world!
// @author You
// @match https://qlabel.tencent.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=tencent.com
// @updateURL https://openuserjs.org/meta/cuipengcheng/信息统计.meta.js
// @downloadURL https://openuserjs.org/install/cuipengcheng/信息统计.user.js
// @copyright 2025, cuipengcheng (https://openuserjs.org/users/cuipengcheng)
// @license MIT
// @grant none
// ==/UserScript==
(function () {
'use strict';
// 解析大整数JSON
function parseJSONWithBigInt(text) {
const bigIntFields = ['detail_id', 'task_id', 'check_id', 'id', 'deliver_id', 'dataset_item_id'];
let processedText = text;
bigIntFields.forEach(field => {
const regex = new RegExp(`"${field}"\\s*:\\s*(\\d{15,})`, 'g');
processedText = processedText.replace(regex, `"${field}": "$1"`);
});
return JSON.parse(processedText);
}
// 自定义 fetch 响应解析函数
async function parseResponse(response) {
const text = await response.text();
try {
return parseJSONWithBigInt(text);
} catch (error) {
console.error('JSON解析失败,尝试使用标准解析:', error);
return JSON.parse(text);
}
}
// 全局状态管理
window.statsDownloadState = {
isRunning: false,
isPaused: false,
currentTaskName: null,
searchType: 'name', // 搜索类型:'name' 或 'id'
selectedStatuses: ['验收通过'], // 选择的批次状态
statsData: [], // 存储所有统计数据(旧格式,保留兼容)
taskDataMap: {} // 按任务名称分组的数据:{ 任务名称: [数据行...] }
};
// 请求延迟配置(避免请求过于频繁)
window.requestDelay = {
// 随机延迟函数
random: (min, max) => {
return Math.floor(Math.random() * (max - min + 1)) + min;
},
// 任务查询延迟(1-2秒)
taskQuery: () => new Promise(resolve =>
setTimeout(resolve, window.requestDelay.random(1000, 2000))
),
// 批次列表请求延迟(800-1500ms)
deliverList: () => new Promise(resolve =>
setTimeout(resolve, window.requestDelay.random(800, 1500))
),
// 题目详情请求延迟(500-1000ms)
detailList: () => new Promise(resolve =>
setTimeout(resolve, window.requestDelay.random(500, 1000))
),
// 批次处理间延迟(1-2秒)
betweenDelivers: () => new Promise(resolve =>
setTimeout(resolve, window.requestDelay.random(1000, 2000))
),
// 任务处理间延迟(2-4秒)
betweenTasks: () => new Promise(resolve =>
setTimeout(resolve, window.requestDelay.random(2000, 4000))
)
};
// 查询任务信息(支持按任务名称或任务编号)
async function fetchTask(searchValue, searchType = 'name') {
const params = {
"page": {
"start": 0,
"size": 10,
"return_total": 1
},
"task_type": 0,
"need_stat_task": true,
"need_stat_deliver": true
};
// 根据搜索类型添加不同的参数
if (searchType === 'id') {
params.task_id = searchValue;
} else {
params.task_name = searchValue;
}
const requestBody = {
"jsonrpc": "2.0",
"method": "listFormalManagerTasks",
"id": Date.now(),
"params": params
};
try {
const response = await fetch("https://qlabel.tencent.com/api/workbench/listFormalManagerTasks", {
"headers": {
"accept": "application/json, text/plain, */*",
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8",
"content-type": "application/json",
"sec-ch-ua": "\"Not;A=Brand\";v=\"99\", \"Google Chrome\";v=\"139\", \"Chromium\";v=\"139\"",
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": "\"macOS\"",
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-origin",
"x-requested-with": "XMLHttpRequest"
},
"referrer": "https://qlabel.tencent.com/workbench/formal-tasks",
"body": JSON.stringify(requestBody),
"method": "POST",
"mode": "cors",
"credentials": "include"
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await parseResponse(response);
return data;
} catch (error) {
console.error('获取任务信息失败:', error);
throw error;
}
}
// 获取批次列表
async function fetchDeliverTasks(taskId) {
let allDelivers = [];
let start = 0;
const size = 10;
try {
while (true) {
const requestBody = {
"jsonrpc": "2.0",
"method": "listDeliverTasks",
"id": Date.now(),
"params": {
"task_id": taskId,
"page": {
"start": start,
"size": size,
"return_total": 1
},
"from": 0
}
};
const response = await fetch("https://qlabel.tencent.com/api/workbench/listDeliverTasks", {
"headers": {
"accept": "application/json, text/plain, */*",
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8",
"content-type": "application/json",
"sec-ch-ua": "\"Not;A=Brand\";v=\"99\", \"Google Chrome\";v=\"139\", \"Chromium\";v=\"139\"",
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": "\"macOS\"",
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-origin",
"x-requested-with": "XMLHttpRequest"
},
"referrer": `https://qlabel.tencent.com/workbench/formal-tasks/${taskId}?tab=delivers`,
"body": JSON.stringify(requestBody),
"method": "POST",
"mode": "cors",
"credentials": "include"
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await parseResponse(response);
if (!data.result || !data.result.data || data.result.data.length === 0) {
break;
}
allDelivers.push(...data.result.data);
if (allDelivers.length >= data.result.total) {
break;
}
start += size;
// 批次列表请求间延迟(800-1500ms)
await window.requestDelay.deliverList();
}
// 根据选择的状态过滤批次
const selectedStatuses = window.statsDownloadState.selectedStatuses || ['验收通过'];
const filteredDelivers = allDelivers.filter(deliver =>
selectedStatuses.includes(deliver.status_name)
);
console.log(`任务 ${taskId} 共 ${allDelivers.length} 个批次,筛选状态 [${selectedStatuses.join(', ')}] 共 ${filteredDelivers.length} 个`);
return filteredDelivers;
} catch (error) {
console.error('获取批次列表失败:', error);
throw error;
}
}
// 获取批次的题目详情列表
async function fetchLabelDeliverDetails(taskId, deliverId, cycleStep = 0) {
let allDetails = [];
let start = 0;
const size = 100; // 增大每次请求的数量
try {
while (true) {
const requestBody = {
"jsonrpc": "2.0",
"method": "listLabelDeliverDetails",
"id": Date.now(),
"params": {
"task_id": taskId,
"deliver_id": deliverId,
"page": {
"start": start,
"size": size,
"return_total": 1
},
"cycle_step": cycleStep
}
};
const response = await fetch("https://qlabel.tencent.com/api/workbench/listLabelDeliverDetails", {
"headers": {
"accept": "application/json, text/plain, */*",
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8",
"content-type": "application/json",
"sec-ch-ua": "\"Not;A=Brand\";v=\"99\", \"Google Chrome\";v=\"139\", \"Chromium\";v=\"139\"",
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": "\"macOS\"",
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-origin",
"x-requested-with": "XMLHttpRequest"
},
"referrer": `https://qlabel.tencent.com/workbench/formal-tasks/${taskId}?tab=delivers&deliver_id=${deliverId}&type=detail`,
"body": JSON.stringify(requestBody),
"method": "POST",
"mode": "cors",
"credentials": "include"
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await parseResponse(response);
if (!data.result || !data.result.data || data.result.data.length === 0) {
break;
}
allDetails.push(...data.result.data);
if (allDetails.length >= data.result.total) {
break;
}
start += size;
// 题目详情请求间延迟(500-1000ms)
await window.requestDelay.detailList();
}
return allDetails;
} catch (error) {
console.error('获取题目详情列表失败:', error);
throw error;
}
}
// 处理单个任务的统计
async function processTaskStats(searchValue, statusElement, searchType = 'name') {
try {
const searchLabel = searchType === 'id' ? '任务编号' : '任务名称';
statusElement.textContent = `正在查询任务 (${searchLabel}): ${searchValue}...`;
// 1. 查询任务信息(添加延迟)
await window.requestDelay.taskQuery();
const taskResponse = await fetchTask(searchValue, searchType);
if (!taskResponse.result || !taskResponse.result.data || taskResponse.result.data.length === 0) {
console.warn(`未找到任务 (${searchLabel}): ${searchValue}`);
statusElement.textContent = `未找到任务 (${searchLabel}): ${searchValue}`;
return;
}
const task = taskResponse.result.data[0];
const taskId = task.task_id;
const fullTaskName = task.task_name;
console.log(`找到任务: ${fullTaskName} (ID: ${taskId})`);
statusElement.textContent = `正在获取批次列表: ${fullTaskName}...`;
// 2. 获取批次列表(根据选择的状态筛选)
const delivers = await fetchDeliverTasks(taskId);
if (delivers.length === 0) {
const statusText = window.statsDownloadState.selectedStatuses.join('、');
console.warn(`任务 ${fullTaskName} 没有符合状态 [${statusText}] 的批次`);
statusElement.textContent = `任务 ${fullTaskName} 没有符合状态 [${statusText}] 的批次`;
return;
}
const statusText = window.statsDownloadState.selectedStatuses.join('、');
console.log(`任务 ${fullTaskName} 共 ${delivers.length} 个符合状态 [${statusText}] 的批次`);
// 3. 遍历每个批次,获取题目详情
for (let i = 0; i < delivers.length; i++) {
if (!window.statsDownloadState.isRunning) {
break;
}
// 检查暂停状态
while (window.statsDownloadState.isPaused && window.statsDownloadState.isRunning) {
statusElement.textContent = `已暂停 - ${fullTaskName} (${i + 1}/${delivers.length})`;
await new Promise(resolve => setTimeout(resolve, 1000));
}
if (!window.statsDownloadState.isRunning) {
break;
}
const deliver = delivers[i];
const deliverId = deliver.deliver_id;
const deliverName = deliver.deliver_name;
// 获取批次的质检环节(从 deliver_task_stat.cycle_step_name)
const cycleStepName = deliver.deliver_task_stat?.cycle_step_name || '';
statusElement.textContent = `处理批次 ${i + 1}/${delivers.length}: ${deliverName}...`;
console.log(`正在处理批次: ${deliverName} (${i + 1}/${delivers.length})`);
// 获取题目详情列表
const details = await fetchLabelDeliverDetails(taskId, deliverId);
console.log(`批次 ${deliverName} 共 ${details.length} 个题目`);
// 4. 统计数据
for (const detail of details) {
// 新格式:不包含任务名称列
const statsRow = {
批次名称: deliverName,
任务明细ID: detail.detail_id ? detail.detail_id.toString() : '',
题目ID: detail.dataset_item_id ? detail.dataset_item_id.toString() : '',
题包Key: detail.pack_key || '',
是否有效: detail.detail_is_valid === 1 ? '有效' : (detail.detail_is_valid || ''),
标注员: detail.labeler_nick_name || '',
质检环节: cycleStepName, // 使用批次的 cycle_step_name
质检员: detail.reviewer_name || '',
质检结果: detail.check_status === 50 ? '质检通过' : (detail.check_status === 60 ? '修改通过' : (detail.check_status || ''))
};
// 按任务名称分组存储
if (!window.statsDownloadState.taskDataMap[fullTaskName]) {
window.statsDownloadState.taskDataMap[fullTaskName] = [];
}
window.statsDownloadState.taskDataMap[fullTaskName].push(statsRow);
// 同时保留旧格式(包含任务名称列)用于兼容
window.statsDownloadState.statsData.push({
任务名称: fullTaskName,
...statsRow
});
}
statusElement.textContent = `已处理 ${i + 1}/${delivers.length} 个批次,共 ${window.statsDownloadState.statsData.length} 条数据`;
// 批次处理间延迟(1-2秒)
if (i < delivers.length - 1) {
await window.requestDelay.betweenDelivers();
}
}
console.log(`任务 ${fullTaskName} 统计完成,共 ${window.statsDownloadState.statsData.length} 条数据`);
} catch (error) {
const searchLabel = searchType === 'id' ? '任务编号' : '任务名称';
console.error(`处理任务失败 (${searchLabel}: ${searchValue}):`, error);
statusElement.textContent = `处理任务失败 (${searchLabel}: ${searchValue}): ${error.message}`;
throw error;
}
}
// 开始统计
async function startStatsDownload(searchValues, searchType = 'name') {
window.statsDownloadState.isRunning = true;
window.statsDownloadState.isPaused = false;
window.statsDownloadState.statsData = [];
window.statsDownloadState.taskDataMap = {}; // 重置任务数据映射
window.statsDownloadState.searchType = searchType;
const statusElement = document.getElementById('stats-status');
const startBtn = document.getElementById('stats-start-btn');
const pauseBtn = document.getElementById('stats-pause-btn');
const stopBtn = document.getElementById('stats-stop-btn');
const exportBtn = document.getElementById('stats-export-btn');
startBtn.disabled = true;
exportBtn.disabled = true;
const searchLabel = searchType === 'id' ? '任务编号' : '任务名称';
try {
for (let i = 0; i < searchValues.length; i++) {
if (!window.statsDownloadState.isRunning) {
break;
}
const searchValue = searchValues[i].trim();
if (!searchValue) continue;
console.log(`开始处理 (${searchLabel}) ${i + 1}/${searchValues.length}: ${searchValue}`);
window.statsDownloadState.currentTaskName = searchValue;
await processTaskStats(searchValue, statusElement, searchType);
// 任务处理间延迟(2-4秒)
if (i < searchValues.length - 1 && window.statsDownloadState.isRunning) {
await window.requestDelay.betweenTasks();
}
}
if (window.statsDownloadState.isRunning) {
statusElement.textContent = `✅ 统计完成!共 ${window.statsDownloadState.statsData.length} 条数据`;
console.log(`所有任务统计完成,共 ${window.statsDownloadState.statsData.length} 条数据`);
exportBtn.disabled = false;
} else {
statusElement.textContent = `已停止 - 共 ${window.statsDownloadState.statsData.length} 条数据`;
}
} catch (error) {
console.error('统计过程中发生错误:', error);
statusElement.textContent = `❌ 统计失败: ${error.message}`;
} finally {
window.statsDownloadState.isRunning = false;
window.statsDownloadState.isPaused = false;
startBtn.disabled = false;
pauseBtn.textContent = '暂停';
if (window.statsDownloadState.statsData.length > 0) {
exportBtn.disabled = false;
}
}
}
// 导出为Excel
async function exportToExcel() {
if (Object.keys(window.statsDownloadState.taskDataMap).length === 0) {
alert('没有数据可导出');
return;
}
try {
// 动态加载 SheetJS 库
if (!window.XLSX) {
const statusElement = document.getElementById('stats-status');
statusElement.textContent = '正在加载 Excel 库...';
await loadSheetJSLibrary();
statusElement.textContent = 'Excel 库加载完成,正在生成文件...';
}
// 创建工作簿
const wb = window.XLSX.utils.book_new();
// 设置列宽(不包含任务名称列)
const colWidths = [
{ wch: 40 }, // 批次名称
{ wch: 20 }, // 任务明细ID
{ wch: 20 }, // 题目ID
{ wch: 35 }, // 题包Key
{ wch: 10 }, // 是否有效
{ wch: 15 }, // 标注员
{ wch: 20 }, // 质检环节
{ wch: 15 }, // 质检员
{ wch: 12 } // 质检结果
];
// 为每个任务创建一个sheet
let sheetIndex = 1;
for (const [taskName, taskData] of Object.entries(window.statsDownloadState.taskDataMap)) {
if (taskData.length === 0) continue;
// 将数据转换为工作表
const ws = window.XLSX.utils.json_to_sheet(taskData);
ws['!cols'] = colWidths;
// 生成sheet名称(Excel sheet名称限制:最长31个字符,不能包含特殊字符)
let sheetName = taskName;
// 替换Excel不支持的字符
sheetName = sheetName.replace(/[\[\]:*?\/\\]/g, '_');
// 限制长度
if (sheetName.length > 31) {
sheetName = sheetName.substring(0, 28) + '...';
}
// 如果sheet名称重复,添加序号
let finalSheetName = sheetName;
let suffix = 1;
while (wb.SheetNames.includes(finalSheetName)) {
finalSheetName = sheetName.substring(0, 28) + `_${suffix}`;
suffix++;
}
// 添加工作表到工作簿
window.XLSX.utils.book_append_sheet(wb, ws, finalSheetName);
console.log(`添加Sheet: ${finalSheetName}, 数据行数: ${taskData.length}`);
sheetIndex++;
}
// 生成文件名
const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-');
const fileName = `标注统计数据_${timestamp}.xlsx`;
// 导出文件
window.XLSX.writeFile(wb, fileName);
const statusElement = document.getElementById('stats-status');
const totalSheets = Object.keys(window.statsDownloadState.taskDataMap).length;
const totalRows = Object.values(window.statsDownloadState.taskDataMap).reduce((sum, data) => sum + data.length, 0);
statusElement.textContent = `✅ 导出成功!${totalSheets}个任务,共${totalRows}条数据`;
console.log(`Excel文件已导出: ${fileName}, ${totalSheets}个Sheet, 共${totalRows}条数据`);
} catch (error) {
console.error('导出Excel失败:', error);
alert('导出Excel失败: ' + error.message);
}
}
// 加载 SheetJS 库
function loadSheetJSLibrary() {
return new Promise((resolve, reject) => {
if (window.XLSX) {
resolve();
return;
}
const script = document.createElement('script');
script.src = 'https://cdn.sheetjs.com/xlsx-0.20.0/package/dist/xlsx.full.min.js';
script.onload = () => {
console.log('SheetJS 库加载成功');
resolve();
};
script.onerror = () => {
reject(new Error('SheetJS 库加载失败'));
};
document.head.appendChild(script);
});
}
// 创建控制面板
function createControlPanel() {
// 如果已存在,先移除
const existing = document.getElementById('stats-control-panel');
if (existing) {
existing.remove();
}
// 创建控制球
const controlBall = document.createElement('div');
controlBall.id = 'stats-control-ball';
controlBall.textContent = '📊';
controlBall.style.cssText = `
position: fixed;
bottom: 80px;
right: 30px;
width: 60px;
height: 60px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
font-size: 28px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
z-index: 10000;
transition: all 0.3s ease;
`;
controlBall.addEventListener('mouseenter', () => {
controlBall.style.transform = 'scale(1.1)';
});
controlBall.addEventListener('mouseleave', () => {
controlBall.style.transform = 'scale(1)';
});
// 创建功能窗口
const panel = document.createElement('div');
panel.id = 'stats-control-panel';
panel.style.cssText = `
position: fixed;
bottom: 150px;
right: 30px;
width: 400px;
background: white;
border: 1px solid #ddd;
border-radius: 12px;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.2);
z-index: 10001;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 14px;
display: none;
`;
panel.innerHTML = `
<div style="padding: 16px; border-bottom: 1px solid #eee;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
<h3 style="margin: 0; font-size: 18px; color: #333;">📊 统计信息下载</h3>
<button id="stats-close-panel" style="
background: none;
border: none;
font-size: 20px;
cursor: pointer;
color: #999;
padding: 0;
width: 24px;
height: 24px;
line-height: 24px;
">×</button>
</div>
<div style="margin-bottom: 12px;">
<label style="display: block; margin-bottom: 6px; color: #666; font-size: 13px;">
搜索方式:
</label>
<div style="display: flex; gap: 16px; padding: 8px 0;">
<label style="display: flex; align-items: center; cursor: pointer; font-size: 13px;">
<input type="radio" name="search-type" value="name" checked style="margin-right: 4px;">
<span>任务名称</span>
</label>
<label style="display: flex; align-items: center; cursor: pointer; font-size: 13px;">
<input type="radio" name="search-type" value="id" style="margin-right: 4px;">
<span>任务编号</span>
</label>
</div>
</div>
<div style="margin-bottom: 12px;">
<label style="display: block; margin-bottom: 6px; color: #666; font-size: 13px;">
批次状态筛选:
</label>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; padding: 8px; background: #f9fafb; border-radius: 6px;">
<label style="display: flex; align-items: center; cursor: pointer; font-size: 12px;">
<input type="checkbox" name="status-filter" value="验收通过" checked style="margin-right: 4px;">
<span>验收通过</span>
</label>
<label style="display: flex; align-items: center; cursor: pointer; font-size: 12px;">
<input type="checkbox" name="status-filter" value="待验收" style="margin-right: 4px;">
<span>待验收</span>
</label>
<label style="display: flex; align-items: center; cursor: pointer; font-size: 12px;">
<input type="checkbox" name="status-filter" value="待交付" style="margin-right: 4px;">
<span>待交付</span>
</label>
<label style="display: flex; align-items: center; cursor: pointer; font-size: 12px;">
<input type="checkbox" name="status-filter" value="验收驳回" style="margin-right: 4px;">
<span>验收驳回</span>
</label>
<label style="display: flex; align-items: center; cursor: pointer; font-size: 12px;">
<input type="checkbox" name="status-filter" value="待审核" style="margin-right: 4px;">
<span>待审核</span>
</label>
</div>
</div>
<div style="margin-bottom: 12px;">
<label style="display: block; margin-bottom: 6px; color: #666; font-size: 13px;">
<span id="stats-input-label">任务名称</span>(多个任务可用逗号、分号、空格或换行分隔):
</label>
<textarea id="stats-task-names" style="
width: 100%;
height: 80px;
padding: 8px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 13px;
resize: vertical;
box-sizing: border-box;
" placeholder="支持多种分隔:任务1,任务2 或 任务1;任务2 或换行输入"></textarea>
</div>
<div style="margin-bottom: 12px;">
<div id="stats-status" style="
padding: 8px 12px;
background: #f5f5f5;
border-radius: 6px;
font-size: 12px;
color: #666;
min-height: 20px;
">待开始...</div>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px; margin-bottom: 12px;">
<button id="stats-start-btn" style="
padding: 10px;
background: #667eea;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
font-weight: 500;
">开始统计</button>
<button id="stats-pause-btn" style="
padding: 10px;
background: #f59e0b;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
font-weight: 500;
">暂停</button>
<button id="stats-stop-btn" style="
padding: 10px;
background: #ef4444;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
font-weight: 500;
">停止</button>
</div>
<button id="stats-export-btn" disabled style="
width: 100%;
padding: 12px;
background: #10b981;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
">导出Excel</button>
<div style="margin-top: 12px; padding: 8px; background: #fef3c7; border-radius: 6px; font-size: 11px; color: #92400e; line-height: 1.4;">
💡 提示:支持按任务名称或任务编号搜索,可多选批次状态(验收通过、待验收、待交付、验收驳回、待审核)。统计数据包括任务明细ID、题目ID、题包Key、是否有效、标注员、质检环节、质检员、质检结果等信息。
</div>
</div>
`;
document.body.appendChild(controlBall);
document.body.appendChild(panel);
// 绑定事件
controlBall.addEventListener('click', () => {
panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
});
document.getElementById('stats-close-panel').addEventListener('click', () => {
panel.style.display = 'none';
});
// 搜索类型切换事件
const searchTypeRadios = document.querySelectorAll('input[name="search-type"]');
searchTypeRadios.forEach(radio => {
radio.addEventListener('change', (e) => {
const searchType = e.target.value;
const inputLabel = document.getElementById('stats-input-label');
const textarea = document.getElementById('stats-task-names');
if (searchType === 'id') {
inputLabel.textContent = '任务编号';
textarea.placeholder = '支持多种分隔:1234567890,0987654321 或换行输入';
} else {
inputLabel.textContent = '任务名称';
textarea.placeholder = '支持多种分隔:任务1,任务2 或 任务1;任务2 或换行输入';
}
});
});
document.getElementById('stats-start-btn').addEventListener('click', () => {
const searchValuesText = document.getElementById('stats-task-names').value.trim();
const searchType = document.querySelector('input[name="search-type"]:checked').value;
const searchLabel = searchType === 'id' ? '任务编号' : '任务名称';
if (!searchValuesText) {
alert(`请输入${searchLabel}`);
return;
}
// 获取选中的状态
const selectedStatuses = Array.from(document.querySelectorAll('input[name="status-filter"]:checked'))
.map(checkbox => checkbox.value);
if (selectedStatuses.length === 0) {
alert('请至少选择一个批次状态');
return;
}
// 更新全局状态
window.statsDownloadState.selectedStatuses = selectedStatuses;
// 支持多种分隔符:中英文逗号、中英文分号、空格、换行等
const searchValues = searchValuesText
.split(/[,,;;\s]+/) // 匹配逗号、分号、空格、换行等
.map(value => value.trim())
.filter(value => value);
if (searchValues.length === 0) {
alert(`请输入有效的${searchLabel}`);
return;
}
console.log(`开始统计 ${searchValues.length} 个任务 (${searchLabel}):`, searchValues);
console.log(`筛选状态: [${selectedStatuses.join(', ')}]`);
startStatsDownload(searchValues, searchType);
});
document.getElementById('stats-pause-btn').addEventListener('click', () => {
const btn = document.getElementById('stats-pause-btn');
window.statsDownloadState.isPaused = !window.statsDownloadState.isPaused;
btn.textContent = window.statsDownloadState.isPaused ? '恢复' : '暂停';
btn.style.background = window.statsDownloadState.isPaused ? '#10b981' : '#f59e0b';
});
document.getElementById('stats-stop-btn').addEventListener('click', () => {
window.statsDownloadState.isRunning = false;
window.statsDownloadState.isPaused = false;
const pauseBtn = document.getElementById('stats-pause-btn');
pauseBtn.textContent = '暂停';
pauseBtn.style.background = '#f59e0b';
console.log('统计已停止');
});
document.getElementById('stats-export-btn').addEventListener('click', () => {
exportToExcel();
});
// 禁用导出按钮样式
const exportBtn = document.getElementById('stats-export-btn');
const updateExportBtnStyle = () => {
if (exportBtn.disabled) {
exportBtn.style.background = '#d1d5db';
exportBtn.style.cursor = 'not-allowed';
} else {
exportBtn.style.background = '#10b981';
exportBtn.style.cursor = 'pointer';
}
};
const observer = new MutationObserver(updateExportBtnStyle);
observer.observe(exportBtn, { attributes: true, attributeFilter: ['disabled'] });
console.log('✅ 统计信息下载控制面板已创建');
}
// 初始化
function initStatsDownload() {
console.log('🚀 初始化统计信息下载功能...');
// 等待页面完全加载
setTimeout(() => {
createControlPanel();
console.log('✅ 统计信息下载功能初始化完成');
}, 2000);
}
// 页面加载完成后初始化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initStatsDownload);
} else {
initStatsDownload();
}
})();