您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
【最终完全版】新增调试模式开关,适配多校区场地命名。12:30定时抢场,支持场地优先级,全自动提交。
// ==UserScript== // @name 深圳大学体育场馆预约助手 // @namespace http://tampermonkey.net/ // @version 9.0 // @description 【最终完全版】新增调试模式开关,适配多校区场地命名。12:30定时抢场,支持场地优先级,全自动提交。 // @author Your Name // @match https://ehall.szu.edu.cn/qljfwapp/sys/lwSzuCgyy/index.do* // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @license MIT // ==/UserScript== (function() { 'use strict'; // --- 1. 全局常量与变量 --- const sportsByCampus = { "粤海校区": ["羽毛球", "足球", "排球", "网球", "篮球", "壁球", "一楼重量型健身", "二楼有氧健身", "游泳"], "丽湖校区": ["羽毛球", "排球", "网球", "篮球", "游泳", "乒乓球", "舞蹈", "桌球", "骑行"] }; const TIME_SLOTS = [ "08:00-09:00", "09:00-10:00", "10:00-11:00", "11:00-12:00", "12:00-13:00", "13:00-14:00", "14:00-15:00", "15:00-16:00", "16:00-17:00", "17:00-18:00", "18:00-19:00", "19:00-20:00", "20:00-21:00", "21:00-22:00" ]; const TARGET_HOUR = 12; const TARGET_MINUTE = 30; const TARGET_SECOND = 0; let actionQueue = []; let observer = null; let statusElement = null; let countdownInterval = null; // --- 2. 样式定义 --- GM_addStyle(` #szu-helper-panel { position: fixed; top: 60px; right: 20px; width: 260px; background-color: #ffffff; border: 1px solid #e0e0e0; border-radius: 8px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); z-index: 9999; padding: 20px; font-family: "Microsoft YaHei", "Helvetica Neue", Helvetica, Arial, sans-serif; } #szu-helper-panel h3 { margin: 0 0 15px 0; font-size: 18px; color: #a20a47; text-align: center; border-bottom: 1px solid #f0f0f0; padding-bottom: 10px; } .szu-helper-group { margin-bottom: 12px; } #szu-helper-panel label { display: block; margin-bottom: 6px; font-size: 14px; color: #333; } #szu-helper-panel select, #szu-helper-panel input[type="text"], #szu-helper-panel input[type="date"] { width: 100%; padding: 8px; border-radius: 4px; box-sizing: border-box; border: 1px solid #ccc; font-size: 14px; } #szu-helper-panel select:disabled { background-color: #f5f5f5; cursor: not-allowed; } #szu-helper-panel .helper-note { font-size: 11px; color: #999; margin-top: 4px; } #szu-helper-panel button { width: 100%; padding: 10px; margin-top: 10px; border: none; border-radius: 4px; background-color: #a20a47; color: white; font-size: 16px; cursor: pointer; transition: background-color 0.3s ease; } #szu-helper-panel button:hover { background-color: #8e093d; } #szu-helper-panel button:disabled { background-color: #ccc; cursor: not-allowed; } #szu-helper-status { margin-top: 12px; font-size: 12px; color: #666; text-align: center; min-height: 16px; line-height: 1.4; } .debug-switch { display: flex; align-items: center; justify-content: space-between; margin-top: 15px; } .debug-switch label { margin-bottom: 0; } .switch { position: relative; display: inline-block; width: 44px; height: 24px; } .switch input { opacity: 0; width: 0; height: 0; } .slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; transition: .4s; border-radius: 24px; } .slider:before { position: absolute; content: ""; height: 18px; width: 18px; left: 3px; bottom: 3px; background-color: white; transition: .4s; border-radius: 50%; } input:checked + .slider { background-color: #a20a47; } input:checked + .slider:before { transform: translateX(20px); } `); // --- 3. 创建操作面板 --- function createPanel() { const panel = document.createElement('div'); panel.id = 'szu-helper-panel'; panel.innerHTML = ` <h3>深大场馆助手 V9</h3> <div class="szu-helper-group"> <label for="campus-select">① 选择校区</label> <select id="campus-select"> <option value="">-- 必须选择 --</option> <option value="粤海校区">粤海校区</option> <option value="丽湖校区">丽湖校区</option> </select> </div> <div class="szu-helper-group"> <label for="sport-select">② 选择项目</label> <select id="sport-select" disabled><option>-- 先选校区 --</option></select> </div> <div class="szu-helper-group" id="court-group" style="display: none;"> <label for="court-select">⑤ 选择场地</label> <input type="text" id="court-select" placeholder="例: A3 或 至畅1 (按顺序抢)"> <div class="helper-note">用空格分隔,按顺序查找可用场地</div> </div> <div class="szu-helper-group"> <label for="date-select">③ 选择日期</label> <input type="date" id="date-select"> </div> <div class="szu-helper-group"> <label for="time-select">④ 选择时间段</label> <select id="time-select"></select> </div> <button id="confirm-btn">一键预约</button> <div class="debug-switch"> <label for="debug-mode">调试模式</label> <label class="switch"> <input type="checkbox" id="debug-mode"> <span class="slider"></span> </label> </div> <div id="szu-helper-status"></div> `; document.body.appendChild(panel); const elements = { campus: document.getElementById('campus-select'), sport: document.getElementById('sport-select'), date: document.getElementById('date-select'), time: document.getElementById('time-select'), courtGroup: document.getElementById('court-group'), court: document.getElementById('court-select'), btn: document.getElementById('confirm-btn'), debug: document.getElementById('debug-mode'), }; statusElement = document.getElementById('szu-helper-status'); // 绑定实时保存事件 elements.campus.addEventListener('change', () => { GM_setValue('selectedCampus', elements.campus.value); updateSportsDropdown(elements); elements.sport.value = ''; GM_setValue('selectedSport', ''); toggleCourtSelection(elements); }); elements.sport.addEventListener('change', () => { GM_setValue('selectedSport', elements.sport.value); toggleCourtSelection(elements); }); elements.date.addEventListener('change', () => GM_setValue('selectedDate', elements.date.value)); elements.time.addEventListener('change', () => GM_setValue('selectedTime', elements.time.value)); elements.court.addEventListener('input', () => GM_setValue('selectedCourt', elements.court.value)); elements.debug.addEventListener('change', () => GM_setValue('debugMode', elements.debug.checked)); elements.btn.addEventListener('click', () => handleConfirmClick(elements)); populateTimeSlots(elements.time); loadSavedChoices(elements); } // --- 4. 动态更新与初始化 --- function toggleCourtSelection(elements) { if (elements.sport.value === '羽毛球') { elements.courtGroup.style.display = 'block'; } else { elements.courtGroup.style.display = 'none'; } } function updateSportsDropdown(elements) { const selectedCampus = elements.campus.value; const sports = sportsByCampus[selectedCampus] || []; elements.sport.innerHTML = ''; if (selectedCampus) { elements.sport.disabled = false; elements.sport.add(new Option("-- 可选,不选则不点击 --", "")); sports.forEach(sport => elements.sport.add(new Option(sport, sport))); } else { elements.sport.disabled = true; elements.sport.add(new Option("-- 请先选校区 --", "")); } } function populateTimeSlots(timeSelect) { timeSelect.add(new Option("-- 可选,不选则不点击 --", "")); TIME_SLOTS.forEach(time => timeSelect.add(new Option(time, time))); } function loadSavedChoices(elements) { const savedCampus = GM_getValue('selectedCampus', ''); if (savedCampus) { elements.campus.value = savedCampus; updateSportsDropdown(elements); elements.sport.value = GM_getValue('selectedSport', ''); } const savedDate = GM_getValue('selectedDate'); if (savedDate) { elements.date.value = savedDate; } else { const now = new Date(); const targetTimeToday = new Date(); targetTimeToday.setHours(TARGET_HOUR, TARGET_MINUTE, TARGET_SECOND, 0); let defaultDate = new Date(); if (now < targetTimeToday) { defaultDate.setDate(defaultDate.getDate() + 1); } elements.date.value = defaultDate.toISOString().split('T')[0]; } elements.time.value = GM_getValue('selectedTime', ''); elements.court.value = GM_getValue('selectedCourt', ''); elements.debug.checked = GM_getValue('debugMode', false); toggleCourtSelection(elements); // 初始化时检查是否显示场地选择 } // --- 5. 核心逻辑:定时与瞬时操作 --- function handleConfirmClick(elements) { const now = new Date(); const targetTimeToday = new Date(); targetTimeToday.setHours(TARGET_HOUR, TARGET_MINUTE, TARGET_SECOND, 0); if (now < targetTimeToday && (now.getHours() < TARGET_HOUR || (now.getHours() === TARGET_HOUR && now.getMinutes() < TARGET_MINUTE))) { elements.btn.disabled = true; statusElement.textContent = '已进入定时抢场模式...'; statusElement.style.color = 'blue'; countdownInterval = setInterval(() => { const currentTime = new Date(); const remaining = targetTimeToday.getTime() - currentTime.getTime(); if (remaining <= 0) { clearInterval(countdownInterval); elements.btn.disabled = false; statusElement.textContent = '时间到!开始执行预约...'; startBookingProcess(elements); } else { const hours = Math.floor((remaining / (1000 * 60 * 60)) % 24); const minutes = Math.floor((remaining / 1000 / 60) % 60); const seconds = Math.floor((remaining / 1000) % 60); statusElement.innerHTML = `等待抢场...<br>剩余: ${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`; } }, 1000); } else { startBookingProcess(elements); } } function startBookingProcess(elements) { const choices = { campus: elements.campus.value, sport: elements.sport.value, date: elements.date.value, time: elements.time.value, court: elements.court.value.trim(), debug: elements.debug.checked }; if (!choices.campus) { statusElement.textContent = '⚠️ 请必须选择一个校区!'; statusElement.style.color = 'orange'; return; } actionQueue = []; actionQueue.push({ description: `校区“${choices.campus}”`, find: () => Array.from(document.querySelectorAll('div.bh-btn.bh-btn-primary, div.campus-tab-default')).find(btn => btn.textContent.trim() === choices.campus) }); if (choices.sport) { actionQueue.push({ description: `项目“${choices.sport}”`, find: () => Array.from(document.querySelectorAll('div.frame-4, div.frame-44')).find(el => el.textContent.trim().includes(choices.sport)) }); } if (choices.date) { actionQueue.push({ description: `日期“${choices.date}”`, find: () => document.querySelector(`label[for="${choices.date}"]`) }); } if (choices.time) { actionQueue.push({ description: `时间段“${choices.time}”`, find: () => document.querySelector(`label[for="${choices.time}"]`) }); } if (choices.sport === '羽毛球' && choices.court) { actionQueue.push({ description: `场地 (优先级: ${choices.court})`, find: () => findAvailableCourtByPriority(choices.court) }); } if (!choices.debug) { actionQueue.push({ description: '“提交预约”按钮', find: () => Array.from(document.querySelectorAll('button.bh-btn')).find(btn => btn.textContent.trim() === '提交预约') }); } statusElement.textContent = '🚀 任务已启动,开始监控页面...'; startObserver(); processActionQueue(); } function findAvailableCourtByPriority(priorityList) { const allCourtInputs = document.querySelectorAll('.rectangle-3 input[type="radio"][value*="羽毛球"]'); if (allCourtInputs.length === 0) return null; const preferences = priorityList.trim().toUpperCase().split(/\s+/).filter(p => p); for (const pref of preferences) { const isAnyMode = (pref === 'ANY' || pref === '任意'); for (const input of allCourtInputs) { const label = document.querySelector(`label[for="${input.id}"]`); if (!label || !label.textContent.includes('(可预约)')) continue; if (isAnyMode) return label; const courtName = input.value.toUpperCase(); if (courtName.includes(pref)) return label; } } return null; } function startObserver() { if (observer) observer.disconnect(); observer = new MutationObserver(() => window.requestAnimationFrame(processActionQueue)); observer.observe(document.body, { childList: true, subtree: true }); } function processActionQueue() { if (actionQueue.length === 0) { if (observer) observer.disconnect(); return; } const currentAction = actionQueue[0]; const elementToClick = currentAction.find(); if (elementToClick) { const style = window.getComputedStyle(elementToClick); if (style.display === 'none' || style.visibility === 'hidden') return; const text = elementToClick.textContent; if (text.includes('(已满员)') || text.includes('(无开放场地)')) { statusElement.textContent = `❌ ${currentAction.description} 不可预约,任务中止。`; statusElement.style.color = 'red'; if (observer) observer.disconnect(); actionQueue = []; return; } statusElement.textContent = `✅ 找到并点击 ${currentAction.description}`; statusElement.style.color = 'green'; elementToClick.click(); actionQueue.shift(); if (actionQueue.length > 0) { statusElement.textContent = `...正在准备下一步: ${actionQueue[0].description}`; } else { statusElement.textContent = '🎉 所有操作已成功完成!'; if (observer) observer.disconnect(); } } } // --- 6. 脚本入口 --- window.addEventListener('load', createPanel); })();