// ==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); // 稍作延迟以确保页面元素完全就绪
}
})();