// ==UserScript== // @name 自动点餐系统 Pro (V9.0 多人队列+定时版) // @namespace http://tampermonkey.net/ // @version 9.02 // @description OA系统自动点餐脚本,支持多人循环点餐,支持定时任务(13:20),Layui深度适配 // @author 太初 // @match https://oa.sccrun.com/* // @match https://xcx.sccrun.com/* // @grant GM_setValue // @grant GM_getValue // @grant GM_openInTab // @grant GM_addStyle // @grant GM_setClipboard // @grant GM_registerMenuCommand // @grant window.close // @run-at document-end // ==/UserScript== (function() { 'use strict'; let isPaused = false; // 暂停标志 // ================= 配置区域 ================= const CONFIG = { URLS: { OA: 'oa.sccrun.com', ORDER: 'xcx.sccrun.com' }, DEFAULT_NAMES: '郭涛', // 默认名单 SCHEDULE_TIME: '13:20', // 定时时间 TIMEOUTS: { STEP_DELAY: 1500, MAX_WAIT: 25000, PAGE_LOAD: 3500 }, COLORS: { MAIN: 'linear-gradient(135deg, #A8C3CE, #B5C9C1)', // 莫兰迪蓝 WARN: 'linear-gradient(135deg, #E8D3C7, #E0C9B8)', // 莫兰迪粉 TEXT: '#4a4a4a' } }; // ================= 样式注入 ================= if (window.self === window.top) { GM_addStyle(` #gt-control-panel { position: fixed; top: 10px; left: 50%; transform: translateX(-50%) scale(0.85); z-index: 2147483647; display: flex; gap: 8px; padding: 10px 20px; background: rgba(255, 255, 255, 0.95); border-radius: 20px; box-shadow: 0 4px 15px rgba(0,0,0,0.15); backdrop-filter: blur(5px); } .gt-btn { border: none; padding: 8px 16px; border-radius: 8px; font-size: 14px; font-weight: 600; color: ${CONFIG.COLORS.TEXT}; cursor: pointer; background: ${CONFIG.COLORS.MAIN}; transition: all 0.2s; } .gt-btn:hover { transform: translateY(-2px); box-shadow: 0 2px 8px rgba(0,0,0,0.1); } .gt-btn:active { transform: scale(0.95); } .gt-btn-warn { background: ${CONFIG.COLORS.WARN}; } #gt-debug-panel { position: fixed; bottom: 20px; right: 20px; width: 400px; height: 250px; background: rgba(0, 0, 0, 0.85); color: #00ff00; font-family: 'Consolas', monospace; font-size: 12px; padding: 10px; border-radius: 8px; overflow-y: auto; z-index: 2147483647; display: none; /* 默认隐藏 */ } .gt-log-item { margin-bottom: 4px; border-bottom: 1px solid #333; } .gt-highlight { outline: 5px solid #ff4757 !important; box-shadow: 0 0 30px rgba(255, 71, 87, 0.8) !important; } /* 设置弹窗样式 */ #gt-settings-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 2147483647; display: flex; justify-content: center; align-items: center; } .gt-modal-content { background: white; padding: 25px; border-radius: 15px; width: 400px; box-shadow: 0 10px 25px rgba(0,0,0,0.2); } .gt-input { width: 100%; padding: 10px; margin: 10px 0; border: 1px solid #ddd; border-radius: 5px; } .gt-label { font-weight: bold; color: #333; display: block; margin-top: 10px;} `); } // ================= 状态与日志系统 ================= let fullLogs = ""; function log(msg, type = 'info') { const time = new Date().toLocaleTimeString().split(' ')[0]; console.log(`[GT-Auto] ${msg}`); if (window.self === window.top) { fullLogs += `[${time}] ${msg}\n`; const panel = document.getElementById('gt-debug-panel'); if (panel) { const div = document.createElement('div'); div.className = 'gt-log-item'; div.style.color = type === 'error' ? '#ff6b6b' : (type === 'success' ? '#00b894' : '#00ff00'); div.innerText = `[${time}] ${msg}`; panel.appendChild(div); panel.scrollTop = panel.scrollHeight; } } } // ================= 核心工具函数 ================= async function checkPaused() { if (isPaused) { while (isPaused) { await new Promise(r => setTimeout(r, 200)); } } } const sleep = async (ms) => { await checkPaused(); await new Promise(r => setTimeout(r, ms)); await checkPaused(); }; // 递归获取 iframe 文档 function getAllDocuments(rootWindow = window) { let docs = [{ doc: rootWindow.document, win: rootWindow }]; try { const frames = rootWindow.document.querySelectorAll('iframe'); for (let i = 0; i < frames.length; i++) { try { let iframe = frames[i]; if (iframe.contentWindow && iframe.contentWindow.document) { docs = docs.concat(getAllDocuments(iframe.contentWindow)); } } catch (e) {} } } catch (e) {} return docs; } // 上下文感知查找 function findInSpecificDocument(doc, selectors, textContent) { for (let selector of selectors) { try { let target = null; if (selector.startsWith('/')) { // XPath const result = doc.evaluate(selector, doc, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); target = result.singleNodeValue; } else if (selector.startsWith('#')) { // ID target = doc.getElementById(selector.substring(1)); } else { // CSS const elements = doc.querySelectorAll(selector); for (let el of elements) { if (textContent) { if (el.innerText.replace(/\s/g, '').includes(textContent) || el.value === textContent) { target = el; break; } } else { target = el; break; } } } if (target && target.offsetParent !== null) return target; } catch (e) {} } return null; } async function waitForElement(selectors, textContent = null) { await checkPaused(); const start = Date.now(); if (!Array.isArray(selectors)) selectors = [selectors]; log(`🔍 查找: ${textContent || selectors[0]}`); while (Date.now() - start < CONFIG.TIMEOUTS.MAX_WAIT) { await checkPaused(); const allCtx = getAllDocuments(window); for (let ctx of allCtx) { const target = findInSpecificDocument(ctx.doc, selectors, textContent); if (target) { target._ownerWindow = ctx.win; return target; } } await sleep(800); } throw new Error(`找不到元素: ${textContent || selectors[0]}`); } async function safeClick(element, desc) { log(`👆 点击: ${desc}`); element.classList.add('gt-highlight'); element.scrollIntoView({behavior: "smooth", block: "center"}); await sleep(500); element.click(); await sleep(1000); element.classList.remove('gt-highlight'); } async function safeInput(element, value) { log(`⌨️ 输入: ${value}`); element.classList.add('gt-highlight'); await sleep(300); let win = element._ownerWindow || window; if (win && win.$) { // Layui/jQuery 注入 try { win.$(element).val(value).trigger('change').trigger('input'); } catch(e){} } element.focus(); element.value = value; ['input', 'change', 'blur'].forEach(evt => element.dispatchEvent(new Event(evt, { bubbles: true }))); await sleep(500); element.classList.remove('gt-highlight'); } // ================= 业务逻辑:设置与队列 ================= function createStopButton() { if (document.getElementById('gt-stop-btn')) return; const btn = document.createElement('button'); btn.id = 'gt-stop-btn'; btn.innerHTML = '🛑 停止运行'; btn.style.cssText = ` position: fixed; top: 120px; right: 20px; z-index: 2147483648; padding: 10px 20px; background: linear-gradient(135deg, #ff7675, #d63031); color: white; border: none; border-radius: 50px; font-weight: bold; box-shadow: 0 4px 15px rgba(214, 48, 49, 0.4); cursor: move; font-size: 15px; transition: transform 0.1s, box-shadow 0.2s; user-select: none; `; // 拖拽逻辑 let isDragging = false; let startX, startY, initialLeft, initialTop; btn.onmousedown = (e) => { isDragging = true; startX = e.clientX; startY = e.clientY; const rect = btn.getBoundingClientRect(); initialLeft = rect.left; initialTop = rect.top; btn.style.cursor = 'grabbing'; // 移除 right 定位,改为 left/top 以便控制 btn.style.right = 'auto'; btn.style.left = initialLeft + 'px'; btn.style.top = initialTop + 'px'; }; const onMouseMove = (e) => { if (!isDragging) return; const dx = e.clientX - startX; const dy = e.clientY - startY; btn.style.left = (initialLeft + dx) + 'px'; btn.style.top = (initialTop + dy) + 'px'; }; const onMouseUp = () => { isDragging = false; btn.style.cursor = 'move'; }; // 绑定全局事件以防甩脱 document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); // 点击事件 (如果是拖拽则不触发点击) let isClick = true; btn.onclick = (e) => { if (Math.abs(e.clientX - startX) > 5 || Math.abs(e.clientY - startY) > 5) return; // 视为拖拽 isPaused = !isPaused; // 切换状态 if (isPaused) { btn.innerHTML = '▶️ 继续运行'; btn.style.background = '#0984e3'; // 蓝色表示可以继续 btn.style.boxShadow = 'none'; log('⏸️ 脚本已暂停', 'warn'); } else { btn.innerHTML = '🛑 停止运行'; btn.style.background = 'linear-gradient(135deg, #ff7675, #d63031)'; // 红色表示可以停止 btn.style.boxShadow = '0 4px 15px rgba(214, 48, 49, 0.4)'; log('▶️ 脚本继续执行', 'success'); } }; btn.onmouseenter = () => { if(!btn.disabled) btn.style.transform = 'scale(1.05)'; }; btn.onmouseleave = () => { if(!btn.disabled) btn.style.transform = 'scale(1)'; }; document.body.appendChild(btn); } function getNamesList() { const str = GM_getValue('CONFIG_NAMES', CONFIG.DEFAULT_NAMES); // 支持中文顿号、英文逗号、空格分隔 return str.split(/[、,,\s]+/).filter(n => n.trim()); } function showSettingsModal() { const existing = document.getElementById('gt-settings-modal'); if (existing) return; const modal = document.createElement('div'); modal.id = 'gt-settings-modal'; modal.innerHTML = `
`; document.body.appendChild(modal); document.getElementById('gt-save-config').onclick = () => { const val = document.getElementById('gt-names-input').value; GM_setValue('CONFIG_NAMES', val); alert('✅ 设置已保存!\n当前名单: ' + getNamesList().join(' -> ')); modal.remove(); }; document.getElementById('gt-cancel-config').onclick = () => modal.remove(); } // 注册油猴菜单 GM_registerMenuCommand("⚙️ 设置人员名单", showSettingsModal); // ================= 业务逻辑:OA 页面 (控制台) ================= function initOA() { if (document.getElementById('gt-control-panel')) return; const panel = document.createElement('div'); panel.id = 'gt-control-panel'; // 1. 启动按钮 const startBtn = document.createElement('button'); startBtn.className = 'gt-btn'; startBtn.textContent = '🚀 开始点餐'; startBtn.onclick = () => startQueueProcess(); // 2. 设置按钮 const configBtn = document.createElement('button'); configBtn.className = 'gt-btn'; configBtn.textContent = '⚙️ 名单设置'; configBtn.style.background = '#74b9ff'; configBtn.onclick = showSettingsModal; panel.appendChild(startBtn); panel.appendChild(configBtn); document.body.appendChild(panel); // 3. 启动定时任务 startScheduler(); } function startScheduler() { log('⏳ 定时任务监控中 (目标 13:20)...'); setInterval(() => { const now = new Date(); const day = now.getDay(); const timeStr = `${now.getHours()}:${now.getMinutes().toString().padStart(2, '0')}`; // 周一到周五 (1-5) 且 时间匹配 if (day >= 1 && day <= 5 && timeStr === CONFIG.SCHEDULE_TIME) { // 防止一分钟内重复触发 const lastRun = GM_getValue('LAST_RUN_DATE', ''); const todayStr = now.toDateString(); if (lastRun !== todayStr) { log('⏰ 到达 13:20,开始自动点餐!'); GM_setValue('LAST_RUN_DATE', todayStr); startQueueProcess(); } } }, 30000); // 30秒检查一次 } function startQueueProcess() { const names = getNamesList(); if (names.length === 0) return alert('请先设置人员名单!'); // 初始化队列 GM_setValue('ORDER_QUEUE', { queue: names, current: null, active: true }); // 打开/跳转到点餐页面 log(`🚀 启动任务,总人数: ${names.length}`); GM_openInTab(`https://${CONFIG.URLS.ORDER}/Home/Index`, { active: true, insert: true }); } // ================= 业务逻辑:点餐页面 (执行端) ================= async function processOrderQueue() { // 读取状态 const state = GM_getValue('ORDER_QUEUE', { active: false, queue: [] }); if (!state.active) return; createStopButton(); // 添加停止按钮 // 创建调试面板 (防止递归时重复创建导致叠加变黑) let debugPanel = document.getElementById('gt-debug-panel'); if (!debugPanel) { debugPanel = document.createElement('div'); debugPanel.id = 'gt-debug-panel'; debugPanel.style.display = 'block'; document.body.appendChild(debugPanel); } // 检查是否还有人需要点餐 if (state.queue.length === 0) { log('🎉 所有人员点餐完成!', 'success'); GM_setValue('ORDER_QUEUE', { active: false, queue: [] }); // 显示关闭按钮 showCloseButton(); return; } // 取出下一个人 const currentPerson = state.queue[0]; log(`👤 当前处理: ${currentPerson} (剩余: ${state.queue.length - 1}人)`); try { await executeSingleOrder(currentPerson); // 成功后,移除该人员 state.queue.shift(); // 移除第一个 GM_setValue('ORDER_QUEUE', state); log('✅ 本次成功,3秒后继续下一位...', 'success'); await sleep(3000); processOrderQueue(); // 递归调用,不再刷新页面 } catch (error) { if (error.message && error.message.includes("用户手动停止")) { log('🛑 脚本已响应停止指令,操作终止', 'error'); return; // 直接退出 } log(`❌ ${currentPerson} 点餐失败: ${error.message}`, 'error'); // 失败移除,防止死循环 state.queue.shift(); GM_setValue('ORDER_QUEUE', state); await sleep(3000); processOrderQueue(); // 失败也继续尝试下一个 } } // 单人点餐流程 (基于 V8.2 验证通过的逻辑) async function executeSingleOrder(targetName) { await sleep(CONFIG.TIMEOUTS.PAGE_LOAD); // 1. 菜单 (修复版) // 先在所有文档中搜索 "加班餐",检测是否已经可见 let subMenuTarget = null; const allDocs = getAllDocuments(window); for (let ctx of allDocs) { // 必须传 ['a', 'span', 'li'] 作为选择器,'加班餐' 作为文本 const btn = findInSpecificDocument(ctx.doc, ['a', 'span', 'li'], '加班餐'); if (btn && btn.offsetParent !== null) { // 元素存在且可见 subMenuTarget = btn; subMenuTarget._ownerWindow = ctx.win; // 绑定上下文 break; } } if (subMenuTarget) { log('👉 菜单已展开,直接点击'); await safeClick(subMenuTarget, '加班餐菜单'); } else { log('📂 需要展开菜单...'); const menuBtn = await waitForElement(['[menu-id="2000"]', 'span'], '订单管理'); await safeClick(menuBtn, '订单管理'); // 展开后再找一次 const subMenuBtn = await waitForElement(['a', 'span', 'li'], '加班餐'); await safeClick(subMenuBtn, '加班餐菜单'); } // 2. 添加 const addBtn = await waitForElement(['button[key="overtime_meal_add"]'], '加班餐添加'); await safeClick(addBtn, '加班餐添加'); // 3. 人员弹窗 log('等待弹窗...'); await sleep(2000); const addPersonBtn = await waitForElement(['button:contains("添加人员")', '/html/body/div[1]/form/div[3]/div/button'], '添加人员'); await safeClick(addPersonBtn, '添加人员'); // 4. 输入姓名 log(`输入: ${targetName}`); const nameInput = await waitForElement(['#Employee_Name']); await safeInput(nameInput, targetName); // 5. 搜索 const searchBtn = await waitForElement(['button#query'], '搜索'); await safeClick(searchBtn, '搜索'); // 6. 精准勾选 (多重策略 - 针对第三层弹窗) log('定位勾选...'); await sleep(1500); // 核心思路: // 1. 用户提供的精确路径 (包含 layui-col-md9 结构,这是弹窗特有的左右布局) // 2. 结合名字过滤的弹窗特有路径 // 3. 兜底策略 const checkTarget = await waitForElement([ // 策略A:用户提供的绝对路径 (修正为 CSS 选择器适配) `body > div.layui-row.layui-col-space15 > div.layui-col-md9 > div > div > div > div > div.layui-table-box > div.layui-table-body.layui-table-main > table > tbody > tr > td.layui-table-col-special > div > div > i`, // 策略B:基于名字 + 弹窗特有结构 (layui-col-md9 是弹窗右侧内容区的特征) `//div[contains(@class, 'layui-col-md9')]//tr[contains(., '${targetName}')]//div[contains(@class, 'layui-form-checkbox')]`, // 策略C:基于名字 + 简单的弹窗结构 `//div[contains(@class, 'layui-col-md9')]//tr[contains(., '${targetName}')]//i[contains(@class, 'layui-icon-ok')]` ], '勾选框 (第三层)'); const currentDoc = checkTarget.ownerDocument; // 锁定上下文 await safeClick(checkTarget, '勾选'); // 7. 确定 (上下文锁定) log('寻找确定...'); await sleep(1000); let confirmBtn = findInSpecificDocument(currentDoc, [ '/html/body/div[1]/div/form/div/div[4]/button[3]', 'button[onclick="define()"]' ]); if (!confirmBtn) confirmBtn = await waitForElement(['button[onclick="define()"]'], '确定'); await safeClick(confirmBtn, '确定按钮'); // 8. 保存 log('提交保存...'); await sleep(1500); const submitBtn = await waitForElement(['button#submit'], '保存'); await safeClick(submitBtn, '保存提交'); } function showCloseButton() { const btn = document.createElement('button'); btn.textContent = '❌ 任务完成,点击关闭页面'; btn.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 2147483647; padding: 30px 60px; font-size: 24px; background: #ff7675; color: white; border: none; border-radius: 15px; box-shadow: 0 10px 40px rgba(0,0,0,0.3); cursor: pointer; `; btn.onclick = () => { window.close(); // 尝试关闭 // 如果 window.close 被浏览器拦截,提示手动关闭 btn.textContent = '浏览器限制,请手动关闭标签页 X'; }; document.body.appendChild(btn); } // ================= 初始化入口 ================= setTimeout(() => { if (window.self !== window.top) return; const host = window.location.hostname; if (host.includes(CONFIG.URLS.OA)) { // OA 页面:负责显示控制台和定时任务 const t = setInterval(() => { if (document.body) { clearInterval(t); initOA(); } }, 500); } else if (host.includes(CONFIG.URLS.ORDER)) { // 点餐页面:负责执行队列 processOrderQueue(); } }, 1000); })();