// ==UserScript== // @name 自动点餐系统 Pro (V9.0 多人队列+定时版) // @namespace http://tampermonkey.net/ // @version 9.0 // @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'; // ================= 配置区域 ================= 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; } } } // ================= 核心工具函数 ================= const sleep = (ms) => new Promise(r => setTimeout(r, ms)); // 递归获取 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) { const start = Date.now(); if (!Array.isArray(selectors)) selectors = [selectors]; log(`🔍 查找: ${textContent || selectors[0]}`); while (Date.now() - start < CONFIG.TIMEOUTS.MAX_WAIT) { 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 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; // 创建调试面板 const 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('✅ 本次成功,准备下一位...', 'success'); await sleep(2000); window.location.reload(); // 刷新页面以重置状态 } catch (error) { log(`❌ ${currentPerson} 点餐失败: ${error.message}`, 'error'); // 失败也移除?还是重试?这里选择移除防止卡死 state.queue.shift(); GM_setValue('ORDER_QUEUE', state); setTimeout(() => window.location.reload(), 3000); } } // 单人点餐流程 (基于 V8.2 验证通过的逻辑) async function executeSingleOrder(targetName) { await sleep(CONFIG.TIMEOUTS.PAGE_LOAD); // 1. 菜单 const menuBtn = await waitForElement(['[menu-id="2000"]', 'span'], '订单管理'); await safeClick(menuBtn, '订单管理'); let subMenuBtn = null; try { subMenuBtn = await waitForElement(['a', 'span', 'li'], '加班餐'); } catch(e) { await safeClick(menuBtn, '重试展开'); // 容错重试 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(2000); const checkIcon = await waitForElement([ `//tr[contains(., '${targetName}')]//i[contains(@class, 'layui-icon-ok')]` ]); const currentDoc = checkIcon.ownerDocument; // 锁定上下文 await safeClick(checkIcon, '勾选'); // 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); })();