您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
深圳大学体育场馆自动预约脚本 - iOS、安卓、移动端、桌面端完全兼容
// ==UserScript== // @name 深圳大学体育场馆自动抢票 // @namespace http://tampermonkey.net/ // @version 1.1.5 // @description 深圳大学体育场馆自动预约脚本 - iOS、安卓、移动端、桌面端完全兼容 // @author zskfree // @match https://ehall.szu.edu.cn/qljfwapp/sys/lwSzuCgyy/* // @match https://ehall-443.webvpn.szu.edu.cn/qljfwapp/sys/lwSzuCgyy/* // @icon 🎾 // @grant none // @run-at document-end // @license MIT // ==/UserScript== (function () { 'use strict'; // 更精确的设备检测 const userAgent = navigator.userAgent; const isMobile = /iPhone|iPad|iPod|Android|Mobile/i.test(userAgent); const isIOS = /iPhone|iPad|iPod/i.test(userAgent); const isIPad = /iPad/i.test(userAgent) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1); // 修改触摸设备检测逻辑,优先判断移动设备 const isTouchDevice = isMobile || isIPad || (navigator.maxTouchPoints > 0 && /Android|Mobile/i.test(userAgent)); console.log('设备检测:', { isMobile, isIOS, isIPad, isTouchDevice }); // 替换现有的 Storage 对象 const Storage = { prefix: 'szu_sports_', maxAge: 7 * 24 * 60 * 60 * 1000, // 7天 compressionThreshold: 1024, // 1KB以上进行压缩 set: function(key, value) { const fullKey = this.prefix + key; const data = { value: value, timestamp: Date.now(), version: '1.1.5' }; let serializedData = JSON.stringify(data); // 如果数据较大,尝试压缩(简单压缩) if (serializedData.length > this.compressionThreshold) { try { // 移除重复的空格和换行符 serializedData = JSON.stringify(data, null, 0); } catch (e) { console.warn('数据压缩失败:', e); } } // 尝试 localStorage try { localStorage.setItem(fullKey, serializedData); return true; } catch (e) { console.warn('localStorage 存储失败:', e); // 清理过期数据后重试 try { this.cleanup(); localStorage.setItem(fullKey, serializedData); return true; } catch (e2) { console.warn('清理后重试失败,尝试 sessionStorage'); // 回退到 sessionStorage try { sessionStorage.setItem(fullKey, serializedData); return true; } catch (e3) { console.warn('sessionStorage 也失败,使用内存存储'); // 最后回退到 Map 结构的内存存储 if (!window.memoryStorage) { window.memoryStorage = new Map(); } window.memoryStorage.set(fullKey, data); return true; } } } }, get: function(key, defaultValue = null) { const fullKey = this.prefix + key; // 尝试 localStorage try { const item = localStorage.getItem(fullKey); if (item !== null) { const data = JSON.parse(item); // 检查版本兼容性 if (data.version && data.version !== '1.1.5') { console.warn(`配置版本不匹配: ${data.version} -> 1.1.5,使用默认值`); this.remove(key); // 清理旧版本数据 return defaultValue; } // 检查数据是否过期 if (data.timestamp && Date.now() - data.timestamp > this.maxAge) { console.warn(`数据已过期: ${key}`); this.remove(key); return defaultValue; } return data.value !== undefined ? data.value : data; // 兼容旧格式 } } catch (e) { console.warn('读取 localStorage 失败:', e); this.remove(key); // 清理损坏的数据 } // 尝试 sessionStorage try { const item = sessionStorage.getItem(fullKey); if (item !== null) { const data = JSON.parse(item); return data.value !== undefined ? data.value : data; } } catch (e) { console.warn('读取 sessionStorage 失败:', e); } // 尝试内存存储 if (window.memoryStorage && window.memoryStorage.has && window.memoryStorage.has(fullKey)) { const data = window.memoryStorage.get(fullKey); return data.value !== undefined ? data.value : data; } else if (window.memoryStorage && window.memoryStorage[fullKey] !== undefined) { // 兼容旧版本的对象格式 return window.memoryStorage[fullKey]; } return defaultValue; }, remove: function(key) { const fullKey = this.prefix + key; try { localStorage.removeItem(fullKey); } catch (e) { console.warn('清理 localStorage 失败:', e); } try { sessionStorage.removeItem(fullKey); } catch (e) { console.warn('清理 sessionStorage 失败:', e); } if (window.memoryStorage) { if (window.memoryStorage.delete) { window.memoryStorage.delete(fullKey); } else { delete window.memoryStorage[fullKey]; } } }, // 清理过期数据 cleanup: function() { const now = Date.now(); let cleanedCount = 0; // 清理 localStorage try { for (let i = localStorage.length - 1; i >= 0; i--) { const key = localStorage.key(i); if (key && key.startsWith(this.prefix)) { try { const data = JSON.parse(localStorage.getItem(key)); if (data.timestamp && now - data.timestamp > this.maxAge) { localStorage.removeItem(key); cleanedCount++; } } catch (e) { // 损坏的数据,直接删除 localStorage.removeItem(key); cleanedCount++; } } } } catch (e) { console.warn('清理 localStorage 失败:', e); } // 清理 sessionStorage 中的过期数据 try { for (let i = sessionStorage.length - 1; i >= 0; i--) { const key = sessionStorage.key(i); if (key && key.startsWith(this.prefix)) { try { const data = JSON.parse(sessionStorage.getItem(key)); if (data.timestamp && now - data.timestamp > this.maxAge) { sessionStorage.removeItem(key); cleanedCount++; } } catch (e) { sessionStorage.removeItem(key); cleanedCount++; } } } } catch (e) { console.warn('清理 sessionStorage 失败:', e); } if (cleanedCount > 0) { console.log(`清理了 ${cleanedCount} 个过期数据项`); } return cleanedCount; }, // 获取存储使用情况 getStorageInfo: function() { const info = { localStorage: { used: 0, available: false }, sessionStorage: { used: 0, available: false }, memoryStorage: { used: 0, available: false } }; // 检查 localStorage try { const testKey = this.prefix + 'storage_test'; localStorage.setItem(testKey, 'test'); localStorage.removeItem(testKey); info.localStorage.available = true; // 计算使用量 let usedSize = 0; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key && key.startsWith(this.prefix)) { usedSize += localStorage.getItem(key).length; } } info.localStorage.used = usedSize; } catch (e) { info.localStorage.available = false; } // 检查 sessionStorage try { const testKey = this.prefix + 'storage_test'; sessionStorage.setItem(testKey, 'test'); sessionStorage.removeItem(testKey); info.sessionStorage.available = true; let usedSize = 0; for (let i = 0; i < sessionStorage.length; i++) { const key = sessionStorage.key(i); if (key && key.startsWith(this.prefix)) { usedSize += sessionStorage.getItem(key).length; } } info.sessionStorage.used = usedSize; } catch (e) { info.sessionStorage.available = false; } // 检查内存存储 if (window.memoryStorage) { info.memoryStorage.available = true; if (window.memoryStorage.size) { info.memoryStorage.used = window.memoryStorage.size; } else { info.memoryStorage.used = Object.keys(window.memoryStorage).length; } } return info; } }; // 在现有 Storage 对象后添加网络错误处理器 const NetworkErrorHandler = { // 错误类型分类 categorizeError: function (error, response = null) { if (response) { if (response.status === 429) return 'rate_limit'; if (response.status >= 500) return 'server_error'; if (response.status === 401 || response.status === 403) return 'auth_error'; if (response.status === 404) return 'not_found'; if (response.status >= 400) return 'client_error'; } if (error.name === 'AbortError') return 'timeout'; if (error.message.includes('网络')) return 'network_error'; if (error.message.includes('超时')) return 'timeout'; return 'unknown_error'; }, // 根据错误类型决定是否应该重试 shouldRetry: function (errorType, retryCount = 0) { const maxRetries = { 'rate_limit': 3, 'server_error': 5, 'network_error': 3, 'timeout': 3, 'unknown_error': 2 }; const noRetry = ['auth_error', 'not_found', 'client_error']; if (noRetry.includes(errorType)) return false; return retryCount < (maxRetries[errorType] || 1); }, // 获取重试延迟时间 getRetryDelay: function (errorType, retryCount = 0) { const baseDelays = { 'rate_limit': 5000, // 5秒 'server_error': 3000, // 3秒 'network_error': 2000, // 2秒 'timeout': 1000, // 1秒 'unknown_error': 2000 // 2秒 }; const baseDelay = baseDelays[errorType] || 2000; // 指数退避,但有上限 return Math.min(baseDelay * Math.pow(1.5, retryCount), 30000); }, // 处理网络错误的统一方法 handleError: async function (error, response = null, retryCount = 0, operation = 'request') { const errorType = this.categorizeError(error, response); // 记录错误日志 const errorMsg = response ? `HTTP ${response.status}: ${response.statusText || '网络错误'}` : error.message; addLog(`❌ ${operation}失败: ${errorMsg}`, 'error'); // 特殊错误处理 switch (errorType) { case 'auth_error': addLog(`🔐 认证失败,请检查登录状态`, 'error'); if (isRunning) stopBooking(); return { shouldStop: true, shouldRetry: false }; case 'rate_limit': addLog(`⏰ 请求过于频繁,等待${this.getRetryDelay(errorType, retryCount) / 1000}秒后重试`, 'warning'); break; case 'server_error': addLog(`🔧 服务器错误,可能是系统维护`, 'warning'); break; case 'network_error': addLog(`🌐 网络连接异常,请检查网络`, 'warning'); break; case 'timeout': addLog(`⏰ 请求超时,可能是网络较慢`, 'warning'); break; } const shouldRetry = this.shouldRetry(errorType, retryCount); const retryDelay = shouldRetry ? this.getRetryDelay(errorType, retryCount) : 0; return { shouldStop: false, shouldRetry, retryDelay, errorType }; } }; // 在 NetworkErrorHandler 后添加请求频率控制器 const RequestThrottler = { requests: [], maxRequestsPerSecond: 2, // 每秒最大请求数 maxConcurrentRequests: 3, // 最大并发请求数 currentRequests: 0, // 当前进行中的请求数 adaptiveMode: true, // 自适应模式 // 清理过期的请求记录 cleanup: function () { const now = Date.now(); this.requests = this.requests.filter(time => now - time < 1000); }, // 检查是否可以发送请求 canMakeRequest: function () { this.cleanup(); return this.requests.length < this.maxRequestsPerSecond && this.currentRequests < this.maxConcurrentRequests; }, // 获取需要等待的时间 getWaitTime: function () { if (this.currentRequests >= this.maxConcurrentRequests) { return 1000; // 等待1秒 } this.cleanup(); if (this.requests.length >= this.maxRequestsPerSecond) { const oldestRequest = Math.min(...this.requests); return Math.max(0, 1000 - (Date.now() - oldestRequest)); } return 0; }, // 自适应调整频率限制 adaptFrequency: function (success = true, responseTime = 0) { if (!this.adaptiveMode) return; if (success && responseTime < 1000) { // 请求成功且响应快,可以适当提高频率 this.maxRequestsPerSecond = Math.min(this.maxRequestsPerSecond + 0.1, 3); } else if (!success || responseTime > 3000) { // 请求失败或响应慢,降低频率 this.maxRequestsPerSecond = Math.max(this.maxRequestsPerSecond - 0.2, 1); } }, // 请求开始时调用 onRequestStart: function () { this.requests.push(Date.now()); this.currentRequests++; }, // 请求结束时调用 onRequestEnd: function (success = true, responseTime = 0) { this.currentRequests = Math.max(0, this.currentRequests - 1); this.adaptFrequency(success, responseTime); }, // 等待直到可以发送请求 waitForSlot: async function () { while (!this.canMakeRequest()) { const waitTime = this.getWaitTime(); if (waitTime > 0) { await new Promise(resolve => setTimeout(resolve, waitTime)); } } }, // 重置频率限制(在错误后使用) reset: function () { this.requests = []; this.currentRequests = 0; this.maxRequestsPerSecond = 2; addLog(`🔄 请求频率已重置`, 'info'); } }; // 在 RequestThrottler 后添加智能重试机制 const SmartRetry = { consecutiveFailures: 0, lastSuccessTime: Date.now(), baseInterval: 1000, // 基础间隔1秒 maxInterval: 30000, // 最大间隔30秒 adaptiveMode: true, // 重置重试状态 reset: function () { this.consecutiveFailures = 0; this.lastSuccessTime = Date.now(); this.baseInterval = CONFIG.RETRY_INTERVAL * 1000; addLog(`🔄 重试机制已重置`, 'info'); }, // 记录成功 onSuccess: function () { if (this.consecutiveFailures > 0) { addLog(`✅ 恢复正常,重置重试策略`, 'success'); } this.consecutiveFailures = 0; this.lastSuccessTime = Date.now(); }, // 记录失败 onFailure: function (errorType = 'unknown') { this.consecutiveFailures++; // 根据错误类型调整策略 if (errorType === 'rate_limit') { this.consecutiveFailures = Math.min(this.consecutiveFailures + 2, 10); // 限频错误加重惩罚 } else if (errorType === 'network_error') { this.consecutiveFailures = Math.min(this.consecutiveFailures + 1, 8); } }, // 获取下一次重试间隔 getNextInterval: function () { if (this.consecutiveFailures === 0) { return this.baseInterval; } // 指数退避,但有上限 const backoffMultiplier = Math.min(Math.pow(1.5, this.consecutiveFailures), 20); const interval = Math.min(this.baseInterval * backoffMultiplier, this.maxInterval); // 添加随机抖动,避免所有客户端同时重试 const jitter = Math.random() * 0.3 + 0.85; // 85%-115%的随机抖动 return Math.floor(interval * jitter); }, // 判断是否应该继续重试 - 修改为始终返回true shouldContinue: function () { // 只在连续失败过多时给出提示,但不停止 if (this.consecutiveFailures >= 15) { addLog(`⚠️ 连续失败${this.consecutiveFailures}次,但继续尝试`, 'warning'); } // 移除长时间无成功的限制,只给出提示 const timeSinceLastSuccess = Date.now() - this.lastSuccessTime; if (timeSinceLastSuccess > 10 * 60 * 1000) { // 10分钟 addLog(`⏰ 超过10分钟无成功响应,继续尝试中...`, 'warning'); } // 始终返回true,让程序按照用户设置的MAX_RETRY_TIMES运行 return true; }, // 获取重试建议 - 移除暂停机制,直接按参数运行 getRetryAdvice: function () { return { shouldPause: false, pauseDuration: 0, message: '按设定参数持续运行' }; }, // 动态调整重试间隔 updateInterval: function () { if (!this.adaptiveMode) return; // 根据当前时间调整间隔 const hour = new Date().getHours(); if (hour >= 12 && hour <= 13) { // 高峰期适当延长间隔 this.baseInterval = Math.max(CONFIG.RETRY_INTERVAL * 1000, 2000); } else { this.baseInterval = CONFIG.RETRY_INTERVAL * 1000; } } }; // 添加移动端专用功能 const MobileOptimization = { wakeLock: null, isVisible: true, lastActivity: Date.now(), heartbeatInterval: null, // 初始化移动端优化 init: function() { if (!isMobile) return; addLog(`📱 启用移动端优化`, 'info'); // 请求屏幕唤醒锁 this.requestWakeLock(); // 监听页面可见性变化 this.setupVisibilityMonitor(); // 启动心跳机制 this.startHeartbeat(); // 监听电池状态(如果支持) this.setupBatteryMonitor(); // 设置触摸反馈 this.setupTouchFeedback(); // 优化滚动性能 this.optimizeScrolling(); }, // 请求屏幕唤醒锁 requestWakeLock: async function() { if ('wakeLock' in navigator) { try { this.wakeLock = await navigator.wakeLock.request('screen'); addLog(`🔆 屏幕保持唤醒已启用`, 'success'); this.wakeLock.addEventListener('release', () => { addLog(`😴 屏幕唤醒锁已释放`, 'warning'); // 如果还在运行,尝试重新获取 if (isRunning) { setTimeout(() => this.requestWakeLock(), 1000); } }); } catch (err) { addLog(`⚠️ 无法获取屏幕唤醒锁: ${err.message}`, 'warning'); } } else { addLog(`📱 当前浏览器不支持屏幕唤醒锁`, 'info'); } }, // 释放屏幕唤醒锁 releaseWakeLock: function() { if (this.wakeLock) { this.wakeLock.release(); this.wakeLock = null; } }, // 设置页面可见性监听 setupVisibilityMonitor: function() { document.addEventListener('visibilitychange', () => { if (document.hidden) { this.isVisible = false; addLog(`📱 页面进入后台`, 'info'); // 如果正在运行,增加心跳频率 if (isRunning && this.heartbeatInterval) { clearInterval(this.heartbeatInterval); this.startHeartbeat(5000); // 5秒心跳 } } else { this.isVisible = true; addLog(`📱 页面回到前台`, 'info'); this.lastActivity = Date.now(); // 恢复正常心跳 if (this.heartbeatInterval) { clearInterval(this.heartbeatInterval); this.startHeartbeat(); } // 重新请求唤醒锁 if (isRunning) { this.requestWakeLock(); } } }); }, // 启动心跳机制 startHeartbeat: function(interval = 30000) { this.heartbeatInterval = setInterval(() => { if (isRunning) { this.lastActivity = Date.now(); // 触发一个微小的DOM操作,保持页面活跃 const statusArea = document.getElementById('status-area'); if (statusArea) { statusArea.style.opacity = statusArea.style.opacity || '1'; } // 检查网络连接 if (!navigator.onLine) { addLog(`📶 网络连接已断开`, 'error'); } else if (!this.isVisible) { // 只在后台时显示心跳日志 addLog(`💓 后台运行正常`, 'info'); } } }, interval); }, // 停止心跳机制 stopHeartbeat: function() { if (this.heartbeatInterval) { clearInterval(this.heartbeatInterval); this.heartbeatInterval = null; } }, // 设置电池监听 setupBatteryMonitor: function() { if ('getBattery' in navigator) { navigator.getBattery().then((battery) => { const updateBatteryInfo = () => { const level = Math.round(battery.level * 100); const charging = battery.charging; if (level <= 20 && !charging) { addLog(`🔋 电池电量较低 (${level}%),建议连接充电器`, 'warning'); } else if (level <= 10 && !charging) { addLog(`🔋 电池电量严重不足 (${level}%),可能影响抢票`, 'error'); } }; // 初始检查 updateBatteryInfo(); // 监听电池变化 battery.addEventListener('levelchange', updateBatteryInfo); battery.addEventListener('chargingchange', updateBatteryInfo); }).catch(err => { console.log('电池 API 不可用:', err); }); } }, // 设置触摸反馈 setupTouchFeedback: function() { if (!isTouchDevice) return; // 为所有按钮添加触觉反馈(如果支持) const addHapticFeedback = (element) => { element.addEventListener('touchstart', () => { // 轻微的触觉反馈 if ('vibrate' in navigator) { navigator.vibrate(10); // 10ms轻微震动 } }, { passive: true }); }; // 应用到现有按钮 setTimeout(() => { const buttons = document.querySelectorAll('button'); buttons.forEach(addHapticFeedback); }, 100); }, // 优化滚动性能 optimizeScrolling: function() { if (!isMobile) return; const style = document.createElement('style'); style.textContent = ` /* 优化移动端滚动 */ #status-area { -webkit-overflow-scrolling: touch; overscroll-behavior: contain; } /* 防止iOS双击缩放 */ * { touch-action: manipulation; } /* 优化输入框 */ input, select, textarea { -webkit-user-select: auto; user-select: auto; } /* 防止长按选择文本 */ #auto-booking-panel { -webkit-user-select: none; user-select: none; -webkit-tap-highlight-color: transparent; } /* 允许输入区域选择文本 */ #auto-booking-panel input, #auto-booking-panel select { -webkit-user-select: auto; user-select: auto; } `; document.head.appendChild(style); }, // 处理长时间运行的页面冻结问题 preventPageFreeze: function() { if (!isMobile) return; // 定期执行一些轻量级操作防止页面冻结 setInterval(() => { if (isRunning) { // 创建一个微任务 Promise.resolve().then(() => { // 轻量级DOM操作 const now = Date.now(); document.body.setAttribute('data-activity', now.toString()); }); } }, 15000); // 每15秒执行一次 }, // 优化内存使用 optimizeMemory: function() { if (!isMobile) return; // 定期清理日志 setInterval(() => { const statusArea = document.getElementById('status-area'); if (statusArea && statusArea.children.length > 100) { // 保留最后50条日志 while (statusArea.children.length > 50) { statusArea.removeChild(statusArea.firstChild); } addLog(`🧹 已清理历史日志`, 'info'); } }, 60000); // 每分钟检查一次 }, // 清理资源 cleanup: function() { this.releaseWakeLock(); this.stopHeartbeat(); addLog(`📱 移动端优化已清理`, 'info'); } }; // 在 MobileOptimization 后添加错误恢复机制 const ErrorRecovery = { errorHistory: [], maxHistorySize: 50, recoveryStrategies: new Map(), // 初始化错误恢复机制 init: function() { // 注册恢复策略 this.registerStrategies(); // 监听全局错误 this.setupGlobalErrorHandler(); addLog(`🛡️ 错误恢复机制已启用`, 'info'); }, // 注册恢复策略 registerStrategies: function() { // 网络错误恢复 this.recoveryStrategies.set('network_error', { immediate: () => { addLog(`🌐 检测到网络错误,检查连接状态`, 'warning'); if (!navigator.onLine) { addLog(`📶 网络已断开,等待重新连接...`, 'error'); return false; } return true; }, delayed: async () => { // 等待3秒后重试 await new Promise(resolve => setTimeout(resolve, 3000)); RequestThrottler.reset(); return true; } }); // 认证错误恢复 this.recoveryStrategies.set('auth_error', { immediate: () => { addLog(`🔐 认证失败,建议刷新页面重新登录`, 'error'); return false; // 无法自动恢复 } }); // 频率限制恢复 this.recoveryStrategies.set('rate_limit', { immediate: () => { addLog(`⏰ 触发频率限制,启用保守模式`, 'warning'); RequestThrottler.maxRequestsPerSecond = 1; // 降低频率 return true; }, delayed: async () => { await new Promise(resolve => setTimeout(resolve, 10000)); // 等待10秒 RequestThrottler.maxRequestsPerSecond = 2; // 恢复正常频率 return true; } }); // 服务器错误恢复 this.recoveryStrategies.set('server_error', { immediate: () => { addLog(`🔧 服务器错误,可能是系统维护`, 'warning'); return false; }, delayed: async () => { await new Promise(resolve => setTimeout(resolve, 30000)); // 等待30秒 return true; } }); }, // 记录错误 recordError: function(error, context = {}) { const errorRecord = { timestamp: Date.now(), message: error.message || String(error), type: error.name || 'Unknown', context: context, stack: error.stack }; this.errorHistory.push(errorRecord); // 限制历史记录大小 if (this.errorHistory.length > this.maxHistorySize) { this.errorHistory.shift(); } return errorRecord; }, // 尝试恢复 attemptRecovery: async function(errorType, error, context = {}) { this.recordError(error, context); const strategy = this.recoveryStrategies.get(errorType); if (!strategy) { addLog(`❌ 未知错误类型: ${errorType}`, 'error'); return false; } // 尝试即时恢复 if (strategy.immediate) { try { const immediateResult = strategy.immediate(); if (immediateResult) { addLog(`✅ 即时恢复成功`, 'success'); return true; } } catch (e) { addLog(`❌ 即时恢复失败: ${e.message}`, 'error'); } } // 尝试延迟恢复 if (strategy.delayed) { try { addLog(`⏳ 尝试延迟恢复...`, 'info'); const delayedResult = await strategy.delayed(); if (delayedResult) { addLog(`✅ 延迟恢复成功`, 'success'); return true; } } catch (e) { addLog(`❌ 延迟恢复失败: ${e.message}`, 'error'); } } return false; }, // 设置全局错误处理 setupGlobalErrorHandler: function() { // 捕获未处理的Promise错误 window.addEventListener('unhandledrejection', (event) => { console.error('未处理的Promise错误:', event.reason); this.recordError(event.reason, { type: 'unhandledrejection' }); // 防止控制台报错 event.preventDefault(); }); // 捕获全局JavaScript错误 window.addEventListener('error', (event) => { console.error('全局JavaScript错误:', event.error); this.recordError(event.error, { type: 'javascript_error', filename: event.filename, lineno: event.lineno }); }); }, // 获取错误统计 getErrorStats: function() { const now = Date.now(); const last24Hours = this.errorHistory.filter(e => now - e.timestamp < 24 * 60 * 60 * 1000); const lastHour = this.errorHistory.filter(e => now - e.timestamp < 60 * 60 * 1000); const typeStats = {}; last24Hours.forEach(error => { const type = error.type || 'unknown'; typeStats[type] = (typeStats[type] || 0) + 1; }); return { total: this.errorHistory.length, last24Hours: last24Hours.length, lastHour: lastHour.length, typeStats: typeStats, latestErrors: this.errorHistory.slice(-5) }; } }; // 运动项目映射 const SPORT_CODES = { "羽毛球": "001", "排球": "003", "网球": "004", "篮球": "005", "游泳": "009", "乒乓球": "013", "桌球": "016" }; // 校区映射 const CAMPUS_CODES = { "粤海": "1", "丽湖": "2" }; // 时间段选项 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 VENUE_CODES = { "至畅": "104", "至快": "111" }; // 修改默认配置,确保每次都使用最新的明天日期 const DEFAULT_CONFIG = { USER_INFO: { YYRGH: "2300123999", YYRXM: "张三" }, TARGET_DATE: getTomorrowDate(), // 已经设置为明天 SPORT: "羽毛球", CAMPUS: "丽湖", PREFERRED_VENUE: "至畅", PREFERRED_TIMES: ["20:00-21:00", "21:00-22:00"], RETRY_INTERVAL: 1, MAX_RETRY_TIMES: 20000, REQUEST_TIMEOUT: 10, YYLX: "1.0" }; // 获取明天日期 function getTomorrowDate() { const tomorrow = new Date(); tomorrow.setDate(tomorrow.getDate() + 1); return tomorrow.toISOString().split('T')[0]; } // 修改保存和加载配置函数 function saveConfig(config) { Storage.set('bookingConfig', config); } // 修改加载配置函数,确保日期始终为明天 function loadConfig() { try { const saved = Storage.get('bookingConfig', null); const config = saved ? { ...DEFAULT_CONFIG, ...saved } : DEFAULT_CONFIG; // 始终更新为明天的日期,避免使用过期日期 config.TARGET_DATE = getTomorrowDate(); return config; } catch (e) { return DEFAULT_CONFIG; } } function savePanelState(isVisible) { Storage.set('panelVisible', isVisible); } function loadPanelState() { return Storage.get('panelVisible', true); } // 全局变量 let CONFIG = loadConfig(); let isRunning = false; let retryCount = 0; let startTime = null; let successfulBookings = []; let controlPanel = null; let floatingButton = null; let isPanelVisible = loadPanelState(); // 获取动态最大预约数量 function getMaxBookings() { const selectedTimeSlots = CONFIG.PREFERRED_TIMES.length; return Math.min(selectedTimeSlots, 2); // 最多2个,但不超过选择的时间段数量 } // 修改创建浮动按钮函数 - 完全重写触摸事件处理 function createFloatingButton() { const button = document.createElement('div'); button.id = 'floating-toggle-btn'; // iOS设备尺寸优化 const buttonSize = isIPad ? '80px' : (isMobile ? '70px' : '60px'); const fontSize = isIPad ? '32px' : (isMobile ? '28px' : '24px'); button.style.cssText = ` position: fixed; top: ${isMobile ? '20px' : '20px'}; right: ${isMobile ? '20px' : '20px'}; width: ${buttonSize}; height: ${buttonSize}; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; z-index: 10001; box-shadow: 0 4px 15px rgba(0,0,0,0.3); transition: all 0.3s ease; border: 3px solid rgba(255,255,255,0.2); font-size: ${fontSize}; user-select: none; -webkit-user-select: none; -webkit-touch-callout: none; -webkit-tap-highlight-color: transparent; touch-action: manipulation; `; button.innerHTML = '🎾'; button.title = '显示/隐藏抢票面板'; // 统一的点击处理函数 function handleButtonClick(e) { console.log('浮动按钮被点击,当前面板状态:', isPanelVisible); if (e) { e.preventDefault(); // 集中处理 preventDefault e.stopPropagation(); // 集中处理 stopPropagation } togglePanel(); } // 为 iPad 特别优化的事件处理 if (isTouchDevice) { let isPressed = false; let touchStartTime = 0; let hasMoved = false; let startX = 0, startY = 0; const pressThreshold = 800; // ms, 定义有效点击的最大时长 const moveThreshold = 10; // pixels, 定义手指移动多少算作移动而非点击 // 通用的按下处理逻辑 function onInteractionStart(clientX, clientY, pointerType = 'touch') { console.log(`浮动按钮 ${pointerType} start`); isPressed = true; touchStartTime = Date.now(); hasMoved = false; startX = clientX; startY = clientY; button.style.transform = 'scale(1.1)'; button.style.opacity = '0.8'; } // 通用的移动处理逻辑 function onInteractionMove(clientX, clientY) { if (!isPressed) return; if (!hasMoved) { if (Math.abs(clientX - startX) > moveThreshold || Math.abs(clientY - startY) > moveThreshold) { hasMoved = true; console.log('浮动按钮 moved'); } } } // 通用的抬起/结束处理逻辑 function onInteractionEnd(e, interactionType = 'touch') { console.log(`浮动按钮 ${interactionType} end`, { isPressed, hasMoved, duration: Date.now() - touchStartTime }); if (!isPressed) { // 如果没有按下状态,则重置并返回 button.style.transform = 'scale(1)'; button.style.opacity = '1'; return; } const pressDuration = Date.now() - touchStartTime; if (!hasMoved && pressDuration < pressThreshold) { console.log('浮动按钮 - TAP detected'); handleButtonClick(e); // 调用统一处理函数 } button.style.transform = 'scale(1)'; button.style.opacity = '1'; isPressed = false; hasMoved = false; } // 通用的取消处理逻辑 function onInteractionCancel() { console.log('浮动按钮 interaction cancel'); isPressed = false; hasMoved = false; button.style.transform = 'scale(1)'; button.style.opacity = '1'; } if (window.PointerEvent) { console.log('使用 Pointer 事件'); button.addEventListener('pointerdown', (e) => { if (!e.isPrimary || (e.pointerType !== 'touch' && e.pointerType !== 'pen')) return; onInteractionStart(e.clientX, e.clientY, e.pointerType); // 不在此处 e.preventDefault(),让滚动等默认行为可以发生,除非确定是点击 }); button.addEventListener('pointermove', (e) => { if (!e.isPrimary || (e.pointerType !== 'touch' && e.pointerType !== 'pen')) return; onInteractionMove(e.clientX, e.clientY); }); button.addEventListener('pointerup', (e) => { if (!e.isPrimary || (e.pointerType !== 'touch' && e.pointerType !== 'pen')) return; onInteractionEnd(e, e.pointerType); }); button.addEventListener('pointercancel', onInteractionCancel); } else { console.log('使用 Touch 事件'); button.addEventListener('touchstart', (e) => { if (e.touches.length > 1) return; // 忽略多点触控 const touch = e.touches[0]; onInteractionStart(touch.clientX, touch.clientY, 'touch'); }, { passive: true }); // passive:true 允许默认滚动行为 button.addEventListener('touchmove', (e) => { if (!isPressed || e.touches.length > 1) return; const touch = e.touches[0]; onInteractionMove(touch.clientX, touch.clientY); }, { passive: true }); // passive:true 允许默认滚动行为 button.addEventListener('touchend', (e) => { // touchend 在 e.touches 中没有信息, 使用 e.changedTouches if (e.changedTouches.length > 1) return; // 通常是单点结束 onInteractionEnd(e, 'touch'); }); // touchend 不应是 passive,因为 handleButtonClick 可能调用 preventDefault button.addEventListener('touchcancel', onInteractionCancel); } } else { // 桌面端使用鼠标事件 button.addEventListener('mouseenter', () => { button.style.transform = 'scale(1.1)'; button.style.boxShadow = '0 6px 20px rgba(0,0,0,0.4)'; }); button.addEventListener('mouseleave', () => { button.style.transform = 'scale(1)'; button.style.boxShadow = '0 4px 15px rgba(0,0,0,0.3)'; }); button.addEventListener('click', handleButtonClick); } document.body.appendChild(button); console.log('浮动按钮创建完成,当前面板状态:', isPanelVisible); return button; } // 修改创建控制面板函数的移动端样式部分 function createControlPanel() { const panel = document.createElement('div'); panel.id = 'auto-booking-panel'; // iOS设备样式优化 - 修复变换原点问题 const mobileStyles = isMobile ? ` width: calc(100vw - 30px); max-width: ${isIPad ? '500px' : '380px'}; top: ${isIPad ? '120px' : '100px'}; left: 50%; /* transform: translateX(-50%); // Initial transform will be set below */ font-size: ${isIPad ? '18px' : '16px'}; max-height: calc(100vh - 150px); -webkit-overflow-scrolling: touch; ` : ` width: 400px; top: 20px; right: 90px; max-height: 90vh; /* transform: translateX(0); // Initial transform will be set below */ `; panel.style.cssText = ` position: fixed; ${mobileStyles} background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 15px; padding: 20px; box-shadow: 0 10px 30px rgba(0,0,0,0.3); z-index: 10000; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif; color: white; border: 2px solid rgba(255,255,255,0.2); overflow-y: auto; /* transition: all 0.3s ease; // Replaced with more specific transition */ transition: opacity 0.3s ease, transform 0.3s ease; /* Specific transitions for animation */ -webkit-user-select: none; user-select: none; -webkit-tap-highlight-color: transparent; /* Initial state will be set below after appending */ `; // iOS输入框样式优化 const inputBaseStyle = ` width: 100%; padding: ${isIPad ? '14px' : (isMobile ? '12px' : '8px')}; border: none; border-radius: 6px; background: rgba(255,255,255,0.95); color: #333; font-size: ${isIPad ? '18px' : (isMobile ? '16px' : '14px')}; box-sizing: border-box; -webkit-appearance: none; appearance: none; outline: none; `; // iOS按钮样式优化 const buttonBaseStyle = ` width: 100%; padding: ${isIPad ? '18px' : (isMobile ? '15px' : '12px')}; border: none; border-radius: 8px; cursor: pointer; font-size: ${isIPad ? '20px' : (isMobile ? '18px' : '16px')}; font-weight: bold; transition: all 0.3s; text-shadow: 1px 1px 2px rgba(0,0,0,0.3); -webkit-appearance: none; appearance: none; outline: none; -webkit-tap-highlight-color: transparent; `; panel.innerHTML = ` <div style="margin-bottom: 15px; text-align: center; position: relative;"> <h3 style="margin: 0; font-size: ${isMobile ? '20px' : '18px'}; text-shadow: 2px 2px 4px rgba(0,0,0,0.5);"> 🎾 自动抢票助手 v1.1.5 </h3> <button id="close-panel" style=" position: absolute; top: -5px; right: -5px; background: rgba(255,255,255,0.2); border: none; color: white; width: ${isMobile ? '35px' : '30px'}; height: ${isMobile ? '35px' : '30px'}; border-radius: 50%; cursor: pointer; font-size: ${isMobile ? '20px' : '16px'}; display: flex; align-items: center; justify-content: center; touch-action: manipulation; " title="隐藏面板">×</button> <button id="toggle-config" style=" background: rgba(255,255,255,0.2); border: 1px solid rgba(255,255,255,0.3); color: white; padding: ${isMobile ? '8px 12px' : '5px 10px'}; border-radius: 5px; cursor: pointer; margin-top: 5px; font-size: ${isMobile ? '14px' : '12px'}; touch-action: manipulation; ">⚙️ 配置设置</button> </div> <!-- 配置区域 --> <div id="config-area" style=" background: rgba(255,255,255,0.1); padding: 15px; border-radius: 8px; margin-bottom: 15px; display: block; /* Or load from saved state */ "> <!-- 用户信息 --> <div style="margin-bottom: 12px;"> <label style="font-size: ${isMobile ? '14px' : '12px'}; display: block; margin-bottom: 3px;">👤 学号/工号:</label> <input id="user-id" type="text" value="${CONFIG.USER_INFO.YYRGH}" style="${inputBaseStyle}"> </div> <div style="margin-bottom: 12px;"> <label style="font-size: ${isMobile ? '14px' : '12px'}; display: block; margin-bottom: 3px;">📝 姓名:</label> <input id="user-name" type="text" value="${CONFIG.USER_INFO.YYRXM}" style="${inputBaseStyle}"> </div> <!-- 预约设置 --> <div style="margin-bottom: 12px;"> <label style="font-size: ${isMobile ? '14px' : '12px'}; display: block; margin-bottom: 3px;">📅 预约日期:</label> <input id="target-date" type="date" value="${CONFIG.TARGET_DATE}" style="${inputBaseStyle}"> </div> <div style="margin-bottom: 12px;"> <label style="font-size: ${isMobile ? '14px' : '12px'}; display: block; margin-bottom: 3px;">🏟️ 运动项目:</label> <select id="sport-type" style="${inputBaseStyle}"> ${Object.keys(SPORT_CODES).map(sport => `<option value="${sport}" ${sport === CONFIG.SPORT ? 'selected' : ''}>${sport}</option>` ).join('')} </select> </div> <div style="margin-bottom: 12px;"> <label style="font-size: ${isMobile ? '14px' : '12px'}; display: block; margin-bottom: 3px;">🏫 校区:</label> <select id="campus" style="${inputBaseStyle}"> ${Object.keys(CAMPUS_CODES).map(campus => `<option value="${campus}" ${campus === CONFIG.CAMPUS ? 'selected' : ''}>${campus}</option>` ).join('')} </select> </div> <!-- 羽毛球场馆选择 --> <div id="venue-selection" style="margin-bottom: 12px; display: ${CONFIG.SPORT === '羽毛球' ? 'block' : 'none'};"> <label style="font-size: ${isMobile ? '14px' : '12px'}; display: block; margin-bottom: 3px;">🏟️ 优先场馆:</label> <select id="preferred-venue" style="${inputBaseStyle}"> <option value="至畅" ${CONFIG.PREFERRED_VENUE === '至畅' ? 'selected' : ''}>🏆 至畅体育馆</option> <option value="至快" ${CONFIG.PREFERRED_VENUE === '至快' ? 'selected' : ''}>⚡ 至快体育馆</option> <option value="全部" ${CONFIG.PREFERRED_VENUE === '全部' ? 'selected' : ''}>🔄 全部场馆</option> </select> <div style="font-size: ${isMobile ? '12px' : '10px'}; color: rgba(255,255,255,0.7); margin-top: 2px;"> 💡 选择"全部"将按至畅>至快的顺序预约 </div> </div> <!-- 时间段选择 --> <div style="margin-bottom: 12px;"> <label style="font-size: ${isMobile ? '14px' : '12px'}; display: block; margin-bottom: 3px;">⏰ 优先时间段 (按优先级排序):</label> <div id="time-slots-container" style=" max-height: ${isMobile ? '120px' : '100px'}; overflow-y: auto; background: rgba(255,255,255,0.1); border-radius: 4px; padding: 5px; "> ${TIME_SLOTS.map(slot => ` <label style="display: block; font-size: ${isMobile ? '14px' : '11px'}; margin: ${isMobile ? '5px 0' : '2px 0'}; cursor: pointer;"> <input type="checkbox" value="${slot}" ${CONFIG.PREFERRED_TIMES.includes(slot) ? 'checked' : ''} style="margin-right: 5px; transform: ${isMobile ? 'scale(1.2)' : 'scale(1)'};"> ${slot} </label> `).join('')} </div> </div> <!-- 运行参数 --> <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 12px;"> <div> <label style="font-size: ${isMobile ? '14px' : '12px'}; display: block; margin-bottom: 3px;">⏱️ 查询间隔(秒):</label> <input id="retry-interval" type="number" min="1" max="60" value="${CONFIG.RETRY_INTERVAL}" style="${inputBaseStyle}"> </div> <div> <label style="font-size: ${isMobile ? '14px' : '12px'}; display: block; margin-bottom: 3px;">🔄 最大重试:</label> <input id="max-retry" type="number" min="10" max="9999" value="${CONFIG.MAX_RETRY_TIMES}" style="${inputBaseStyle}"> </div> </div> <div style="margin-bottom: 12px;"> <label style="font-size: ${isMobile ? '14px' : '12px'}; display: block; margin-bottom: 3px;">⏰ 请求超时(秒):</label> <input id="request-timeout" type="number" min="5" max="60" value="${CONFIG.REQUEST_TIMEOUT}" style="${inputBaseStyle}"> </div> <button id="save-config" style=" ${buttonBaseStyle} background: linear-gradient(45deg, #4caf50, #45a049); color: white; font-size: ${isMobile ? '16px' : '14px'}; margin-bottom: 10px; ">💾 保存配置</button> </div> <!-- 当前配置显示 --> <div style="background: rgba(255,255,255,0.1); padding: 12px; border-radius: 8px; margin-bottom: 15px;"> <div style="font-size: ${isMobile ? '15px' : '13px'}; margin-bottom: 5px;"> 👤 <span id="display-user">${CONFIG.USER_INFO.YYRXM} (${CONFIG.USER_INFO.YYRGH})</span> </div> <div style="font-size: ${isMobile ? '15px' : '13px'}; margin-bottom: 5px;"> 📅 <span id="display-date">${CONFIG.TARGET_DATE}</span> | 🏟️ <span id="display-sport">${CONFIG.SPORT}</span> | 🏫 <span id="display-campus">${CONFIG.CAMPUS}</span> </div> <div id="venue-display" style="font-size: ${isMobile ? '15px' : '13px'}; margin-bottom: 5px; display: ${CONFIG.SPORT === '羽毛球' ? 'block' : 'none'};"> 🏟️ 优先场馆: <span id="display-venue">${CONFIG.PREFERRED_VENUE || '至畅'}</span> </div> <div style="font-size: ${isMobile ? '15px' : '13px'}; margin-bottom: 5px;"> ⏰ <span id="display-times">${CONFIG.PREFERRED_TIMES.join(', ')}</span> </div> <div style="font-size: ${isMobile ? '15px' : '13px'};"> ⚙️ 间隔:<span id="display-interval">${CONFIG.RETRY_INTERVAL}</span>s | 重试:<span id="display-retry">${CONFIG.MAX_RETRY_TIMES}</span> | 超时:<span id="display-timeout">${CONFIG.REQUEST_TIMEOUT}</span>s </div> <div style="font-size: ${isMobile ? '15px' : '13px'}; margin-top: 5px;"> 🎯 进度: <span id="booking-progress">0/${getMaxBookings()} 个时段</span> </div> </div> <!-- 控制按钮 --> <div style="margin-bottom: 15px;"> <button id="start-btn" style=" ${buttonBaseStyle} background: linear-gradient(45deg, #ff6b6b, #ee5a52); color: white; "> 🚀 开始抢票 </button> </div> <!-- 状态日志 --> <div id="status-area" style=" background: rgba(0,0,0,0.2); padding: 10px; border-radius: 8px; font-size: ${isMobile ? '14px' : '12px'}; max-height: ${isMobile ? '250px' : '200px'}; overflow-y: auto; border: 1px solid rgba(255,255,255,0.1); "> <div style="color: #ffd700;">🔧 等待开始...</div> </div> <div style="margin-top: 15px; text-align: center; font-size: ${isMobile ? '13px' : '11px'}; opacity: 0.8;"> ${isMobile ? '📱 触摸优化版本' : '⚡ 快捷键: Ctrl+Shift+S 开始/停止 | Ctrl+Shift+H 显示/隐藏面板'} </div> `; document.body.appendChild(panel); // 定义 transform 值,方便复用 const transformVisibleMobile = 'translateX(-50%) translateY(0)'; const transformHiddenMobile = 'translateX(-50%) translateY(-30px)'; // 轻微向上滑出作为隐藏状态 const transformVisibleDesktop = 'translateX(0)'; const transformHiddenDesktop = 'translateX(100%)'; // 从右侧滑出作为隐藏状态 // 根据保存的状态设置面板初始可见性、透明度和位置 if (isPanelVisible) { panel.style.display = 'block'; panel.style.opacity = '1'; if (isMobile) { panel.style.transform = transformVisibleMobile; } else { panel.style.transform = transformVisibleDesktop; } } else { panel.style.display = 'none'; // 初始隐藏 panel.style.opacity = '0'; // 透明 // 设置为隐藏时的 transform,这样 togglePanel 显示时可以从此状态过渡 if (isMobile) { panel.style.transform = transformHiddenMobile; } else { panel.style.transform = transformHiddenDesktop; } } bindEventsIOS(panel); // 将 panel 作为参数传递 return panel; } // 修改切换面板函数 function togglePanel() { console.log('togglePanel 被调用,当前面板状态 (切换前):', isPanelVisible); isPanelVisible = !isPanelVisible; savePanelState(isPanelVisible); console.log('切换后面板状态:', isPanelVisible); if (controlPanel) { const transformVisibleMobile = 'translateX(-50%) translateY(0)'; const transformHiddenMobile = 'translateX(-50%) translateY(-30px)'; const transformVisibleDesktop = 'translateX(0)'; const transformHiddenDesktop = 'translateX(100%)'; // 面板从右侧滑出 // 确保 transition 属性在 controlPanel 上 (已在 createControlPanel 中设置) // controlPanel.style.transition = 'opacity 0.3s ease, transform 0.3s ease'; if (isPanelVisible) { // 如果要显示面板 console.log('准备显示面板'); controlPanel.style.display = 'block'; // 必须先 block 才能应用 transform 和 opacity // 设置动画起始状态 (面板在隐藏位置,透明) // 这确保了即使面板之前是 display:none,动画也能从正确的视觉起点开始 if (isMobile) { controlPanel.style.transform = transformHiddenMobile; } else { controlPanel.style.transform = transformHiddenDesktop; } controlPanel.style.opacity = '0'; // 使用 setTimeout 确保浏览器渲染了起始状态,然后再开始过渡 setTimeout(() => { controlPanel.style.opacity = '1'; if (isMobile) { controlPanel.style.transform = transformVisibleMobile; } else { controlPanel.style.transform = transformVisibleDesktop; } console.log('面板显示动画开始'); }, 10); // 短暂延迟,让浏览器捕获起始状态 } else { // 如果要隐藏面板 console.log('准备隐藏面板'); // 开始隐藏动画 (移动到隐藏位置,变透明) controlPanel.style.opacity = '0'; if (isMobile) { controlPanel.style.transform = transformHiddenMobile; } else { controlPanel.style.transform = transformHiddenDesktop; } console.log('面板隐藏动画开始'); // 等待过渡动画完成后再设置 display: none setTimeout(() => { if (!isPanelVisible) { // 再次检查状态,防止快速切换导致问题 controlPanel.style.display = 'none'; console.log('面板已完全隐藏 (display: none)'); } }, 300); // 300ms 对应 CSS 中的 transition-duration } } // 更新浮动按钮样式 if (floatingButton) { console.log('更新浮动按钮样式,面板可见:', isPanelVisible); if (isPanelVisible) { floatingButton.style.background = 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'; floatingButton.innerHTML = '🎾'; floatingButton.title = '隐藏抢票面板'; } else { floatingButton.style.background = 'linear-gradient(135deg, #ff6b6b 0%, #ee5a52 100%)'; floatingButton.innerHTML = '📱'; // 可以考虑用不同图标指示面板已隐藏 floatingButton.title = '显示抢票面板'; } console.log('浮动按钮样式更新完成'); } console.log('面板状态切换完成:', isPanelVisible); } // 修改 iOS 事件绑定函数 function bindEventsIOS(panelElement) { // 接受 panelElement 作为参数 // 为所有按钮添加通用的触摸处理 function addButtonTouchHandler(button, clickHandler) { if (isTouchDevice) { let touchStarted = false; let touchStartTime = 0; // 移除可能存在的旧事件监听器 button.removeEventListener('click', clickHandler); button.addEventListener('touchstart', (e) => { touchStarted = true; touchStartTime = Date.now(); button.style.opacity = '0.7'; button.style.transform = 'scale(0.95)'; e.preventDefault(); }, { passive: false }); button.addEventListener('touchend', (e) => { if (touchStarted && (Date.now() - touchStartTime) < 1000) { e.preventDefault(); e.stopPropagation(); button.style.opacity = '1'; button.style.transform = 'scale(1)'; // 延迟执行点击处理 setTimeout(() => { try { clickHandler(); } catch (error) { console.error('Button click handler error:', error); } }, 50); } touchStarted = false; }, { passive: false }); button.addEventListener('touchcancel', () => { touchStarted = false; button.style.opacity = '1'; button.style.transform = 'scale(1)'; }, { passive: true }); } else { // 桌面端直接使用点击事件 button.addEventListener('click', clickHandler); } } // 面板关闭按钮 const closeBtn = panelElement.querySelector('#close-panel'); // 使用 panelElement.querySelector if (closeBtn) { addButtonTouchHandler(closeBtn, () => { togglePanel(); }); } // 配置显示/隐藏按钮 const toggleConfigBtn = panelElement.querySelector('#toggle-config'); // 使用 panelElement.querySelector if (toggleConfigBtn) { addButtonTouchHandler(toggleConfigBtn, () => { const configArea = panelElement.querySelector('#config-area'); // 使用 panelElement.querySelector if (configArea.style.display === 'none') { configArea.style.display = 'block'; toggleConfigBtn.textContent = '⚙️ 隐藏配置'; } else { configArea.style.display = 'none'; toggleConfigBtn.textContent = '⚙️ 显示配置'; } }); } // 运动项目变化时显示/隐藏场馆选择 const sportTypeSelect = panelElement.querySelector('#sport-type'); // 使用 panelElement.querySelector if (sportTypeSelect) { // select 元素使用 change 事件 sportTypeSelect.addEventListener('change', () => { const sportType = sportTypeSelect.value; const venueSelection = panelElement.querySelector('#venue-selection'); // 使用 panelElement.querySelector const venueDisplay = panelElement.querySelector('#venue-display'); // 使用 panelElement.querySelector if (sportType === '羽毛球') { if (venueSelection) venueSelection.style.display = 'block'; if (venueDisplay) venueDisplay.style.display = 'block'; } else { if (venueSelection) venueSelection.style.display = 'none'; if (venueDisplay) venueDisplay.style.display = 'none'; } }); } // 保存配置按钮 const saveConfigBtn = panelElement.querySelector('#save-config'); // 使用 panelElement.querySelector if (saveConfigBtn) { addButtonTouchHandler(saveConfigBtn, () => { try { updateConfigFromUI(); updateDisplayConfig(); addLog('✅ 配置已保存', 'success'); // 新增:保存配置后自动隐藏配置区域 const configArea = panelElement.querySelector('#config-area'); const toggleConfigBtn = panelElement.querySelector('#toggle-config'); if (configArea && toggleConfigBtn) { configArea.style.display = 'none'; toggleConfigBtn.textContent = '⚙️ 显示配置'; addLog('📦 配置区域已自动隐藏', 'info'); } } catch (error) { addLog('❌ 保存配置失败: ' + error.message, 'error'); } }); } // 开始/停止按钮 const startBtn = panelElement.querySelector('#start-btn'); // 使用 panelElement.querySelector if (startBtn) { addButtonTouchHandler(startBtn, () => { try { if (isRunning) { stopBooking(); } else { updateConfigFromUI(); if (validateConfig()) { startBooking(); } } } catch (error) { addLog('❌ 操作失败: ' + error.message, 'error'); } }); } // 快捷键 - 只在非移动端添加 if (!isMobile) { document.addEventListener('keydown', (e) => { if (e.ctrlKey && e.shiftKey) { if (e.key === 'S') { e.preventDefault(); if (isRunning) { stopBooking(); } else { updateConfigFromUI(); if (validateConfig()) { startBooking(); } } } else if (e.key === 'H') { e.preventDefault(); togglePanel(); } else if (e.key === 'C') { e.preventDefault(); if (isPanelVisible) { const toggleBtn = panelElement.querySelector('#toggle-config'); // 使用 panelElement.querySelector if (toggleBtn) toggleBtn.click(); } } } }); } // iOS输入框优化 if (isIOS) { const inputs = panelElement.querySelectorAll('input, select'); // 使用 panelElement.querySelectorAll inputs.forEach(input => { // 防止iOS Safari缩放 input.addEventListener('focus', (e) => { // 对于iOS设备,设置字体大小防止缩放 if (input.type !== 'date' && input.type !== 'number') { e.target.style.fontSize = '16px'; } // 延迟滚动到视图中 setTimeout(() => { e.target.scrollIntoView({ behavior: 'smooth', block: 'center' }); }, 300); }); input.addEventListener('blur', (e) => { // 恢复原始字体大小 e.target.style.fontSize = ''; }); }); } // checkbox 特殊处理 const checkboxes = panelElement.querySelectorAll('input[type="checkbox"]'); // 使用 panelElement.querySelectorAll checkboxes.forEach(checkbox => { if (isTouchDevice) { // 为 checkbox 的父级 label 添加触摸处理 const label = checkbox.closest('label'); if (label) { label.style.touchAction = 'manipulation'; label.addEventListener('touchend', (e) => { // 阻止事件冒泡,让浏览器处理 checkbox 切换 e.stopPropagation(); }, { passive: true }); } } }); } // 从UI更新配置 function updateConfigFromUI() { // 获取选中的时间段 const selectedTimes = Array.from(document.querySelectorAll('#time-slots-container input[type="checkbox"]:checked')) .map(cb => cb.value); CONFIG = { USER_INFO: { YYRGH: document.getElementById('user-id').value.trim(), YYRXM: document.getElementById('user-name').value.trim() }, TARGET_DATE: document.getElementById('target-date').value, SPORT: document.getElementById('sport-type').value, CAMPUS: document.getElementById('campus').value, PREFERRED_VENUE: document.getElementById('preferred-venue')?.value || '至畅', // 新增场馆选择 PREFERRED_TIMES: selectedTimes, RETRY_INTERVAL: parseInt(document.getElementById('retry-interval').value), MAX_RETRY_TIMES: parseInt(document.getElementById('max-retry').value), REQUEST_TIMEOUT: parseInt(document.getElementById('request-timeout').value), YYLX: "1.0" }; saveConfig(CONFIG); // 更新进度显示 updateProgress(); } // 更新显示配置 function updateDisplayConfig() { document.getElementById('display-user').textContent = `${CONFIG.USER_INFO.YYRXM} (${CONFIG.USER_INFO.YYRGH})`; document.getElementById('display-date').textContent = CONFIG.TARGET_DATE; document.getElementById('display-sport').textContent = CONFIG.SPORT; document.getElementById('display-campus').textContent = CONFIG.CAMPUS; // 更新场馆显示 const venueDisplayElement = document.getElementById('display-venue'); if (venueDisplayElement) { venueDisplayElement.textContent = CONFIG.PREFERRED_VENUE || '至畅'; } document.getElementById('display-times').textContent = CONFIG.PREFERRED_TIMES.join(', '); document.getElementById('display-interval').textContent = CONFIG.RETRY_INTERVAL; document.getElementById('display-retry').textContent = CONFIG.MAX_RETRY_TIMES; document.getElementById('display-timeout').textContent = CONFIG.REQUEST_TIMEOUT; } // 验证配置 function validateConfig() { const errors = []; const warnings = []; // 用户信息验证 if (!CONFIG.USER_INFO.YYRGH || !CONFIG.USER_INFO.YYRXM) { errors.push('请填写完整的用户信息'); } // 学号格式验证(更严格) const userIdPattern = /^\d{8,12}$/; if (CONFIG.USER_INFO.YYRGH && !userIdPattern.test(CONFIG.USER_INFO.YYRGH)) { errors.push('学号格式不正确(应为8-12位数字)'); } // 学号范围验证(深圳大学学号规则) if (CONFIG.USER_INFO.YYRGH) { const userId = CONFIG.USER_INFO.YYRGH; const currentYear = new Date().getFullYear(); const yearPrefix = parseInt(userId.substring(0, 2)); // 检查年份前缀是否合理(最近20年) if (yearPrefix < (currentYear - 2020) || yearPrefix > (currentYear - 2000 + 10)) { warnings.push('学号年份可能不正确,请检查'); } } // 姓名格式验证 const namePattern = /^[\u4e00-\u9fa5]{2,10}$/; if (CONFIG.USER_INFO.YYRXM && !namePattern.test(CONFIG.USER_INFO.YYRXM)) { errors.push('姓名格式不正确(应为2-10个中文字符)'); } // 日期验证 if (!CONFIG.TARGET_DATE) { errors.push('请选择预约日期'); } else { const targetDate = new Date(CONFIG.TARGET_DATE); const today = new Date(); const maxDate = new Date(); today.setHours(0, 0, 0, 0); maxDate.setDate(today.getDate() + 7); if (isNaN(targetDate.getTime())) { errors.push('预约日期格式不正确'); } else if (targetDate < today) { errors.push('预约日期不能是过去的日期'); } else if (targetDate > maxDate) { warnings.push('预约日期超过7天,可能无法预约'); } } // 时间段验证 if (!CONFIG.PREFERRED_TIMES || CONFIG.PREFERRED_TIMES.length === 0) { errors.push('请至少选择一个时间段'); } else if (CONFIG.PREFERRED_TIMES.length > 5) { warnings.push('选择的时间段过多,建议不超过5个以提高成功率'); } // 验证时间段格式 const timePattern = /^\d{2}:\d{2}-\d{2}:\d{2}$/; const invalidTimes = CONFIG.PREFERRED_TIMES.filter(time => !timePattern.test(time)); if (invalidTimes.length > 0) { errors.push(`时间段格式不正确: ${invalidTimes.join(', ')}`); } // 运行参数验证 if (CONFIG.RETRY_INTERVAL < 1 || CONFIG.RETRY_INTERVAL > 60) { errors.push('查询间隔应在1-60秒之间'); } else if (CONFIG.RETRY_INTERVAL < 1) { warnings.push('查询间隔过短,建议设置1秒以上'); } if (CONFIG.MAX_RETRY_TIMES < 10 || CONFIG.MAX_RETRY_TIMES > 999999) { errors.push('最大重试次数应在10-999999之间'); } else if (CONFIG.MAX_RETRY_TIMES > 999999) { warnings.push('最大重试次数过高,可能影响系统性能'); } if (CONFIG.REQUEST_TIMEOUT < 5 || CONFIG.REQUEST_TIMEOUT > 60) { errors.push('请求超时应在5-60秒之间'); } // 场馆和运动项目验证 if (!SPORT_CODES[CONFIG.SPORT]) { errors.push('运动项目不支持'); } if (!CAMPUS_CODES[CONFIG.CAMPUS]) { errors.push('校区选择无效'); } // 羽毛球场馆验证 if (CONFIG.SPORT === '羽毛球' && CONFIG.PREFERRED_VENUE) { const validVenues = ['至畅', '至快', '全部']; if (!validVenues.includes(CONFIG.PREFERRED_VENUE)) { errors.push('羽毛球场馆选择无效'); } } // 配置组合合理性验证 if (CONFIG.CAMPUS === '粤海' && CONFIG.SPORT === '羽毛球' && CONFIG.PREFERRED_VENUE === '至畅') { warnings.push('粤海校区可能没有至畅体育馆,请确认场馆信息'); } // 时间合理性验证 const now = new Date(); if (CONFIG.TARGET_DATE === now.toISOString().split('T')[0]) { // 如果是今天,检查时间段是否已过 const currentHour = now.getHours(); const pastTimes = CONFIG.PREFERRED_TIMES.filter(time => { const hour = parseInt(time.split(':')[0]); return hour <= currentHour; }); if (pastTimes.length > 0) { warnings.push(`今日已过时间段: ${pastTimes.join(', ')}`); } } // 显示错误和警告 errors.forEach(error => addLog(`❌ ${error}`, 'error')); warnings.forEach(warning => addLog(`⚠️ ${warning}`, 'warning')); // 额外的提示信息 if (warnings.length > 0 && errors.length === 0) { addLog(`💡 发现 ${warnings.length} 个警告,建议检查配置`, 'warning'); } if (errors.length === 0) { addLog(`✅ 配置验证通过`, 'success'); // 显示优化建议 if (CONFIG.RETRY_INTERVAL >= 5) { addLog(`💡 当前查询间隔较长,如需更快响应可适当调低`, 'info'); } } return errors.length === 0; } // 添加状态日志 function addLog(message, type = 'info') { const statusArea = document.getElementById('status-area'); if (!statusArea) return; const colors = { info: '#e3f2fd', success: '#c8e6c9', warning: '#fff3e0', error: '#ffcdd2' }; const timestamp = new Date().toLocaleTimeString(); const logEntry = document.createElement('div'); logEntry.style.cssText = ` color: ${colors[type]}; margin-bottom: 3px; border-left: 3px solid ${colors[type]}; padding-left: 8px; `; logEntry.innerHTML = `[${timestamp}] ${message}`; statusArea.appendChild(logEntry); statusArea.scrollTop = statusArea.scrollHeight; // 保持最多50条日志 while (statusArea.children.length > 50) { statusArea.removeChild(statusArea.firstChild); } } // 更新预约进度 function updateProgress() { const currentMaxBookings = getMaxBookings(); const progressElement = document.getElementById('booking-progress'); if (progressElement) { progressElement.textContent = `${successfulBookings.length}/${currentMaxBookings} 个时段`; } } // 带超时的网络请求 async function fetchWithTimeout(url, options, timeout = CONFIG.REQUEST_TIMEOUT * 1000) { const startTime = Date.now(); let retryCount = 0; const maxRetries = 3; while (retryCount <= maxRetries) { // 等待请求槽位 await RequestThrottler.waitForSlot(); const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout); try { // 记录请求开始 RequestThrottler.onRequestStart(); const response = await fetch(url, { ...options, signal: controller.signal, credentials: 'same-origin', mode: 'cors', cache: 'no-cache' }); clearTimeout(timeoutId); const responseTime = Date.now() - startTime; // 记录请求结束 RequestThrottler.onRequestEnd(response.ok, responseTime); // 处理非OK响应 if (!response.ok) { const errorResult = await NetworkErrorHandler.handleError( new Error(`HTTP ${response.status}`), response, retryCount, '网络请求' ); if (errorResult.shouldStop) { throw new Error('请求被终止'); } if (errorResult.shouldRetry && retryCount < maxRetries) { retryCount++; addLog(`🔄 ${errorResult.retryDelay / 1000}秒后重试 (${retryCount}/${maxRetries})`, 'info'); await new Promise(resolve => setTimeout(resolve, errorResult.retryDelay)); continue; } throw new Error(`HTTP ${response.status}: ${response.statusText}`); } return response; } catch (error) { clearTimeout(timeoutId); RequestThrottler.onRequestEnd(false, Date.now() - startTime); if (retryCount >= maxRetries) { throw error; } const errorResult = await NetworkErrorHandler.handleError( error, null, retryCount, '网络请求' ); if (errorResult.shouldStop || !errorResult.shouldRetry) { throw error; } retryCount++; addLog(`🔄 ${errorResult.retryDelay / 1000}秒后重试 (${retryCount}/${maxRetries})`, 'info'); await new Promise(resolve => setTimeout(resolve, errorResult.retryDelay)); } } } // 动态获取基础 URL function getBaseUrl() { const currentUrl = window.location.href; if (currentUrl.includes('ehall-443.webvpn.szu.edu.cn')) { return 'https://ehall-443.webvpn.szu.edu.cn'; } else { return 'https://ehall.szu.edu.cn'; } } // 修改获取可用时段函数,使用动态 URL async function getAvailableSlots() { try { const allAvailable = []; const sportCode = SPORT_CODES[CONFIG.SPORT]; const campusCode = CAMPUS_CODES[CONFIG.CAMPUS]; const baseUrl = getBaseUrl(); // 动态获取基础 URL // 获取已预约成功的时间段 const bookedTimeSlots = successfulBookings.map(booking => booking.timeSlot); // 过滤掉已预约成功的时间段,只查询剩余需要预约的时间段 const remainingTimeSlots = CONFIG.PREFERRED_TIMES.filter(timeSlot => !bookedTimeSlots.includes(timeSlot) ); // 如果所有时间段都已预约,直接返回空数组 if (remainingTimeSlots.length === 0) { return []; } for (const timeSlot of remainingTimeSlots) { const [startTime, endTime] = timeSlot.split("-"); const payload = new URLSearchParams({ XMDM: sportCode, YYRQ: CONFIG.TARGET_DATE, YYLX: CONFIG.YYLX, KSSJ: startTime, JSSJ: endTime, XQDM: campusCode }); // 使用动态 URL const response = await fetchWithTimeout( `${baseUrl}/qljfwapp/sys/lwSzuCgyy/modules/sportVenue/getOpeningRoom.do`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json, text/javascript, */*; q=0.01' }, body: payload } ); if (!response.ok) { addLog(`❌ 请求失败: HTTP ${response.status}`, 'error'); continue; } const data = await response.json(); if (data.code !== "0") { addLog(`❌ 查询时段 ${timeSlot} 失败: ${data.msg || '未知错误'}`, 'error'); continue; } if (data.datas && data.datas.getOpeningRoom) { const rooms = data.datas.getOpeningRoom.rows || []; let availableCount = 0; for (const room of rooms) { if (!room.disabled && room.text === "可预约") { const venueName = room.CDMC || ''; // 根据场馆选择过滤 if (CONFIG.SPORT === "羽毛球" && CONFIG.PREFERRED_VENUE !== "全部") { if (CONFIG.PREFERRED_VENUE === "至畅" && !venueName.includes("至畅")) { continue; // 跳过非至畅场馆 } if (CONFIG.PREFERRED_VENUE === "至快" && !venueName.includes("至快")) { continue; // 跳过非至快场馆 } } let venuePriority = 2; let courtPriority = 0; // 场地优先级,数字越小优先级越高 // 场馆优先级判断 if (venueName.includes("至畅")) { venuePriority = 0; // 至畅最优先 // 丽湖校区至畅羽毛球场优先级设置 if (CONFIG.CAMPUS === "丽湖" && CONFIG.SPORT === "羽毛球") { // 匹配"5号场"或"五号场" if (venueName.includes("5号场") || venueName.includes("五号场")) { courtPriority = -2; // 5号场地最优先 } // 匹配"10号场"或"十号场" else if (venueName.includes("10号场") || venueName.includes("十号场")) { courtPriority = -1; // 10号场地次优先 } // 匹配"1号场"或"一号场" else if (venueName.match(/[^0-9]1号场|^1号场|一号场/)) { courtPriority = 2; // 1号场地最低优先级 } // 匹配"6号场"或"六号场" else if (venueName.includes("6号场") || venueName.includes("六号场")) { courtPriority = 2; // 6号场地最低优先级 } // 其他至畅场地为默认优先级 0 } } else if (venueName.includes("至快")) { venuePriority = 1; // 至快次之 } const slotInfo = { name: `${timeSlot} - ${venueName}`, wid: room.WID, timeSlot: timeSlot, startTime: startTime, endTime: endTime, venueName: venueName, venueCode: room.CGBM || '', priority: CONFIG.PREFERRED_TIMES.indexOf(timeSlot), venuePriority: venuePriority, courtPriority: courtPriority // 场地优先级 }; allAvailable.push(slotInfo); availableCount++; } } // 只在找到可预约场地时显示简化信息 if (availableCount > 0) { addLog(`✅ ${timeSlot} 找到 ${availableCount} 个可预约场地`, 'success'); } } } // 排序逻辑:优先级数字越小越优先 allAvailable.sort((a, b) => { // 首先按场地优先级排序(数字越小优先级越高) if (a.courtPriority !== b.courtPriority) { return a.courtPriority - b.courtPriority; } // 其次按场馆优先级排序 if (a.venuePriority !== b.venuePriority) { return a.venuePriority - b.venuePriority; } // 最后按时间优先级排序 return a.priority - b.priority; }); // 🔍 简化调试信息显示 if (allAvailable.length > 0) { // 只在羽毛球且有特殊优先级场地时显示详细信息 if (CONFIG.CAMPUS === "丽湖" && CONFIG.SPORT === "羽毛球") { const hasSpecialCourts = allAvailable.some(slot => slot.courtPriority === -2 || slot.courtPriority === -1 ); if (hasSpecialCourts) { const topSlot = allAvailable[0]; let priorityText = ""; if (topSlot.courtPriority === -2) { priorityText = " (🏆 5号场优先)"; } else if (topSlot.courtPriority === -1) { priorityText = " (⭐ 10号场)"; } addLog(`🎯 优选场地: ${topSlot.venueName}${priorityText}`, 'info'); } } } return allAvailable; } catch (error) { addLog(`🔥 获取时段失败: ${error.message}`, 'error'); return []; } } // 预约场地 async function bookSlot(wid, slotName) { try { const timeSlot = CONFIG.PREFERRED_TIMES.find(time => slotName.includes(time)); if (!timeSlot) { addLog(`❌ 无法从 ${slotName} 中提取时间信息`, 'error'); return false; } // 使用新的场馆代码映射 let venueCode = "104"; // 默认值 for (const [venueName, code] of Object.entries(VENUE_CODES)) { if (slotName.includes(venueName)) { venueCode = code; break; } } const [startTime, endTime] = timeSlot.split("-"); const sportCode = SPORT_CODES[CONFIG.SPORT]; const campusCode = CAMPUS_CODES[CONFIG.CAMPUS]; const baseUrl = getBaseUrl(); // 动态获取基础 URL const payload = new URLSearchParams({ DHID: "", YYRGH: CONFIG.USER_INFO.YYRGH, CYRS: "", YYRXM: CONFIG.USER_INFO.YYRXM, CGDM: venueCode, CDWID: wid, XMDM: sportCode, XQWID: campusCode, KYYSJD: timeSlot, YYRQ: CONFIG.TARGET_DATE, YYLX: CONFIG.YYLX, YYKS: `${CONFIG.TARGET_DATE} ${startTime}`, YYJS: `${CONFIG.TARGET_DATE} ${endTime}`, PC_OR_PHONE: "pc" }); // 使用动态 URL const response = await fetchWithTimeout( `${baseUrl}/qljfwapp/sys/lwSzuCgyy/sportVenue/insertVenueBookingInfo.do`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json, text/javascript, */*; q=0.01' }, body: payload } ); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const result = await response.json(); if (result.code === "0" && result.msg === "成功") { const dhid = result.data?.DHID || "Unknown"; addLog(`🎉 预约成功!场地:${slotName}`, 'success'); addLog(`📋 预约单号:${dhid}`, 'success'); successfulBookings.push({ timeSlot: timeSlot, venueName: slotName, dhid: dhid, slotName: slotName }); updateProgress(); return true; } else { const errorMsg = result.msg || "未知错误"; addLog(`❌ 预约失败:${errorMsg}`, 'error'); if (errorMsg.includes("只能预订2次") || errorMsg.includes("超过限制")) { addLog(`🎊 已达到预约上限!`, 'success'); return 'limit_reached'; } return false; } } catch (error) { addLog(`💥 预约异常: ${error.message}`, 'error'); return false; } } // 添加时间检查功能 function checkBookingTime() { const now = new Date(); const hours = now.getHours(); const minutes = now.getMinutes(); const seconds = now.getSeconds(); // 检查是否在12:25-12:30之间 if (hours === 12 && minutes >= 25 && minutes < 30) { const targetTime = new Date(); targetTime.setHours(12, 29, 55, 0); // 设置为12:29:55 const currentTime = now.getTime(); const targetTimeMs = targetTime.getTime(); if (currentTime < targetTimeMs) { const waitTime = targetTimeMs - currentTime; const waitMinutes = Math.floor(waitTime / 60000); const waitSeconds = Math.floor((waitTime % 60000) / 1000); return { shouldWait: true, waitTime: waitTime, waitText: `${waitMinutes}分${waitSeconds}秒` }; } } return { shouldWait: false }; } // 等待到指定时间的函数 async function waitForBookingTime() { const timeCheck = checkBookingTime(); if (timeCheck.shouldWait) { addLog(`⏰ 检测到当前时间在12:25-12:30之间`, 'info'); addLog(`🕐 将等待到12:29:55开始抢票 (还需等待${timeCheck.waitText})`, 'warning'); // 创建倒计时显示 const countdownInterval = setInterval(() => { const currentCheck = checkBookingTime(); if (currentCheck.shouldWait) { const waitMinutes = Math.floor(currentCheck.waitTime / 60000); const waitSeconds = Math.floor((currentCheck.waitTime % 60000) / 1000); // 更新按钮显示倒计时 const startBtn = document.getElementById('start-btn'); if (startBtn && isRunning) { startBtn.textContent = `⏰ 等待开始 ${waitMinutes}:${waitSeconds.toString().padStart(2, '0')}`; } // 每30秒显示一次等待提示 if (waitSeconds % 30 === 0) { addLog(`⏳ 继续等待... 还有${waitMinutes}分${waitSeconds}秒`, 'info'); } } else { // 时间到了,清除倒计时 clearInterval(countdownInterval); addLog(`🚀 等待结束,开始抢票!`, 'success'); // 更新按钮显示 const startBtn = document.getElementById('start-btn'); if (startBtn && isRunning) { startBtn.textContent = '⏹️ 停止抢票'; } } }, 1000); // 每秒更新一次 // 等待到指定时间 await new Promise(resolve => { const checkTime = () => { const currentCheck = checkBookingTime(); if (!currentCheck.shouldWait) { clearInterval(countdownInterval); resolve(); } else { setTimeout(checkTime, 100); // 每100ms检查一次,确保精确 } }; checkTime(); }); } } // 更新 startBooking 函数,移除退出机制 async function startBooking() { if (isRunning) return; isRunning = true; retryCount = 0; startTime = new Date(); const currentMaxBookings = getMaxBookings(); // 重置重试机制 SmartRetry.reset(); SmartRetry.updateInterval(); const startBtn = document.getElementById('start-btn'); if (startBtn) { startBtn.textContent = '⏹️ 停止抢票'; startBtn.style.background = 'linear-gradient(45deg, #f44336, #d32f2f)'; } addLog(`🚀 开始自动抢票!`, 'success'); addLog(`📊 ${CONFIG.SPORT} | ${CONFIG.CAMPUS} | ${CONFIG.TARGET_DATE} | 目标: ${currentMaxBookings} 个时段`, 'info'); // 添加场馆选择提示 if (CONFIG.SPORT === "羽毛球") { if (CONFIG.PREFERRED_VENUE === "全部") { addLog(`🏟️ 场馆策略: 全部场馆 (至畅 > 至快)`, 'info'); } else { addLog(`🏟️ 场馆策略: 仅${CONFIG.PREFERRED_VENUE}体育馆`, 'info'); } if (CONFIG.CAMPUS === "丽湖" && (CONFIG.PREFERRED_VENUE === "至畅" || CONFIG.PREFERRED_VENUE === "全部")) { addLog(`🎾 至畅场地优先级: 5号 > 10号 > 其他 > 1号/6号`, 'info'); } } try { // 检查是否需要等待到特定时间 await waitForBookingTime(); if (!isRunning) return; // 重新设置开始时间(排除等待时间) startTime = new Date(); addLog(`⚡ 正式开始抢票循环!`, 'success'); while (isRunning && retryCount < CONFIG.MAX_RETRY_TIMES) { if (successfulBookings.length >= currentMaxBookings) { addLog(`🎊 恭喜!已成功预约 ${currentMaxBookings} 个时间段!`, 'success'); break; } // 移除 shouldContinue 检查,让程序按用户设置运行 retryCount++; // 获取重试建议 const advice = SmartRetry.getRetryAdvice(); if (advice.shouldPause && retryCount > 1) { addLog(`⏸️ ${advice.message}`, 'warning'); await new Promise(resolve => setTimeout(resolve, advice.pauseDuration)); if (!isRunning) break; } // 简化查询进度显示 if (retryCount === 1 || retryCount % 10 === 0 || retryCount <= 5) { addLog(`🔍 第 ${retryCount} 次查询 (${successfulBookings.length}/${currentMaxBookings})`); } try { const availableSlots = await getAvailableSlots(); if (availableSlots.length > 0) { SmartRetry.onSuccess(); // 记录成功 // 简化找到场地的提示 if (availableSlots.length <= 5) { addLog(`🎉 找到 ${availableSlots.length} 个可预约时段`, 'success'); } else { addLog(`🎉 找到 ${availableSlots.length} 个可预约时段 (显示前5个)`, 'success'); } // 预约逻辑保持不变... const timeSlotGroups = {}; availableSlots.forEach(slot => { if (!timeSlotGroups[slot.timeSlot]) { timeSlotGroups[slot.timeSlot] = []; } timeSlotGroups[slot.timeSlot].push(slot); }); for (const timeSlot of CONFIG.PREFERRED_TIMES) { if (successfulBookings.length >= currentMaxBookings) break; if (successfulBookings.some(booking => booking.timeSlot === timeSlot)) { continue; } if (timeSlotGroups[timeSlot]) { const slotsInTime = timeSlotGroups[timeSlot]; slotsInTime.sort((a, b) => { if (a.courtPriority !== b.courtPriority) { return a.courtPriority - b.courtPriority; } return a.venuePriority - b.venuePriority; }); const firstSlot = slotsInTime[0]; let priorityText = ""; if (CONFIG.CAMPUS === "丽湖" && CONFIG.SPORT === "羽毛球" && firstSlot.venueName.includes("至畅")) { if (firstSlot.courtPriority === -2) { priorityText = " 🏆"; } else if (firstSlot.courtPriority === -1) { priorityText = " ⭐"; } else if (firstSlot.courtPriority === 2) { priorityText = " 🔻"; } } addLog(`🎯 预约: ${firstSlot.venueName}${priorityText}`, 'info'); const result = await bookSlot(firstSlot.wid, firstSlot.name); if (result === true) { addLog(`✨ ${timeSlot} 预约成功!`, 'success'); if (successfulBookings.length < currentMaxBookings) { await new Promise(resolve => setTimeout(resolve, 1000)); } } else if (result === 'limit_reached') { break; } else { await new Promise(resolve => setTimeout(resolve, 500)); } } } } else { SmartRetry.onFailure('no_slots'); // 记录无可用时段 if (retryCount <= 3 || retryCount % 20 === 0) { addLog(`🔍 暂无可预约场地`, 'warning'); } } } catch (error) { const errorType = NetworkErrorHandler.categorizeError(error); SmartRetry.onFailure(errorType); // 尝试错误恢复,但不因为恢复失败而退出 try { await ErrorRecovery.attemptRecovery(errorType, error, { operation: 'getAvailableSlots', retryCount: retryCount }); } catch (recoveryError) { // 恢复失败也继续运行 addLog(`🔧 错误恢复失败,继续尝试`, 'warning'); } // 只有认证错误才退出,其他错误都继续 if (errorType === 'auth_error') { addLog(`🔐 认证错误,需要重新登录`, 'error'); break; } } if (successfulBookings.length < currentMaxBookings && isRunning && retryCount < CONFIG.MAX_RETRY_TIMES) { // 严格按照用户设置的查询间隔,添加小的随机抖动 const baseInterval = CONFIG.RETRY_INTERVAL * 1000; // 转换为毫秒 const jitter = Math.random() * 200 - 100; // ±100ms的随机抖动 const actualInterval = Math.max(100, baseInterval + jitter); // 确保最小间隔100ms await new Promise(resolve => setTimeout(resolve, actualInterval)); } } } catch (error) { addLog(`💥 程序异常: ${error.message}`, 'error'); ErrorRecovery.recordError(error, { operation: 'startBooking' }); } finally { stopBooking(); } } // 更新 stopBooking 函数 function stopBooking() { if (!isRunning) return; isRunning = false; const currentMaxBookings = getMaxBookings(); // 清理移动端优化资源 if (isMobile) { MobileOptimization.cleanup(); } const startBtn = document.getElementById('start-btn'); if (startBtn) { startBtn.textContent = '🚀 开始抢票'; startBtn.style.background = 'linear-gradient(45deg, #ff6b6b, #ee5a52)'; } if (successfulBookings.length > 0) { addLog(`🎉 抢票结束!成功预约 ${successfulBookings.length}/${currentMaxBookings} 个时段`, 'success'); successfulBookings.forEach((booking, index) => { addLog(`${index + 1}. ${booking.slotName} (${booking.dhid})`, 'success'); }); } else { addLog(`😢 很遗憾,没有成功预约到任何时段`, 'warning'); } const elapsed = startTime ? Math.round((new Date() - startTime) / 1000) : 0; addLog(`📊 运行时间: ${elapsed}秒, 查询次数: ${retryCount}`, 'info'); // 显示错误统计 const errorStats = ErrorRecovery.getErrorStats(); if (errorStats.total > 0) { addLog(`🛡️ 错误统计: 总计${errorStats.total}个, 最近1小时${errorStats.lastHour}个`, 'info'); } } // iOS兼容的初始化检查 function checkIOSCompatibility() { const issues = []; // 检查存储可用性 if (!Storage.set('test', 'test') || Storage.get('test') !== 'test') { issues.push('存储功能受限'); } // 检查 fetch 支持 if (typeof fetch === 'undefined') { issues.push('网络请求不支持'); } // 检查触摸支持 if (isIOS && !isTouchDevice) { issues.push('触摸事件检测异常'); } if (issues.length > 0) { addLog(`⚠️ iOS兼容性问题: ${issues.join(', ')}`, 'warning'); addLog(`💡 建议刷新页面或重启Safari`, 'info'); } else { addLog(`✅ iOS兼容性检查通过`, 'success'); } return issues.length === 0; } // 初始化函数 function init() { // 添加系统健康检查 const systemHealth = checkSystemHealth(); if (!systemHealth.healthy) { addLog(`⚠️ 系统检查发现问题: ${systemHealth.issues.join(', ')}`, 'warning'); } // 初始化错误恢复机制 ErrorRecovery.init(); // 初始化移动端优化 if (isMobile) { MobileOptimization.init(); MobileOptimization.preventPageFreeze(); MobileOptimization.optimizeMemory(); } // 初始化智能重试机制 SmartRetry.reset(); // 清理存储 const cleanedCount = Storage.cleanup(); if (cleanedCount > 0) { addLog(`🧹 清理了 ${cleanedCount} 个过期配置项`, 'info'); } // 显示存储状态 const storageInfo = Storage.getStorageInfo(); let storageStatus = '💾 存储状态: '; if (storageInfo.localStorage.available) { storageStatus += `localStorage(${Math.round(storageInfo.localStorage.used/1024)}KB) `; } if (storageInfo.sessionStorage.available) { storageStatus += `sessionStorage(${Math.round(storageInfo.sessionStorage.used/1024)}KB) `; } if (storageInfo.memoryStorage.available) { storageStatus += `memory(${storageInfo.memoryStorage.used}项)`; } addLog(storageStatus, 'info'); // 更新 URL 检查逻辑,支持 WebVPN const currentUrl = window.location.href; const isValidUrl = currentUrl.includes('ehall.szu.edu.cn/qljfwapp/sys/lwSzuCgyy') || currentUrl.includes('ehall-443.webvpn.szu.edu.cn/qljfwapp/sys/lwSzuCgyy'); if (!isValidUrl) { console.log('URL 不匹配,退出初始化。当前URL:', currentUrl); return; } console.log('开始初始化...', { isMobile, isIOS, isIPad, isTouchDevice, userAgent: navigator.userAgent, platform: navigator.platform, maxTouchPoints: navigator.maxTouchPoints, hasPointerEvent: !!window.PointerEvent, currentUrl: currentUrl }); // 检查 PointerEvent 支持 if (window.PointerEvent) { console.log('✅ 支持 PointerEvent API'); } else { console.log('❌ 不支持 PointerEvent API,使用 TouchEvent'); } // 确保配置中的日期为明天 CONFIG.TARGET_DATE = getTomorrowDate(); // iOS兼容性检查 const isCompatible = checkIOSCompatibility(); // 创建浮动按钮 floatingButton = createFloatingButton(); console.log('浮动按钮创建完成', floatingButton); // 创建控制面板 controlPanel = createControlPanel(); console.log('控制面板创建完成', controlPanel); // 更新界面显示 updateDisplayConfig(); // 同时更新输入框的值 const targetDateInput = document.getElementById('target-date'); if (targetDateInput) { targetDateInput.value = getTomorrowDate(); } const deviceInfo = isIPad ? 'iPad' : (isMobile ? '移动端' : '桌面端'); addLog(`🎮 自动抢票助手已就绪!(${deviceInfo})`, 'success'); if (isIOS) { addLog(`🍎 iOS优化版本,触摸操作已优化`, 'info'); if (window.PointerEvent) { addLog(`🎯 使用 PointerEvent API`, 'info'); } else { addLog(`📱 使用 TouchEvent API`, 'info'); } if (!isCompatible) { addLog(`⚠️ 发现兼容性问题,建议检查Safari设置`, 'warning'); } } addLog(`📝 已加载配置,可随时修改`, 'info'); console.log('初始化完成'); // 测试面板状态 console.log('初始面板状态:', isPanelVisible); } // 新增:页面可见性变化时也更新日期 document.addEventListener('visibilitychange', () => { if (!document.hidden) { // 页面重新可见时,检查并更新日期 const newTomorrowDate = getTomorrowDate(); if (CONFIG.TARGET_DATE !== newTomorrowDate) { CONFIG.TARGET_DATE = newTomorrowDate; // 更新输入框 const targetDateInput = document.getElementById('target-date'); if (targetDateInput) { targetDateInput.value = newTomorrowDate; } // 更新显示 updateDisplayConfig(); // 保存更新后的配置 saveConfig(CONFIG); addLog(`📅 日期已自动更新为明天: ${newTomorrowDate}`, 'info'); } } }); // 添加系统健康检查函数 function checkSystemHealth() { const issues = []; // 检查网络连接 if (!navigator.onLine) { issues.push('网络连接断开'); } // 检查存储空间 try { const testKey = 'szu_sports_health_check'; const testData = 'x'.repeat(1024); // 1KB test data localStorage.setItem(testKey, testData); localStorage.removeItem(testKey); } catch (e) { issues.push('存储空间不足'); } // 检查时间同步(简单检查) const serverTime = new Date().getTime(); const clientTime = Date.now(); if (Math.abs(serverTime - clientTime) > 60000) { // 1分钟差异 issues.push('系统时间可能不准确'); } // 检查浏览器兼容性 if (!window.fetch) issues.push('浏览器不支持fetch API'); if (!window.Promise) issues.push('浏览器不支持Promise'); if (!window.AbortController) issues.push('浏览器不支持AbortController'); return { healthy: issues.length === 0, issues: issues }; } // 确保页面加载完成后初始化 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { // DOM 已经加载完成 setTimeout(init, 100); // 稍作延迟以确保页面元素完全就绪 } })();