// ==UserScript== // @name 标注助手 - 框数统计 // @namespace https://dawnchan.online // @version 1.0.3 // @description 自动累加标注框数,支持手动累加和自动遍历,可自定义等待时间、帧数,面板可拖动/缩放。任务模式支持多任务管理,显示变化量及记录时间,自动跳转第一帧。 // @author cc // @match *://*/*pointcloud* // @match *://*/w/pointcloud/* // @grant none // @require https://cdn.sheetjs.com/xlsx-0.20.0/package/dist/xlsx.full.min.js // ==/UserScript== (function() { 'use strict'; console.log('标注助手脚本已加载 v1.0.3'); // ========== 配置 ========== const STORAGE_KEY_SINGLE = 'daily_box_total'; const STORAGE_KEY_TASKS = 'annotation_tasks'; const STORAGE_KEY_SETTINGS = 'annotation_settings'; const DEFAULT_WAIT_MS = 100; const DEFAULT_MAX_FRAMES = 81; // 全局变量 let stopAutoFlag = false; let currentMode = 'single'; // 'single' 或 'task' let currentTaskId = ''; let autoSaveInterval = null; // 自动保存定时器 let lastSaveTime = null; // 上次保存时间 // ========== 单次模式存储 ========== function loadSingleTotal() { let val = localStorage.getItem(STORAGE_KEY_SINGLE); return val ? parseInt(val, 10) : 0; } function saveSingleTotal(total) { localStorage.setItem(STORAGE_KEY_SINGLE, total); } function addToSingleTotal(frameTotal) { let running = loadSingleTotal(); running += frameTotal; saveSingleTotal(running); return running; } function resetSingleTotal() { localStorage.removeItem(STORAGE_KEY_SINGLE); } // ========== 任务模式存储 ========== function loadTasks() { let tasks = localStorage.getItem(STORAGE_KEY_TASKS); return tasks ? JSON.parse(tasks) : {}; } function saveTasks(tasks) { localStorage.setItem(STORAGE_KEY_TASKS, JSON.stringify(tasks)); } function getCurrentTask() { if (!currentTaskId) return null; const tasks = loadTasks(); return tasks[currentTaskId] || null; } function getAllTaskIds() { const tasks = loadTasks(); return Object.keys(tasks); } function updateTaskTotal(taskId, newTotal, startTime = '', endTime = '') { const tasks = loadTasks(); let task = tasks[taskId]; const now = new Date().toISOString(); let change = newTotal; let lastTotal = 0; if (task) { lastTotal = task.totalBoxes; change = newTotal - lastTotal; task.totalBoxes = newTotal; task.lastUpdate = now; if (startTime) task.startTime = startTime; if (endTime) task.endTime = endTime; if (!task.history) task.history = []; task.history.unshift({ total: newTotal, timestamp: now, change: change }); if (task.history.length > 10) task.history.pop(); } else { task = { id: taskId, name: taskId, // 任务名称 totalBoxes: newTotal, startTime: startTime || '', endTime: endTime || '', lastUpdate: now, history: [{ total: newTotal, timestamp: now, change: newTotal }], calendar: {} // 新增:日历记录 }; tasks[taskId] = task; } saveTasks(tasks); return { lastTotal, change }; } // 新增:添加日历记录 function addCalendarRecord(taskId, date, period, boxes, note = '') { const tasks = loadTasks(); if (!tasks[taskId]) return false; if (!tasks[taskId].calendar) { tasks[taskId].calendar = {}; } if (!tasks[taskId].calendar[date]) { tasks[taskId].calendar[date] = {}; } tasks[taskId].calendar[date][period] = { boxes: parseInt(boxes, 10), note: note, timestamp: new Date().toISOString() }; saveTasks(tasks); return true; } // 新增:获取日历记录 function getCalendarRecords(taskId) { const tasks = loadTasks(); if (!tasks[taskId] || !tasks[taskId].calendar) { return {}; } return tasks[taskId].calendar; } // 新增:删除日历记录 function deleteCalendarRecord(taskId, date, period) { const tasks = loadTasks(); if (!tasks[taskId] || !tasks[taskId].calendar || !tasks[taskId].calendar[date]) { return false; } delete tasks[taskId].calendar[date][period]; // 如果该日期没有记录了,删除日期键 if (Object.keys(tasks[taskId].calendar[date]).length === 0) { delete tasks[taskId].calendar[date]; } saveTasks(tasks); return true; } // 新增:获取时段配置 function getTimePeriods() { const periods = localStorage.getItem('time_periods'); return periods ? JSON.parse(periods) : { morning: { name: '上午', start: '08:00', end: '12:00' }, afternoon: { name: '下午', start: '14:00', end: '18:00' }, allday: { name: '全天', start: '08:00', end: '18:00' } }; } // 新增:保存时段配置 function saveTimePeriods(periods) { localStorage.setItem('time_periods', JSON.stringify(periods)); } function deleteTask(taskId) { const tasks = loadTasks(); delete tasks[taskId]; saveTasks(tasks); if (currentTaskId === taskId) { currentTaskId = ''; const taskSelect = document.getElementById('task-select'); if (taskSelect) taskSelect.value = ''; updateTaskDisplay(); } } // 辅助函数:判断当前是否为第一帧 function isFirstFrame() { // 方法1:检查页码文本 const currentPageElem = document.querySelector('#pages .page-select'); if (currentPageElem && currentPageElem.textContent.trim() === '1') { return true; } // 方法2:检查进度文本 "1/81 " const progressText = document.getElementById('page-progress-text'); if (progressText && progressText.textContent.trim().startsWith('1/')) { return true; } return false; } // 辅助函数:跳转到第一帧 async function jumpToFirstFrame(waitMs) { const firstPage = document.querySelector('#pages p.page:first-child'); if (!firstPage) { console.warn('未找到第一帧页码元素'); return false; } if (isFirstFrame()) { console.log('已经是第一帧,无需跳转'); return true; } firstPage.click(); // 等待页面加载 await new Promise(resolve => setTimeout(resolve, waitMs)); // 可选:等待进度条更新或页码变化 let retries = 0; while (!isFirstFrame() && retries < 5) { await new Promise(resolve => setTimeout(resolve, 500)); retries++; } return isFirstFrame(); } // ========== 获取当前帧框数 ========== function getCurrentFrameTotal() { let frameTotal = 0; const elements = document.querySelectorAll('.left-seg-num'); elements.forEach(el => { let match = el.innerText.match(/\(\s*(\d+)\s*\)/); if (match) frameTotal += parseInt(match[1], 10); }); return frameTotal; } // ========== 单次模式手动累加 ========== function handleSingleAddFrame() { const frameTotal = getCurrentFrameTotal(); const newTotal = addToSingleTotal(frameTotal); appendLog(`累加当前帧: ${frameTotal} 框 | 单次累计: ${newTotal}`); updateSingleDisplay(); } // ========== 自动遍历 ========== async function autoCountAllFrames(waitMs, maxFrames) { const nextBtn = document.querySelector('#page-actions button:last-child'); if (!nextBtn) { alert('未找到 "下一帧"按钮,请检查页面结构'); return; } waitMs = parseInt(waitMs, 10); if (isNaN(waitMs) || waitMs <= 0) waitMs = DEFAULT_WAIT_MS; maxFrames = parseInt(maxFrames, 10); if (isNaN(maxFrames) || maxFrames <= 0) maxFrames = Infinity; // 可中断的等待函数 const waitWithCheck = async (ms) => { const checkInterval = 50; // 每50ms检查一次停止标志 const chunks = Math.ceil(ms / checkInterval); for (let i = 0; i < chunks; i++) { if (stopAutoFlag) return true; // 返回true表示被中断 await new Promise(resolve => setTimeout(resolve, checkInterval)); } return false; // 返回false表示正常完成 }; if (currentMode === 'single') { // 单次模式:逐帧累加,显示每帧细节 if (!confirm(`将自动统计最多 ${maxFrames === Infinity ? '全部' : maxFrames} 帧,每帧等待 ${waitMs} 毫秒。\n模式: 单次累计\n请确保当前是第一帧且数字已加载。\n确定开始吗?`)) return; stopAutoFlag = false; // 重置停止标志 appendLog(`开始自动统计,模式: 单次,等待 ${waitMs}ms,最多 ${maxFrames === Infinity ? '不限' : maxFrames} 帧`); let countedFrames = 0; // 先累加当前帧 let frameTotal = getCurrentFrameTotal(); let newTotal = addToSingleTotal(frameTotal); countedFrames++; appendLog(`帧 ${countedFrames}: ${frameTotal} 框 | 单次累计: ${newTotal}`); updateSingleDisplay(); // 循环后续帧 while (countedFrames < maxFrames) { if (stopAutoFlag) { appendLog('用户手动停止了自动遍历'); alert('已停止自动遍历'); return; } if (nextBtn.disabled || nextBtn.classList.contains('is-disabled')) { appendLog(`已到达最后一帧,共统计 ${countedFrames} 帧`); alert(`自动统计完成!共统计 ${countedFrames} 帧,单次累计总数: ${loadSingleTotal()}`); return; } nextBtn.click(); const interrupted = await waitWithCheck(waitMs); if (interrupted) { appendLog('用户手动停止了自动遍历'); alert('已停止自动遍历'); return; } frameTotal = getCurrentFrameTotal(); newTotal = addToSingleTotal(frameTotal); countedFrames++; appendLog(`帧 ${countedFrames}: ${frameTotal} 框 | 单次累计: ${newTotal}`); updateSingleDisplay(); } appendLog(`已达到设定的最大帧数 ${maxFrames},自动停止`); alert(`已达到最大帧数 ${maxFrames},当前单次累计: ${loadSingleTotal()}`); } else if (currentMode === 'task') { // 任务模式:先跳转到第一帧,然后统计整个任务的总框数 if (!currentTaskId) { alert('请先选择或创建任务'); return; } // 检查是否需要跳转第一帧 if (!isFirstFrame()) { const doJump = confirm('当前不是第一帧,是否跳转到第一帧开始统计?\n(选择 "取消"则从当前帧开始统计)'); if (doJump) { appendLog('正在跳转到第一帧...'); const jumped = await jumpToFirstFrame(waitMs); if (!jumped) { alert('跳转到第一帧失败,请手动切换到第一帧后重试'); return; } appendLog('已跳转到第一帧'); } else { appendLog('用户选择不从第一帧开始,将从当前帧开始统计'); } } else { appendLog('当前已是第一帧,开始统计'); } if (!confirm(`将自动统计任务 [${currentTaskId}] 的总框数,从当前帧开始到最后一帧,每帧等待 ${waitMs} 毫秒。\n请确保当前是第一帧且数字已加载。\n确定开始吗?`)) return; stopAutoFlag = false; // 重置停止标志 appendLog(`开始统计任务 [${currentTaskId}] 总框数,等待 ${waitMs}ms`); let totalBoxes = 0; let countedFrames = 0; // 先累加当前帧 totalBoxes += getCurrentFrameTotal(); countedFrames++; // 循环后续帧 while (countedFrames < maxFrames) { // 添加停止检查 if (stopAutoFlag) { appendLog('用户手动停止了自动遍历'); alert('已停止自动遍历'); return; } if (nextBtn.disabled || nextBtn.classList.contains('is-disabled')) { appendLog(`已到达最后一帧,共统计 ${countedFrames} 帧`); break; } nextBtn.click(); const interrupted = await waitWithCheck(waitMs); if (interrupted) { appendLog('用户手动停止了自动遍历'); alert('已停止自动遍历'); return; } totalBoxes += getCurrentFrameTotal(); countedFrames++; if (countedFrames >= maxFrames) break; } // 获取起止时间 const startTime = document.getElementById('task-start-time')?.value || ''; const endTime = document.getElementById('task-end-time')?.value || ''; const { lastTotal, change } = updateTaskTotal(currentTaskId, totalBoxes, startTime, endTime); appendLog(`任务 [${currentTaskId}] 总框数: ${totalBoxes} | 上次记录: ${lastTotal} | 变化: ${change >= 0 ? '+' : ''}${change}`); updateTaskDisplay(); alert(`任务 [${currentTaskId}] 统计完成!\n总框数: ${totalBoxes}\n较上次${change >= 0 ? '增加' : '减少'}: ${Math.abs(change)}`); populateTaskSelect(); } } // 手动输入任务总框数 function manualUpdateTaskTotal() { if (!currentTaskId) { alert('请先选择或创建任务'); return; } const input = prompt(`请输入任务 [${currentTaskId}] 的总框数:`); if (input === null) return; const newTotal = parseInt(input, 10); if (isNaN(newTotal)) { alert('请输入有效的数字'); return; } const startTime = document.getElementById('task-start-time')?.value || ''; const endTime = document.getElementById('task-end-time')?.value || ''; const { lastTotal, change } = updateTaskTotal(currentTaskId, newTotal, startTime, endTime); appendLog(`手动更新任务 [${currentTaskId}] 总框数: ${newTotal} | 上次: ${lastTotal} | 变化: ${change >= 0 ? '+' : ''}${change}`); updateTaskDisplay(); alert(`任务 [${currentTaskId}] 总框数已更新为 ${newTotal},较上次${change >= 0 ? '增加' : '减少'} ${Math.abs(change)}`); populateTaskSelect(); } // ========== 重置 ========== function resetCurrentMode() { if (currentMode === 'single') { if (confirm('确定要重置单次累计数吗?')) { resetSingleTotal(); updateSingleDisplay(); appendLog('单次累计已重置为0'); } } else { if (!currentTaskId) { alert('未选择任务'); return; } if (confirm(`确定要删除任务 [${currentTaskId}] 吗?`)) { deleteTask(currentTaskId); appendLog(`任务 [${currentTaskId}] 已删除`); currentTaskId = ''; const taskSelect = document.getElementById('task-select'); if (taskSelect) taskSelect.value = ''; const startTimeInput = document.getElementById('task-start-time'); const endTimeInput = document.getElementById('task-end-time'); if (startTimeInput) startTimeInput.value = ''; if (endTimeInput) endTimeInput.value = ''; updateTaskDisplay(); populateTaskSelect(); } } } // ========== UI 更新 ========== function updateSingleDisplay() { const totalSpan = document.getElementById('box-total-display'); if (totalSpan) totalSpan.textContent = loadSingleTotal(); } function updateTaskDisplay() { const task = getCurrentTask(); const totalSpan = document.getElementById('task-total-display'); const lastSpan = document.getElementById('task-last-display'); const changeSpan = document.getElementById('task-change-display'); const timeSpan = document.getElementById('task-time-display'); if (totalSpan) totalSpan.textContent = task ? task.totalBoxes : '0'; if (lastSpan) lastSpan.textContent = task && task.history && task.history[1] ? task.history[1].total : '-'; if (changeSpan) { if (task && task.history && task.history[0]) { const change = task.history[0].change; changeSpan.textContent = (change >= 0 ? '+' : '') + change; changeSpan.style.color = change >= 0 ? '#2ecc71' : '#e74c3c'; } else { changeSpan.textContent = '0'; } } if (timeSpan && task) { const lastUpdate = task.lastUpdate ? new Date(task.lastUpdate).toLocaleString() : '-'; timeSpan.textContent = lastUpdate; } } function populateTaskSelect() { const select = document.getElementById('task-select'); if (!select) return; const tasks = loadTasks(); const currentId = currentTaskId; select.innerHTML = ''; for (const id of Object.keys(tasks).sort()) { const option = document.createElement('option'); option.value = id; option.textContent = `${id} (${tasks[id].totalBoxes}框)`; if (id === currentId) option.selected = true; select.appendChild(option); } } function onTaskSelectChange() { const select = document.getElementById('task-select'); const newId = select.value; if (!newId) { currentTaskId = ''; updateTaskDisplay(); return; } currentTaskId = newId; const task = getCurrentTask(); if (task) { if (task.startTime) document.getElementById('task-start-time').value = task.startTime.slice(0,16); if (task.endTime) document.getElementById('task-end-time').value = task.endTime.slice(0,16); } else { document.getElementById('task-start-time').value = ''; document.getElementById('task-end-time').value = ''; } updateTaskDisplay(); appendLog(`切换到任务: ${currentTaskId}`); } function createNewTask() { const newId = prompt('请输入新任务 ID(例如 TASK-001):'); if (!newId || newId.trim() === '') return; const trimmedId = newId.trim(); const tasks = loadTasks(); if (tasks[trimmedId]) { alert('任务ID已存在,请使用其他ID'); return; } // 获取当前时间 const now = new Date(); const startTime = now.toISOString().slice(0, 16); // 格式化为 datetime-local 输入框需要的格式 // 更新任务信息 updateTaskTotal(trimmedId, 0, startTime, ''); // 设置任务名称 const updatedTasks = loadTasks(); tasks[trimmedId].name = trimmedId; saveTasks(updatedTasks); populateTaskSelect(); const select = document.getElementById('task-select'); if (select) { select.value = trimmedId; onTaskSelectChange(); } appendLog(`创建新任务: ${trimmedId}`); } // ========== 日志 ========== function appendLog(msg) { const logArea = document.getElementById('stat-log'); const now = new Date(); const timeStr = `${now.getHours().toString().padStart(2,'0')}:${now.getMinutes().toString().padStart(2,'0')}:${now.getSeconds().toString().padStart(2,'0')}`; const formattedMsg = `[${timeStr}] ${msg}`; if (logArea) { const entry = document.createElement('div'); entry.textContent = formattedMsg; entry.style.borderBottom = '1px solid #444'; entry.style.padding = '2px 0'; logArea.appendChild(entry); logArea.scrollTop = logArea.scrollHeight; } else { console.log(formattedMsg); } } function clearLog() { const logArea = document.getElementById('stat-log'); if (logArea) logArea.innerHTML = ''; } // ========== 模式切换 ========== function setMode(mode) { currentMode = mode; const singlePanel = document.getElementById('single-panel'); const taskPanel = document.getElementById('task-panel'); const singleModeBtn = document.getElementById('single-mode-btn'); const taskModeBtn = document.getElementById('task-mode-btn'); if (mode === 'single') { if (singlePanel) singlePanel.style.display = 'block'; if (taskPanel) taskPanel.style.display = 'none'; if (singleModeBtn) { singleModeBtn.style.background = 'rgba(52, 152, 219, 0.3)'; singleModeBtn.style.color = '#87ceeb'; } if (taskModeBtn) { taskModeBtn.style.background = 'transparent'; taskModeBtn.style.color = '#ecf0f1'; } updateSingleDisplay(); } else { if (singlePanel) singlePanel.style.display = 'none'; if (taskPanel) taskPanel.style.display = 'block'; if (taskModeBtn) { taskModeBtn.style.background = 'rgba(230, 126, 34, 0.3)'; taskModeBtn.style.color = '#87ceeb'; } if (singleModeBtn) { singleModeBtn.style.background = 'transparent'; singleModeBtn.style.color = '#ecf0f1'; } populateTaskSelect(); updateTaskDisplay(); } } // ========== 可拖动 & 可缩放面板 ========== function makeDraggableAndResizable(panel) { let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0; const header = panel.querySelector('.panel-header'); if (!header) return; header.style.cursor = 'move'; header.onmousedown = dragMouseDown; function dragMouseDown(e) { e.preventDefault(); pos3 = e.clientX; pos4 = e.clientY; document.onmouseup = closeDragElement; document.onmousemove = elementDrag; } function elementDrag(e) { e.preventDefault(); pos1 = pos3 - e.clientX; pos2 = pos4 - e.clientY; pos3 = e.clientX; pos4 = e.clientY; let top = panel.offsetTop - pos2; let left = panel.offsetLeft - pos1; top = Math.min(Math.max(0, top), window.innerHeight - panel.offsetHeight); left = Math.min(Math.max(0, left), window.innerWidth - panel.offsetWidth); panel.style.top = top + 'px'; panel.style.left = left + 'px'; panel.style.bottom = 'auto'; panel.style.right = 'auto'; } function closeDragElement() { document.onmouseup = null; document.onmousemove = null; } const resizeHandle = document.createElement('div'); resizeHandle.style.cssText = ` position: absolute; bottom: 0; right: 0; width: 15px; height: 15px; cursor: nw-resize; background: #7f8c8d; border-radius: 2px 0 2px 0; `; panel.appendChild(resizeHandle); let startX, startY, startWidth, startHeight; resizeHandle.onmousedown = function(e) { e.preventDefault(); startX = e.clientX; startY = e.clientY; startWidth = panel.offsetWidth; startHeight = panel.offsetHeight; document.onmousemove = resize; document.onmouseup = stopResize; }; function resize(e) { let newWidth = startWidth + (e.clientX - startX); let newHeight = startHeight + (e.clientY - startY); newWidth = Math.min(Math.max(260, newWidth), 600); newHeight = Math.min(Math.max(280, newHeight), 600); panel.style.width = newWidth + 'px'; panel.style.height = newHeight + 'px'; } function stopResize() { document.onmousemove = null; document.onmouseup = null; } } // ========== 任务详情弹窗 ========== function showTaskDetail() { if (!currentTaskId) { alert('请先选择任务'); return; } const task = getCurrentTask(); if (!task) { alert('任务不存在'); return; } // 创建详情弹窗 const detailModal = document.createElement('div'); detailModal.id = 'task-detail-modal'; detailModal.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 9999999; display: flex; align-items: center; justify-content: center; `; // 创建弹窗内容 const modalContent = document.createElement('div'); modalContent.style.cssText = ` background: #303030; color: #ecf0f1; border-radius: 8px; padding: 20px; width: 80%; max-width: 900px; max-height: 90vh; overflow-y: auto; box-shadow: 0 4px 20px rgba(0,0,0,0.3); `; // 弹窗头部 const modalHeader = document.createElement('div'); modalHeader.style.cssText = ` display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; padding-bottom: 10px; border-bottom: 1px solid #555; `; const modalTitle = document.createElement('h2'); modalTitle.textContent = `任务详情: ${task.name}`; modalTitle.style.margin = '0'; const closeBtn = document.createElement('button'); closeBtn.innerHTML = '×'; closeBtn.style.cssText = ` background: transparent; border: none; color: #ecf0f1; font-size: 24px; cursor: pointer; line-height: 1; `; closeBtn.onclick = () => { document.body.removeChild(detailModal); }; modalHeader.appendChild(modalTitle); modalHeader.appendChild(closeBtn); modalContent.appendChild(modalHeader); // 任务基本信息 const basicInfo = document.createElement('div'); basicInfo.style.cssText = ` margin-bottom: 20px; padding: 15px; background: #1e2a36; border-radius: 6px; `; basicInfo.innerHTML = `
任务ID: ${task.id}
创建时间: ${new Date(task.history[task.history.length-1].timestamp).toLocaleString()}
开始时间: ${task.startTime ? new Date(task.startTime).toLocaleString() : '-'}
当前总框数: ${task.totalBoxes}
最后更新: ${new Date(task.lastUpdate).toLocaleString()}
`; modalContent.appendChild(basicInfo); // 操作记录 const historySection = document.createElement('div'); historySection.style.marginBottom = '20px'; const historyTitle = document.createElement('h3'); historyTitle.textContent = '操作记录'; historyTitle.style.marginBottom = '10px'; historySection.appendChild(historyTitle); const historyTable = document.createElement('table'); historyTable.style.cssText = ` width: 100%; border-collapse: collapse; margin-bottom: 10px; `; historyTable.innerHTML = ` 时间 操作类型 变化量 总框数 ${task.history.map(item => ` ${new Date(item.timestamp).toLocaleString()} ${item.change === item.total ? '创建任务' : '更新框数'} ${item.change >= 0 ? '+' : ''}${item.change} ${item.total} `).join('')} `; historySection.appendChild(historyTable); modalContent.appendChild(historySection); // 日历部分 const calendarSection = document.createElement('div'); calendarSection.style.marginBottom = '20px'; const calendarTitle = document.createElement('h3'); calendarTitle.textContent = '日历记录'; calendarTitle.style.marginBottom = '10px'; calendarSection.appendChild(calendarTitle); // 创建日历容器 const calendarContainer = document.createElement('div'); calendarContainer.id = 'calendar-container'; calendarContainer.style.cssText = ` display: flex; gap: 20px; `; // 创建日历视图 const calendarView = document.createElement('div'); calendarView.style.flex = '2'; calendarView.style.border = '1px solid #555'; calendarView.style.borderRadius = '6px'; calendarView.style.padding = '15px'; // 创建日历记录编辑区 const calendarEdit = document.createElement('div'); calendarEdit.style.flex = '1'; calendarEdit.style.border = '1px solid #555'; calendarEdit.style.borderRadius = '6px'; calendarEdit.style.padding = '15px'; calendarContainer.appendChild(calendarView); calendarContainer.appendChild(calendarEdit); calendarSection.appendChild(calendarContainer); modalContent.appendChild(calendarSection); // 导出按钮 const exportSection = document.createElement('div'); exportSection.style.textAlign = 'right'; const exportBtn = document.createElement('button'); exportBtn.textContent = '导出数据'; exportBtn.style.cssText = ` background: #3498db; border: none; color: #fff; padding: 8px 16px; border-radius: 4px; cursor: pointer; margin-right: 10px; `; exportBtn.onclick = () => exportTaskData(task); const exportCsvBtn = document.createElement('button'); exportCsvBtn.textContent = '导出CSV'; exportCsvBtn.style.cssText = ` background: #2ecc71; border: none; color: #fff; padding: 8px 16px; border-radius: 4px; cursor: pointer; `; exportCsvBtn.onclick = () => exportTaskData(task, 'csv'); exportSection.appendChild(exportBtn); exportSection.appendChild(exportCsvBtn); modalContent.appendChild(exportSection); detailModal.appendChild(modalContent); document.body.appendChild(detailModal); // 初始化日历 initCalendar(task); } // ========== 初始化日历 ========== function initCalendar(task) { const calendarView = document.querySelector('#calendar-container > div:first-child'); const calendarEdit = document.querySelector('#calendar-container > div:last-child'); // 获取当前日期 const now = new Date(); const currentYear = now.getFullYear(); const currentMonth = now.getMonth(); // 获取时段配置 const timePeriods = getTimePeriods(); // 创建日历导航 const navDiv = document.createElement('div'); navDiv.style.cssText = ` display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; `; const prevMonthBtn = document.createElement('button'); prevMonthBtn.textContent = '<'; prevMonthBtn.style.cssText = ` background: transparent; border: 1px solid #555; color: #ecf0f1; width: 30px; height: 30px; border-radius: 4px; cursor: pointer; `; const monthTitle = document.createElement('span'); monthTitle.textContent = `${currentYear}年${currentMonth + 1}月`; monthTitle.style.fontSize = '16px'; monthTitle.style.fontWeight = 'bold'; const nextMonthBtn = document.createElement('button'); nextMonthBtn.textContent = '>'; nextMonthBtn.style.cssText = ` background: transparent; border: 1px solid #555; color: #ecf0f1; width: 30px; height: 30px; border-radius: 4px; cursor: pointer; `; navDiv.appendChild(prevMonthBtn); navDiv.appendChild(monthTitle); navDiv.appendChild(nextMonthBtn); calendarView.appendChild(navDiv); // 创建星期标题 const weekDays = ['日', '一', '二', '三', '四', '五', '六']; const weekHeader = document.createElement('div'); weekHeader.style.cssText = ` display: grid; grid-template-columns: repeat(7, 1fr); gap: 5px; margin-bottom: 10px; `; weekDays.forEach(day => { const dayDiv = document.createElement('div'); dayDiv.textContent = day; dayDiv.style.textAlign = 'center'; dayDiv.style.fontWeight = 'bold'; weekHeader.appendChild(dayDiv); }); calendarView.appendChild(weekHeader); // 创建日历网格 const calendarGrid = document.createElement('div'); calendarGrid.style.cssText = ` display: grid; grid-template-columns: repeat(7, 1fr); gap: 5px; `; // 获取当月第一天是星期几 const firstDay = new Date(currentYear, currentMonth, 1).getDay(); // 获取当月天数 const daysInMonth = new Date(currentYear, currentMonth + 1, 0).getDate(); // 添加空白格子 for (let i = 0; i < firstDay; i++) { const emptyCell = document.createElement('div'); emptyCell.style.height = '40px'; calendarGrid.appendChild(emptyCell); } // 获取日历记录 const calendarRecords = getCalendarRecords(currentTaskId); // 添加日期格子 for (let day = 1; day <= daysInMonth; day++) { const dateStr = `${currentYear}-${String(currentMonth + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`; const dayCell = document.createElement('div'); dayCell.textContent = day; dayCell.style.cssText = ` height: 40px; display: flex; align-items: center; justify-content: center; border-radius: 4px; cursor: pointer; transition: background 0.2s; `; // 检查该日期是否有记录 if (calendarRecords[dateStr]) { const periods = Object.keys(calendarRecords[dateStr]); if (periods.length === 3) { // 全天有记录 dayCell.style.background = '#f39c12'; } else if (periods.includes('morning') && periods.includes('afternoon')) { // 上午和下午都有记录 dayCell.style.background = '#2ecc71'; } else if (periods.includes('morning')) { // 只有上午有记录 dayCell.style.background = '#27ae60'; } else if (periods.includes('afternoon')) { // 只有下午有记录 dayCell.style.background = '#82e0aa'; } } dayCell.onmouseover = () => { if (dayCell.style.background === '' || dayCell.style.background === 'transparent') { dayCell.style.background = '#444'; } }; dayCell.onmouseout = () => { if (!calendarRecords[dateStr]) { dayCell.style.background = ''; } }; dayCell.onclick = () => { showCalendarEdit(dateStr, calendarRecords[dateStr] || {}); }; calendarGrid.appendChild(dayCell); } calendarView.appendChild(calendarGrid); // 创建时段设置按钮 const periodSettingsBtn = document.createElement('button'); periodSettingsBtn.textContent = '设置时段'; periodSettingsBtn.style.cssText = ` background: #f39c12; border: none; color: #fff; padding: 8px 16px; border-radius: 4px; cursor: pointer; margin-top: 15px; width: 100%; `; periodSettingsBtn.onclick = () => showPeriodSettings(); calendarEdit.appendChild(periodSettingsBtn); // 创建日历记录编辑区 const editTitle = document.createElement('h4'); editTitle.textContent = '记录编辑'; editTitle.style.marginBottom = '10px'; calendarEdit.appendChild(editTitle); const editContent = document.createElement('div'); editContent.id = 'calendar-edit-content'; editContent.innerHTML = '

点击日历中的日期进行编辑

'; calendarEdit.appendChild(editContent); // 月份切换功能 let displayMonth = currentMonth; let displayYear = currentYear; prevMonthBtn.onclick = () => { displayMonth--; if (displayMonth < 0) { displayMonth = 11; displayYear--; } monthTitle.textContent = `${displayYear}年${displayMonth + 1}月`; updateCalendarGrid(displayYear, displayMonth, calendarGrid, calendarRecords); }; nextMonthBtn.onclick = () => { displayMonth++; if (displayMonth > 11) { displayMonth = 0; displayYear++; } monthTitle.textContent = `${displayYear}年${displayMonth + 1}月`; updateCalendarGrid(displayYear, displayMonth, calendarGrid, calendarRecords); }; } // ========== 更新日历网格 ========== function updateCalendarGrid(year, month, calendarGrid, calendarRecords) { // 清空日历网格 calendarGrid.innerHTML = ''; // 获取当月第一天是星期几 const firstDay = new Date(year, month, 1).getDay(); // 获取当月天数 const daysInMonth = new Date(year, month + 1, 0).getDate(); // 添加空白格子 for (let i = 0; i < firstDay; i++) { const emptyCell = document.createElement('div'); emptyCell.style.height = '40px'; calendarGrid.appendChild(emptyCell); } // 添加日期格子 for (let day = 1; day <= daysInMonth; day++) { const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`; const dayCell = document.createElement('div'); dayCell.textContent = day; dayCell.style.cssText = ` height: 40px; display: flex; align-items: center; justify-content: center; border-radius: 4px; cursor: pointer; transition: background 0.2s; `; // 检查该日期是否有记录 if (calendarRecords[dateStr]) { const periods = Object.keys(calendarRecords[dateStr]); if (periods.length === 3) { // 全天有记录 dayCell.style.background = '#f39c12'; } else if (periods.includes('morning') && periods.includes('afternoon')) { // 上午和下午都有记录 dayCell.style.background = '#2ecc71'; } else if (periods.includes('morning')) { // 只有上午有记录 dayCell.style.background = '#27ae60'; } else if (periods.includes('afternoon')) { // 只有下午有记录 dayCell.style.background = '#82e0aa'; } } dayCell.onmouseover = () => { if (dayCell.style.background === '' || dayCell.style.background === 'transparent') { dayCell.style.background = '#444'; } }; dayCell.onmouseout = () => { if (!calendarRecords[dateStr]) { dayCell.style.background = ''; } }; dayCell.onclick = () => { showCalendarEdit(dateStr, calendarRecords[dateStr] || {}); }; calendarGrid.appendChild(dayCell); } } // ========== 显示日历记录编辑 ========== function showCalendarEdit(dateStr, records) { const editContent = document.getElementById('calendar-edit-content'); const timePeriods = getTimePeriods(); // 创建编辑表单 const editForm = document.createElement('div'); const dateTitle = document.createElement('h4'); dateTitle.textContent = `编辑记录: ${dateStr}`; dateTitle.style.marginBottom = '10px'; editForm.appendChild(dateTitle); // 为每个时段创建输入框 Object.keys(timePeriods).forEach(periodKey => { const period = timePeriods[periodKey]; const periodDiv = document.createElement('div'); periodDiv.style.marginBottom = '15px'; const periodLabel = document.createElement('label'); periodLabel.textContent = `${period.name} (${period.start}-${period.end}):`; periodLabel.style.display = 'block'; periodLabel.style.marginBottom = '5px'; periodDiv.appendChild(periodLabel); const periodInput = document.createElement('input'); periodInput.type = 'number'; periodInput.placeholder = '输入框数'; periodInput.value = records[periodKey] ? records[periodKey].boxes : ''; periodInput.style.cssText = ` width: 100%; padding: 8px; border-radius: 4px; border: 1px solid #555; background: #1e2a36; color: #ecf0f1; margin-bottom: 5px; `; periodDiv.appendChild(periodInput); const noteInput = document.createElement('input'); noteInput.type = 'text'; noteInput.placeholder = '备注(可选)'; noteInput.value = records[periodKey] ? records[periodKey].note : ''; noteInput.style.cssText = ` width: 100%; padding: 8px; border-radius: 4px; border: 1px solid #555; background: #1e2a36; color: #ecf0f1; `; periodDiv.appendChild(noteInput); // 如果已有记录,添加删除按钮 if (records[periodKey]) { const deleteBtn = document.createElement('button'); deleteBtn.textContent = '删除记录'; deleteBtn.style.cssText = ` background: #e74c3c; border: none; color: #fff; padding: 5px 10px; border-radius: 4px; cursor: pointer; margin-top: 5px; `; deleteBtn.onclick = () => { if (confirm(`确定要删除${period.name}的记录吗?`)) { deleteCalendarRecord(currentTaskId, dateStr, periodKey); // 刷新日历 const calendarRecords = getCalendarRecords(currentTaskId); const calendarGrid = document.querySelector('#calendar-container > div:first-child > div:last-child'); const monthTitle = document.querySelector('#calendar-container > div:first-child > div:first-child > span'); const [year, month] = monthTitle.textContent.split('年').map(s => parseInt(s.replace('月', ''))); updateCalendarGrid(year, month - 1, calendarGrid, calendarRecords); // 刷新编辑区 showCalendarEdit(dateStr, calendarRecords[dateStr] || {}); } }; periodDiv.appendChild(deleteBtn); } editForm.appendChild(periodDiv); }); // 保存按钮 const saveBtn = document.createElement('button'); saveBtn.textContent = '保存记录'; saveBtn.style.cssText = ` background: #2ecc71; border: none; color: #fff; padding: 8px 16px; border-radius: 4px; cursor: pointer; margin-top: 10px; width: 100%; `; saveBtn.onclick = () => { const periodInputs = editForm.querySelectorAll('input[type="number"]'); const noteInputs = editForm.querySelectorAll('input[type="text"]'); // 保存每个时段的记录 Object.keys(timePeriods).forEach((periodKey, index) => { const boxes = periodInputs[index].value; const note = noteInputs[index].value; if (boxes) { addCalendarRecord(currentTaskId, dateStr, periodKey, boxes, note); } }); // 刷新日历 const calendarRecords = getCalendarRecords(currentTaskId); const calendarGrid = document.querySelector('#calendar-container > div:first-child > div:last-child'); const monthTitle = document.querySelector('#calendar-container > div:first-child > div:first-child > span'); const [year, month] = monthTitle.textContent.split('年').map(s => parseInt(s.replace('月', ''))); updateCalendarGrid(year, month - 1, calendarGrid, calendarRecords); alert('记录已保存'); }; editForm.appendChild(saveBtn); // 清空编辑区并添加新表单 editContent.innerHTML = ''; editContent.appendChild(editForm); } // ========== 显示时段设置 ========== function showPeriodSettings() { // 创建设置弹窗 const settingsModal = document.createElement('div'); settingsModal.id = 'period-settings-modal'; settingsModal.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 9999999; display: flex; align-items: center; justify-content: center; `; // 创建弹窗内容 const modalContent = document.createElement('div'); modalContent.style.cssText = ` background: #303030; color: #ecf0f1; border-radius: 8px; padding: 20px; width: 400px; max-height: 80vh; overflow-y: auto; box-shadow: 0 4px 20px rgba(0,0,0,0.3); `; // 弹窗头部 const modalHeader = document.createElement('div'); modalHeader.style.cssText = ` display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; padding-bottom: 10px; border-bottom: 1px solid #555; `; const modalTitle = document.createElement('h2'); modalTitle.textContent = '时段设置'; modalTitle.style.margin = '0'; const closeBtn = document.createElement('button'); closeBtn.innerHTML = '×'; closeBtn.style.cssText = ` background: transparent; border: none; color: #ecf0f1; font-size: 24px; cursor: pointer; line-height: 1; `; closeBtn.onclick = () => { document.body.removeChild(settingsModal); }; modalHeader.appendChild(modalTitle); modalHeader.appendChild(closeBtn); modalContent.appendChild(modalHeader); // 获取当前时段配置 const timePeriods = getTimePeriods(); // 创建时段设置表单 const settingsForm = document.createElement('div'); Object.keys(timePeriods).forEach(periodKey => { const period = timePeriods[periodKey]; const periodDiv = document.createElement('div'); periodDiv.style.marginBottom = '15px'; const nameLabel = document.createElement('label'); nameLabel.textContent = '时段名称:'; nameLabel.style.display = 'block'; nameLabel.style.marginBottom = '5px'; periodDiv.appendChild(nameLabel); const nameInput = document.createElement('input'); nameInput.type = 'text'; nameInput.value = period.name; nameInput.style.cssText = ` width: 100%; padding: 8px; border-radius: 4px; border: 1px solid #555; background: #1e2a36; color: #ecf0f1; margin-bottom: 10px; `; periodDiv.appendChild(nameInput); const startLabel = document.createElement('label'); startLabel.textContent = '开始时间:'; startLabel.style.display = 'block'; startLabel.style.marginBottom = '5px'; periodDiv.appendChild(startLabel); const startInput = document.createElement('input'); startInput.type = 'time'; startInput.value = period.start; startInput.style.cssText = ` width: 100%; padding: 8px; border-radius: 4px; border: 1px solid #555; background: #1e2a36; color: #ecf0f1; margin-bottom: 10px; `; periodDiv.appendChild(startInput); const endLabel = document.createElement('label'); endLabel.textContent = '结束时间:'; endLabel.style.display = 'block'; endLabel.style.marginBottom = '5px'; periodDiv.appendChild(endLabel); const endInput = document.createElement('input'); endInput.type = 'time'; endInput.value = period.end; endInput.style.cssText = ` width: 100%; padding: 8px; border-radius: 4px; border: 1px solid #555; background: #1e2a36; color: #ecf0f1; margin-bottom: 10px; `; periodDiv.appendChild(endInput); settingsForm.appendChild(periodDiv); }); modalContent.appendChild(settingsForm); // 添加新时段按钮 const addPeriodBtn = document.createElement('button'); addPeriodBtn.textContent = '添加时段'; addPeriodBtn.style.cssText = ` background: #2ecc71; border: none; color: #fff; padding: 8px 16px; border-radius: 4px; cursor: pointer; margin-bottom: 10px; width: 100%; `; addPeriodBtn.onclick = () => { const periodKey = `custom_${Date.now()}`; const periodDiv = document.createElement('div'); periodDiv.style.marginBottom = '15px'; const nameLabel = document.createElement('label'); nameLabel.textContent = '时段名称:'; nameLabel.style.display = 'block'; nameLabel.style.marginBottom = '5px'; periodDiv.appendChild(nameLabel); const nameInput = document.createElement('input'); nameInput.type = 'text'; nameInput.value = '新时段'; nameInput.style.cssText = ` width: 100%; padding: 8px; border-radius: 4px; border: 1px solid #555; background: #1e2a36; color: #ecf0f1; margin-bottom: 10px; `; periodDiv.appendChild(nameInput); const startLabel = document.createElement('label'); startLabel.textContent = '开始时间:'; startLabel.style.display = 'block'; startLabel.style.marginBottom = '5px'; periodDiv.appendChild(startLabel); const startInput = document.createElement('input'); startInput.type = 'time'; startInput.value = '09:00'; startInput.style.cssText = ` width: 100%; padding: 8px; border-radius: 4px; border: 1px solid #555; background: #1e2a36; color: #ecf0f1; margin-bottom: 10px; `; periodDiv.appendChild(startInput); const endLabel = document.createElement('label'); endLabel.textContent = '结束时间:'; endLabel.style.display = 'block'; endLabel.style.marginBottom = '5px'; periodDiv.appendChild(endLabel); const endInput = document.createElement('input'); endInput.type = 'time'; endInput.value = '12:00'; endInput.style.cssText = ` width: 100%; padding: 8px; border-radius: 4px; border: 1px solid #555; background: #1e2a36; color: #ecf0f1; margin-bottom: 10px; `; periodDiv.appendChild(endInput); const deleteBtn = document.createElement('button'); deleteBtn.textContent = '删除时段'; deleteBtn.style.cssText = ` background: #e74c3c; border: none; color: #fff; padding: 5px 10px; border-radius: 4px; cursor: pointer; margin-top: 5px; `; deleteBtn.onclick = () => { settingsForm.removeChild(periodDiv); }; periodDiv.appendChild(deleteBtn); settingsForm.appendChild(periodDiv); }; modalContent.appendChild(addPeriodBtn); // 保存按钮 const saveBtn = document.createElement('button'); saveBtn.textContent = '保存设置'; saveBtn.style.cssText = ` background: #3498db; border: none; color: #fff; padding: 8px 16px; border-radius: 4px; cursor: pointer; width: 100%; `; saveBtn.onclick = () => { const newPeriods = {}; const periodDivs = settingsForm.children; for (let i = 0; i < periodDivs.length; i++) { const periodDiv = periodDivs[i]; const inputs = periodDiv.querySelectorAll('input'); const name = inputs[0].value; const start = inputs[1].value; const end = inputs[2].value; const periodKey = i < 3 ? (i === 0 ? 'morning' : i === 1 ? 'afternoon' : 'allday') : `custom_${i}`; newPeriods[periodKey] = { name: name, start: start, end: end }; } saveTimePeriods(newPeriods); alert('时段设置已保存'); document.body.removeChild(settingsModal); // 刷新日历 const calendarRecords = getCalendarRecords(currentTaskId); const calendarGrid = document.querySelector('#calendar-container > div:first-child > div:last-child'); const monthTitle = document.querySelector('#calendar-container > div:first-child > div:first-child > span'); const [year, month] = monthTitle.textContent.split('年').map(s => parseInt(s.replace('月', ''))); updateCalendarGrid(year, month - 1, calendarGrid, calendarRecords); }; modalContent.appendChild(saveBtn); settingsModal.appendChild(modalContent); document.body.appendChild(settingsModal); } // ========== 导出任务数据 ========== function exportTaskData(task, format = 'excel') { if (!task) return; // 准备导出数据 const exportData = { taskInfo: { id: task.id, name: task.name, createTime: task.history[task.history.length - 1].timestamp, startTime: task.startTime, totalBoxes: task.totalBoxes, lastUpdate: task.lastUpdate }, history: task.history, calendar: task.calendar || {} }; if (format === 'excel') { // 导出为Excel格式 const wb = XLSX.utils.book_new(); // 任务基本信息 const infoData = [ ['任务信息', ''], ['任务ID', exportData.taskInfo.id], ['任务名称', exportData.taskInfo.name], ['创建时间', new Date(exportData.taskInfo.createTime).toLocaleString()], ['开始时间', exportData.taskInfo.startTime ? new Date(exportData.taskInfo.startTime).toLocaleString() : '-'], ['当前总框数', exportData.taskInfo.totalBoxes], ['最后更新', new Date(exportData.taskInfo.lastUpdate).toLocaleString()] ]; const infoWs = XLSX.utils.aoa_to_sheet(infoData); XLSX.utils.book_append_sheet(wb, infoWs, '任务信息'); // 操作记录 const historyData = [['时间', '操作类型', '变化量', '总框数']]; exportData.history.forEach(item => { historyData.push([ new Date(item.timestamp).toLocaleString(), item.change === item.total ? '创建任务' : '更新框数', item.change, item.total ]); }); const historyWs = XLSX.utils.aoa_to_sheet(historyData); XLSX.utils.book_append_sheet(wb, historyWs, '操作记录'); // 日历记录 const calendarData = [['日期', '时段', '框数', '备注', '记录时间']]; Object.keys(exportData.calendar).forEach(date => { Object.keys(exportData.calendar[date]).forEach(period => { const record = exportData.calendar[date][period]; calendarData.push([ date, period, record.boxes, record.note || '', new Date(record.timestamp).toLocaleString() ]); }); }); const calendarWs = XLSX.utils.aoa_to_sheet(calendarData); XLSX.utils.book_append_sheet(wb, calendarWs, '日历记录'); // 下载Excel文件 XLSX.writeFile(wb, `任务_${task.name}_${new Date().toISOString().slice(0,10)}.xlsx`); } else if (format === 'csv') { // 导出为CSV格式 let csvContent = 'data:text/csv;charset=utf-8,\uFEFF'; // 添加BOM以支持中文 // 任务基本信息 csvContent += '任务信息\n'; csvContent += `任务ID,${exportData.taskInfo.id}\n`; csvContent += `任务名称,${exportData.taskInfo.name}\n`; csvContent += `创建时间,${new Date(exportData.taskInfo.createTime).toLocaleString()}\n`; csvContent += `开始时间,${exportData.taskInfo.startTime ? new Date(exportData.taskInfo.startTime).toLocaleString() : '-'}\n`; csvContent += `当前总框数,${exportData.taskInfo.totalBoxes}\n`; csvContent += `最后更新,${new Date(exportData.taskInfo.lastUpdate).toLocaleString()}\n\n`; // 操作记录 csvContent += '操作记录\n'; csvContent += '时间,操作类型,变化量,总框数\n'; exportData.history.forEach(item => { csvContent += `${new Date(item.timestamp).toLocaleString()},${item.change === item.total ? '创建任务' : '更新框数'},${item.change},${item.total}\n`; }); csvContent += '\n'; // 日历记录 csvContent += '日历记录\n'; csvContent += '日期,时段,框数,备注,记录时间\n'; Object.keys(exportData.calendar).forEach(date => { Object.keys(exportData.calendar[date]).forEach(period => { const record = exportData.calendar[date][period]; csvContent += `${date},${period},${record.boxes},"${record.note || ''}",${new Date(record.timestamp).toLocaleString()}\n`; }); }); // 下载CSV文件 const encodedUri = encodeURI(csvContent); const link = document.createElement('a'); link.setAttribute('href', encodedUri); link.setAttribute('download', `任务_${task.name}_${new Date().toISOString().slice(0,10)}.csv`); document.body.appendChild(link); link.click(); document.body.removeChild(link); } } // ========== 自动保存功能 ========== function loadSettings() { const settings = localStorage.getItem(STORAGE_KEY_SETTINGS); return settings ? JSON.parse(settings) : { autoSaveEnabled: true, // 默认开启自动保存 autoSaveInterval: 5, // 默认5分钟自动保存 autoSaveUnit: 'minutes', // 默认单位为分钟 showSaveNotification: true // 默认显示保存通知 }; } function saveSettings(settings) { localStorage.setItem(STORAGE_KEY_SETTINGS, JSON.stringify(settings)); } function enableAutoSave() { const settings = loadSettings(); if (autoSaveInterval) clearInterval(autoSaveInterval); if (!settings.autoSaveEnabled) return; let intervalMs; switch (settings.autoSaveUnit) { case 'seconds': intervalMs = settings.autoSaveInterval * 1000; break; case 'minutes': intervalMs = settings.autoSaveInterval * 60 * 1000; break; case 'hours': intervalMs = settings.autoSaveInterval * 60 * 60 * 1000; break; default: intervalMs = settings.autoSaveInterval * 60 * 1000; } // 等待保存按钮出现(因为页面可能动态加载) function waitForSaveButton(callback) { const check = () => { // 使用你提供的精确选择器 const saveBtn = document.querySelector('#top > div:nth-child(17) > button:nth-child(2)'); if (saveBtn) { callback(saveBtn); } else { setTimeout(check, 500); } }; check(); } waitForSaveButton((saveBtn) => { autoSaveInterval = setInterval(() => { if (!saveBtn.disabled) { saveBtn.click(); console.log('已自动点击保存按钮'); } else { console.warn('保存按钮当前不可用(disabled)'); } }, intervalMs); console.log(`自动保存已启动,间隔 ${intervalMs} ms`); }); } function toggleAutoSave() { const settings = loadSettings(); const toggleInput = document.getElementById('auto-save-toggle'); if(!toggleInput) return; settings.autoSaveEnabled = toggleInput.checked; saveSettings(settings); enableAutoSave(); const notification = document.createElement('div'); notification.style.cssText = ` position: fixed; top: 20px; left: 50%; transform: translateX(-50%); background: rgba(52, 152, 219, 0.9); color: white; padding: 10px 20px; border-radius: 4px; z-index: 9999999; box-shadow: 0 2px 10px rgba(0,0,0,0.2); font-size: 14px; `; notification.textContent = `自动点击保存已${settings.autoSaveEnabled ? '开启' : '关闭'}`; document.body.appendChild(notification); setTimeout(() => { notification.style.opacity = '0'; notification.style.transition = 'opacity 0.5s'; setTimeout(() => notification.remove(), 500); }, 3000); } // ========== 显示保存设置弹窗 ========== function showSaveSettings() { // 创建设置弹窗 const settingsModal = document.createElement('div'); settingsModal.id = 'save-settings-modal'; settingsModal.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 9999999; display: flex; align-items: center; justify-content: center; `; // 创建弹窗内容 const modalContent = document.createElement('div'); modalContent.style.cssText = ` background: #303030; color: #ecf0f1; border-radius: 8px; padding: 20px; width: 400px; max-height: 80vh; overflow-y: auto; box-shadow: 0 4px 20px rgba(0,0,0,0.3); `; // 弹窗头部 const modalHeader = document.createElement('div'); modalHeader.style.cssText = ` display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; padding-bottom: 10px; border-bottom: 1px solid #555; `; const modalTitle = document.createElement('h2'); modalTitle.textContent = '自动点击保存设置'; modalTitle.style.margin = '0'; const closeBtn = document.createElement('button'); closeBtn.innerHTML = '×'; closeBtn.style.cssText = ` background: transparent; border: none; color: #ecf0f1; font-size: 24px; cursor: pointer; line-height: 1; `; closeBtn.onclick = () => { document.body.removeChild(settingsModal); }; modalHeader.appendChild(modalTitle); modalHeader.appendChild(closeBtn); modalContent.appendChild(modalHeader); // 获取当前设置 const settings = loadSettings(); // 创建设置表单 const settingsForm = document.createElement('div'); // 自动保存开关 const autoSaveDiv = document.createElement('div'); autoSaveDiv.style.marginBottom = '15px'; const autoSaveLabel = document.createElement('label'); autoSaveLabel.textContent = '启用自动点击保存'; autoSaveLabel.style.display = 'block'; autoSaveLabel.style.marginBottom = '5px'; autoSaveDiv.appendChild(autoSaveLabel); const autoSaveCheckbox = document.createElement('input'); autoSaveCheckbox.type = 'checkbox'; autoSaveCheckbox.checked = settings.autoSaveEnabled; autoSaveCheckbox.style.marginRight = '10px'; autoSaveDiv.appendChild(autoSaveCheckbox); settingsForm.appendChild(autoSaveDiv); // 自动保存间隔 const intervalDiv = document.createElement('div'); intervalDiv.style.marginBottom = '15px'; const intervalLabel = document.createElement('label'); intervalLabel.textContent = '自动点击间隔:'; intervalLabel.style.display = 'block'; intervalLabel.style.marginBottom = '5px'; intervalDiv.appendChild(intervalLabel); // 创建单行容器,包含输入框和单位选择 const intervalRow = document.createElement('div'); intervalRow.style.cssText = ` display: flex; align-items: center; gap: 8px; `; const intervalInput = document.createElement('input'); intervalInput.type = 'number'; intervalInput.min = '1'; intervalInput.value = settings.autoSaveInterval; intervalInput.style.cssText = ` flex: 1; padding: 8px; border-radius: 4px; border: 1px solid #555; background: #1e2a36; color: #ecf0f1; `; intervalRow.appendChild(intervalInput); const unitSelect = document.createElement('select'); unitSelect.style.cssText = ` flex: 1; padding: 8px; border-radius: 4px; border: 1px solid #555; background: #1e2a36; color: #ecf0f1; `; // 添加单位选项 const units = [ { value: 'seconds', text: '秒' }, { value: 'minutes', text: '分钟' }, { value: 'hours', text: '小时' } ]; units.forEach(unit => { const option = document.createElement('option'); option.value = unit.value; option.textContent = unit.text; if (unit.value === settings.autoSaveUnit) { option.selected = true; } unitSelect.appendChild(option); }); intervalRow.appendChild(unitSelect); intervalDiv.appendChild(intervalRow); settingsForm.appendChild(intervalDiv); modalContent.appendChild(settingsForm); // 保存按钮 const saveBtn = document.createElement('button'); saveBtn.textContent = '保存设置'; saveBtn.style.cssText = ` background: #2ecc71; border: none; color: #fff; padding: 8px 16px; border-radius: 4px; cursor: pointer; width: 100%; `; saveBtn.onclick = () => { const newSettings = { autoSaveEnabled: autoSaveCheckbox.checked, autoSaveInterval: parseInt(intervalInput.value, 10), autoSaveUnit: unitSelect.value }; saveSettings(newSettings); enableAutoSave(); alert('设置已保存'); document.body.removeChild(settingsModal); }; modalContent.appendChild(saveBtn); settingsModal.appendChild(modalContent); document.body.appendChild(settingsModal); } // ========== UI 构建 ========== function createControlPanel() { if (document.getElementById('box-helper-panel')) return; const panel = document.createElement('div'); panel.id = 'box-helper-panel'; panel.style.cssText = ` position: fixed; top: 10px; right: 10px; background: #303030; color: #ecf0f1; border-radius: 8px; padding: 12px; z-index: 999999; font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; font-size: 13px; box-shadow: 0 2px 10px rgba(0,0,0,0.3); width: 340px; height: auto; text-align: left; resize: both; overflow: auto; `; // 创建悬浮球 const floatingBall = document.createElement('div'); floatingBall.id = 'floating-ball'; floatingBall.style.cssText = ` position: fixed; top: 10px; right: 10px; width: 50px; height: 50px; background: rgba(200, 200, 200, 0.5); border-radius: 50%; z-index: 999998; cursor: move; display: none; backdrop-filter: blur(3px); transition: transform 0.3s ease; box-shadow: 0 2px 8px rgba(0,0,0,0.2); align-items: center; justify-content: center; user-select: none; `; // 添加 "框"字图案 const boxText = document.createElement('div'); boxText.textContent = '框'; boxText.style.cssText = ` color: #333; font-weight: bold; font-size: 20px; opacity: 0.8; `; floatingBall.appendChild(boxText); // 悬浮球拖动功能 let isDraggingBall = false; let hasMovedBall = false; let ballPos1 = 0, ballPos2 = 0, ballPos3 = 0, ballPos4 = 0; let startX = 0, startY = 0; floatingBall.onmousedown = function(e) { e.preventDefault(); isDraggingBall = true; hasMovedBall = false; startX = e.clientX; startY = e.clientY; ballPos3 = e.clientX; ballPos4 = e.clientY; document.onmouseup = closeDragBall; document.onmousemove = dragBall; }; function dragBall(e) { e.preventDefault(); ballPos1 = ballPos3 - e.clientX; ballPos2 = ballPos4 - e.clientY; ballPos3 = e.clientX; ballPos4 = e.clientY; let top = floatingBall.offsetTop - ballPos2; let left = floatingBall.offsetLeft - ballPos1; // 限制在屏幕内 top = Math.min(Math.max(0, top), window.innerHeight - floatingBall.offsetHeight); left = Math.min(Math.max(0, left), window.innerWidth - floatingBall.offsetWidth); // 检查是否在屏幕边缘 const edgeThreshold = 25; if (left < edgeThreshold) { floatingBall.style.transform = 'translateX(-50%)'; } else if (left > window.innerWidth - edgeThreshold - floatingBall.offsetWidth) { floatingBall.style.transform = 'translateX(50%)'; } else { floatingBall.style.transform = 'translateX(0)'; } floatingBall.style.top = top + 'px'; floatingBall.style.left = left + 'px'; floatingBall.style.right = 'auto'; floatingBall.style.bottom = 'auto'; // 检查是否发生了移动 const moveDistance = Math.sqrt(Math.pow(e.clientX - startX, 2) + Math.pow(e.clientY - startY, 2)); if (moveDistance > 5) { hasMovedBall = true; } } function closeDragBall() { document.onmouseup = null; document.onmousemove = null; isDraggingBall = false; } // 悬浮球点击事件 floatingBall.onclick = function(e) { if (!hasMovedBall) { panel.style.display = 'block'; floatingBall.style.display = 'none'; panel.style.opacity = '1'; panel.style.transform = 'translateX(0)'; } }; document.body.appendChild(floatingBall); const header = document.createElement('div'); header.className = 'panel-header'; header.style.cssText = ` display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; padding-bottom: 8px; border-bottom: 1px solid #555; user-select: none; `; const title = document.createElement('span'); title.textContent = '框数统计助手 v1.0.3'; title.style.cssText = ` font-weight: bold; font-size: 16px; `; // 创建右侧按钮容器 const rightButtons = document.createElement('div'); rightButtons.style.cssText = ` display: flex; align-items: center; gap: 8px; `; // ===== 自动保存开关(滑动式)===== const toggleSwitch = document.createElement('label'); toggleSwitch.className = 'toggle-switch'; toggleSwitch.style.cssText = ` position: relative; display: inline-block; width: 44px; height: 22px; cursor: pointer; `; const toggleInput = document.createElement('input'); toggleInput.type = 'checkbox'; toggleInput.id = 'auto-save-toggle'; toggleInput.checked = loadSettings().autoSaveEnabled; toggleInput.style.cssText = ` opacity: 0; width: 0; height: 0; position: absolute; z-index: 1; `; const toggleSlider = document.createElement('span'); toggleSlider.className = 'toggle-switch-slider'; // 添加类名 toggleSlider.innerHTML = ' '; // 占位 // 添加全局样式 const styleId = 'toggle-switch-style'; if (!document.getElementById(styleId)) { const style = document.createElement('style'); style.id = styleId; style.textContent = ` .toggle-switch { position: relative; display: inline-block; width: 44px; height: 22px; cursor: pointer; } .toggle-switch-slider { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background-color: #95a5a6; border-radius: 22px; transition: background-color 0.3s ease; cursor: pointer; box-shadow: inset 0 2px 5px rgba(0,0,0,0.2); } .toggle-switch-slider:hover { background-color: #7f8c8d; } .toggle-switch-slider::before { position: absolute; content: ""; height: 18px; width: 18px; left: 2px; bottom: 2px; background-color: #ecf0f1; border-radius: 50%; transition: transform 0.3s ease, background-color 0.3s ease; box-shadow: 0 2px 5px rgba(0,0,0,0.25); } #auto-save-toggle:checked + .toggle-switch-slider { background-color: #2ecc71; } #auto-save-toggle:checked + .toggle-switch-slider:hover { background-color: #27ae60; } #auto-save-toggle:checked + .toggle-switch-slider::before { transform: translateX(22px); background-color: #ffffff; } `; document.head.appendChild(style); } toggleSwitch.appendChild(toggleInput); toggleSwitch.appendChild(toggleSlider); // 设置按钮(齿轮图标) const settingsBtn = document.createElement('button'); settingsBtn.title = '自动保存设置'; settingsBtn.innerHTML = ` `; settingsBtn.style.cssText = ` background: transparent; border: none; color: #ecf0f1; width: 28px; height: 28px; border-radius: 4px; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; transition: background 0.2s; `; settingsBtn.onmouseover = () => settingsBtn.style.background = 'rgba(255,255,255,0.1)'; settingsBtn.onmouseout = () => settingsBtn.style.background = 'transparent'; settingsBtn.onclick = () => showSaveSettings(); const minimizeBtn = document.createElement('button'); minimizeBtn.title = '最小化'; minimizeBtn.innerHTML = '−'; minimizeBtn.style.cssText = ` background: #555; border: none; color: #ecf0f1; width: 24px; height: 24px; border-radius: 4px; cursor: pointer; font-size: 16px; line-height: 1; display: flex; align-items: center; justify-content: center; transition: background 0.2s; `; minimizeBtn.onmouseover = () => minimizeBtn.style.background = '#666'; minimizeBtn.onmouseout = () => minimizeBtn.style.background = '#555'; minimizeBtn.onclick = () => { // 获取面板当前位置 const panelRect = panel.getBoundingClientRect(); // 设置悬浮球位置为面板当前位置 floatingBall.style.top = panelRect.top + 'px'; floatingBall.style.left = panelRect.left + 'px'; floatingBall.style.right = 'auto'; floatingBall.style.bottom = 'auto'; // 隐藏面板,显示悬浮球 panel.style.opacity = '0'; panel.style.transform = 'translateX(20px)'; setTimeout(() => { panel.style.display = 'none'; floatingBall.style.display = 'flex'; // 重置悬浮球位置到屏幕右侧 floatingBall.style.top = '50%'; floatingBall.style.left = 'auto'; floatingBall.style.right = '10px'; floatingBall.style.transform = 'translateX(50%)'; }, 300); }; // 将开关和设置按钮添加到右侧容器 rightButtons.appendChild(toggleSwitch); rightButtons.appendChild(settingsBtn); rightButtons.appendChild(minimizeBtn); // ✅ 现在 minimizeBtn 已经初始化完毕 // 添加事件监听器 toggleInput.addEventListener('change', function() { toggleAutoSave(); }); // 将标题和右侧按钮添加到头部 header.appendChild(title); header.appendChild(rightButtons); panel.appendChild(header); // 模式切换区域 - 分为左右两部分 const modeSwitchDiv = document.createElement('div'); modeSwitchDiv.style.cssText = ` display: flex; gap: 0; margin-bottom: 12px; border-radius: 4px; overflow: hidden; border: 1px solid #555; `; const singleModeBtn = document.createElement('div'); singleModeBtn.id = 'single-mode-btn'; singleModeBtn.textContent = '单次统计'; singleModeBtn.style.cssText = ` flex: 1; background: transparent; color: #ecf0f1; padding: 10px; cursor: pointer; text-align: center; font-weight: normal; transition: all 0.2s; `; singleModeBtn.onclick = () => { setMode('single'); updateModeButtons(); }; const taskModeBtn = document.createElement('div'); taskModeBtn.id = 'task-mode-btn'; taskModeBtn.textContent = '任务模式'; taskModeBtn.style.cssText = ` flex: 1; background: transparent; color: #ecf0f1; padding: 10px; cursor: pointer; text-align: center; font-weight: normal; transition: all 0.2s; border-left: 1px solid #555; `; taskModeBtn.onclick = () => { setMode('task'); updateModeButtons(); }; modeSwitchDiv.appendChild(singleModeBtn); modeSwitchDiv.appendChild(taskModeBtn); panel.appendChild(modeSwitchDiv); // 更新模式按钮样式函数 function updateModeButtons() { if (currentMode === 'single') { singleModeBtn.style.background = 'rgba(52, 152, 219, 0.3)'; singleModeBtn.style.color = '#87ceeb'; taskModeBtn.style.background = 'transparent'; taskModeBtn.style.color = '#ecf0f1'; } else { taskModeBtn.style.background = 'rgba(230, 126, 34, 0.3)'; taskModeBtn.style.color = '#87ceeb'; singleModeBtn.style.background = 'transparent'; singleModeBtn.style.color = '#ecf0f1'; } } // 单次模式面板 const singlePanel = document.createElement('div'); singlePanel.id = 'single-panel'; const totalDiv = document.createElement('div'); totalDiv.style.marginBottom = '12px'; totalDiv.style.fontSize = '14px'; totalDiv.innerHTML = `累计框数:${loadSingleTotal()}`; singlePanel.appendChild(totalDiv); const singleBtnGroup = document.createElement('div'); singleBtnGroup.style.display = 'flex'; singleBtnGroup.style.gap = '8px'; singleBtnGroup.style.marginBottom = '12px'; const addBtn = document.createElement('button'); addBtn.textContent = '累加当前帧'; addBtn.style.cssText = `flex:1; background:#3498db; border:none; color:#fff; padding:6px; border-radius:4px; cursor:pointer;`; addBtn.onclick = () => handleSingleAddFrame(); const resetBtn = document.createElement('button'); resetBtn.textContent = '重置累计'; resetBtn.style.cssText = `flex:1; background:#e67e22; border:none; color:#fff; padding:6px; border-radius:4px; cursor:pointer;`; resetBtn.onclick = () => resetCurrentMode(); singleBtnGroup.appendChild(addBtn); singleBtnGroup.appendChild(resetBtn); singlePanel.appendChild(singleBtnGroup); panel.appendChild(singlePanel); // 任务模式面板 const taskPanel = document.createElement('div'); taskPanel.id = 'task-panel'; taskPanel.style.display = 'none'; const taskSelectDiv = document.createElement('div'); taskSelectDiv.style.marginBottom = '12px'; taskSelectDiv.style.display = 'flex'; taskSelectDiv.style.gap = '8px'; taskSelectDiv.style.alignItems = 'center'; const taskSelectLabel = document.createElement('span'); taskSelectLabel.textContent = '任务:'; taskSelectLabel.style.fontSize = '12px'; const taskSelect = document.createElement('select'); taskSelect.id = 'task-select'; taskSelect.style.flex = '1'; taskSelect.style.padding = '4px'; taskSelect.style.borderRadius = '4px'; taskSelect.style.border = 'none'; taskSelect.onchange = onTaskSelectChange; const newTaskBtn = document.createElement('button'); newTaskBtn.textContent = '新建'; newTaskBtn.style.cssText = `background:#2ecc71; border:none; color:#fff; padding:4px 8px; border-radius:4px; cursor:pointer;`; newTaskBtn.onclick = createNewTask; const taskDetailBtn = document.createElement('button'); taskDetailBtn.textContent = '详情'; taskDetailBtn.style.cssText = `background:#3498db; border:none; color:#fff; padding:4px 8px; border-radius:4px; cursor:pointer;`; taskDetailBtn.onclick = showTaskDetail; taskSelectDiv.appendChild(taskSelectLabel); taskSelectDiv.appendChild(taskSelect); taskSelectDiv.appendChild(newTaskBtn); taskSelectDiv.appendChild(taskDetailBtn); taskPanel.appendChild(taskSelectDiv); const timeDiv = document.createElement('div'); timeDiv.style.marginBottom = '12px'; timeDiv.innerHTML = `
`; taskPanel.appendChild(timeDiv); const taskStatsDiv = document.createElement('div'); taskStatsDiv.style.marginBottom = '12px'; taskStatsDiv.style.backgroundColor = '#1e2a36'; taskStatsDiv.style.padding = '8px'; taskStatsDiv.style.borderRadius = '4px'; taskStatsDiv.innerHTML = `
总框数:0
上次记录:-
变化量:0
统计时间:-
`; taskPanel.appendChild(taskStatsDiv); const taskBtnGroup = document.createElement('div'); taskBtnGroup.style.display = 'flex'; taskBtnGroup.style.gap = '8px'; taskBtnGroup.style.marginBottom = '12px'; const manualUpdateBtn = document.createElement('button'); manualUpdateBtn.textContent = '手动输入总框数'; manualUpdateBtn.style.cssText = `flex:1; background:#f39c12; border:none; color:#fff; padding:6px; border-radius:4px; cursor:pointer;`; manualUpdateBtn.onclick = () => manualUpdateTaskTotal(); const taskResetBtn = document.createElement('button'); taskResetBtn.textContent = '删除任务'; taskResetBtn.style.cssText = `flex:1; background:#e67e22; border:none; color:#fff; padding:6px; border-radius:4px; cursor:pointer;`; taskResetBtn.onclick = () => resetCurrentMode(); taskBtnGroup.appendChild(manualUpdateBtn); taskBtnGroup.appendChild(taskResetBtn); taskPanel.appendChild(taskBtnGroup); panel.appendChild(taskPanel); // 公共参数区域 const paramDiv = document.createElement('div'); paramDiv.style.marginBottom = '12px'; paramDiv.style.borderTop = '1px solid #555'; paramDiv.style.paddingTop = '10px'; const paramRow1 = document.createElement('div'); paramRow1.style.display = 'flex'; paramRow1.style.gap = '8px'; paramRow1.style.alignItems = 'center'; paramRow1.style.justifyContent = 'center'; const waitLabel = document.createElement('span'); waitLabel.textContent = '等待:'; waitLabel.style.fontSize = '12px'; const waitInput = document.createElement('input'); waitInput.type = 'number'; waitInput.value = DEFAULT_WAIT_MS; waitInput.style.cssText = ` width: 70px; padding: 4px; border-radius: 4px; border: none; `; const maxGroup = document.createElement('span'); maxGroup.style.whiteSpace = 'nowrap'; const maxLabel = document.createElement('span'); maxLabel.textContent = '最多帧数:'; maxLabel.style.fontSize = '12px'; const maxInput = document.createElement('input'); maxInput.type = 'number'; maxInput.value = ''; maxInput.placeholder = '不限'; maxInput.style.cssText = ` width: 70px; padding: 4px; border-radius: 4px; border: none; `; maxGroup.appendChild(maxLabel); maxGroup.appendChild(maxInput); paramRow1.appendChild(waitLabel); paramRow1.appendChild(waitInput); paramRow1.appendChild(maxGroup); // 创建按钮行容器 const buttonRow = document.createElement('div'); buttonRow.style.display = 'flex'; buttonRow.style.justifyContent = 'center'; buttonRow.style.gap = '10px'; buttonRow.style.marginTop = '10px'; // 自动遍历按钮 const autoBtn = document.createElement('button'); autoBtn.textContent = '自动遍历'; autoBtn.style.cssText = ` min-height: 32px; min-width: 90px; white-space: nowrap; background:#2ecc71; border:none; color:#fff; padding:6px 12px; border-radius:4px; cursor:pointer; `; autoBtn.onclick = () => autoCountAllFrames(waitInput.value, maxInput.value); // 停止按钮 const stopBtn = document.createElement('button'); stopBtn.textContent = '停止'; stopBtn.style.cssText = ` min-height: 32px; min-width: 60px; white-space: nowrap; background:#e74c3c; border:none; color:#fff; padding:6px 12px; border-radius:4px; cursor:pointer; `; stopBtn.onclick = () => { stopAutoFlag = true; appendLog('停止按钮已按下,将在当前帧完成后停止'); }; buttonRow.appendChild(autoBtn); buttonRow.appendChild(stopBtn); paramDiv.appendChild(paramRow1); paramDiv.appendChild(buttonRow); panel.appendChild(paramDiv); // 日志区域 const logContainer = document.createElement('div'); logContainer.style.marginTop = '8px'; logContainer.style.borderTop = '1px solid #555'; logContainer.style.paddingTop = '8px'; const logHeader = document.createElement('div'); logHeader.style.display = 'flex'; logHeader.style.justifyContent = 'space-between'; logHeader.style.marginBottom = '4px'; const logTitle = document.createElement('span'); logTitle.textContent = '统计记录'; logTitle.style.fontWeight = 'bold'; const clearLogBtn = document.createElement('button'); clearLogBtn.textContent = '清空'; clearLogBtn.style.cssText = `background:#7f8c8d; border:none; color:#fff; padding:2px 6px; border-radius:4px; cursor:pointer; font-size:11px;`; clearLogBtn.onclick = clearLog; logHeader.appendChild(logTitle); logHeader.appendChild(clearLogBtn); logContainer.appendChild(logHeader); const logArea = document.createElement('div'); logArea.id = 'stat-log'; logArea.style.maxHeight = '200px'; logArea.style.overflowY = 'auto'; logArea.style.backgroundColor = '#1e2a36'; logArea.style.padding = '4px'; logArea.style.borderRadius = '4px'; logArea.style.fontSize = '11px'; logArea.style.fontFamily = 'monospace'; logContainer.appendChild(logArea); panel.appendChild(logContainer); document.body.appendChild(panel); console.log('控制面板已添加到页面'); makeDraggableAndResizable(panel); // 初始化模式按钮状态 updateModeButtons(); setMode('single'); } function init() { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { setTimeout(() => { createControlPanel(); enableAutoSave(); }, 500); }); } else { setTimeout(() => { createControlPanel(); enableAutoSave(); }, 500); } } init(); })();