Greasy Fork

Greasy Fork is available in English.

深圳大学体育场馆自动抢票

深圳大学体育场馆自动预约脚本 - iOS、安卓、移动端、桌面端完全兼容

当前为 2025-11-13 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         深圳大学体育场馆自动抢票
// @namespace    http://tampermonkey.net/
// @version      1.1.8
// @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        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @connect      qyapi.weixin.qq.com
// @run-at       document-end
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    // ==================== 设备检测模块 ====================
    const Device = (() => {
        const ua = navigator.userAgent;
        const platform = navigator.platform;
        const maxTouch = navigator.maxTouchPoints;

        return {
            isMobile: /iPhone|iPad|iPod|Android|Mobile/i.test(ua),
            isIOS: /iPhone|iPad|iPod/i.test(ua),
            isIPad: /iPad/i.test(ua) || (platform === 'MacIntel' && maxTouch > 1),
            hasPointer: !!window.PointerEvent,
            get isTouch() {
                return this.isMobile || this.isIPad || (maxTouch > 0 && /Android|Mobile/i.test(ua));
            }
        };
    })();

    // ==================== 样式管理器 ====================
    const Styles = {
        getSize: (desktop, mobile, iPad) => Device.isIPad ? iPad : (Device.isMobile ? mobile : desktop),

        get input() {
            const padding = this.getSize('8px', '12px', '14px');
            const fontSize = this.getSize('14px', '16px', '18px');
            return `width:100%;padding:${padding};border:none;border-radius:6px;background:rgba(255,255,255,0.95);color:#333;font-size:${fontSize};box-sizing:border-box;-webkit-appearance:none;appearance:none;outline:none;`;
        },

        get button() {
            const padding = this.getSize('12px', '15px', '18px');
            const fontSize = this.getSize('16px', '18px', '20px');
            return `width:100%;padding:${padding};border:none;border-radius:8px;cursor:pointer;font-size:${fontSize};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;`;
        }
    };

    // ==================== 存储管理器 ====================
    const Storage = {
        prefix: 'szu_sports_',
        maxAge: 7 * 24 * 60 * 60 * 1000,
        version: '1.1.8',

        set(key, value) {
            const data = { value, timestamp: Date.now(), version: this.version };
            const fullKey = this.prefix + key;

            try {
                localStorage.setItem(fullKey, JSON.stringify(data));
                return true;
            } catch {
                try {
                    sessionStorage.setItem(fullKey, JSON.stringify(data));
                    return true;
                } catch {
                    if (!window.memoryStorage) window.memoryStorage = new Map();
                    window.memoryStorage.set(fullKey, data);
                    return true;
                }
            }
        },

        get(key, defaultValue = null) {
            const fullKey = this.prefix + key;
            const now = Date.now();

            const tryParse = (item) => {
                if (!item) return null;
                try {
                    const data = JSON.parse(item);
                    if (data.version !== this.version || (data.timestamp && now - data.timestamp > this.maxAge)) {
                        this.remove(key);
                        return null;
                    }
                    return data.value !== undefined ? data.value : data;
                } catch {
                    this.remove(key);
                    return null;
                }
            };

            // 尝试 localStorage
            const localItem = tryParse(localStorage.getItem(fullKey));
            if (localItem !== null) return localItem;

            // 尝试 sessionStorage
            const sessionItem = tryParse(sessionStorage.getItem(fullKey));
            if (sessionItem !== null) return sessionItem;

            // 尝试内存存储
            if (window.memoryStorage?.has(fullKey)) {
                const data = window.memoryStorage.get(fullKey);
                return data.value !== undefined ? data.value : data;
            }

            return defaultValue;
        },

        remove(key) {
            const fullKey = this.prefix + key;
            try { localStorage.removeItem(fullKey); } catch { }
            try { sessionStorage.removeItem(fullKey); } catch { }
            window.memoryStorage?.delete(fullKey);
        },

        cleanup() {
            const now = Date.now();
            let count = 0;

            [localStorage, sessionStorage].forEach(storage => {
                try {
                    for (let i = storage.length - 1; i >= 0; i--) {
                        const key = storage.key(i);
                        if (key?.startsWith(this.prefix)) {
                            try {
                                const data = JSON.parse(storage.getItem(key));
                                if (data.timestamp && now - data.timestamp > this.maxAge) {
                                    storage.removeItem(key);
                                    count++;
                                }
                            } catch {
                                storage.removeItem(key);
                                count++;
                            }
                        }
                    }
                } catch { }
            });

            return count;
        }
    };

    // ==================== 网络错误处理器 ====================
    const NetworkErrorHandler = {
        categorize(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 >= 400) return 'client_error';
            }
            if (error.name === 'AbortError' || error.message.includes('超时')) return 'timeout';
            if (error.message.includes('网络')) return 'network_error';
            return 'unknown_error';
        },

        shouldRetry(errorType, retryCount = 0) {
            const maxRetries = { rate_limit: 3, server_error: 5, network_error: 3, timeout: 3, unknown_error: 2 };
            const noRetry = ['auth_error', 'client_error'];
            return !noRetry.includes(errorType) && retryCount < (maxRetries[errorType] || 1);
        },

        getRetryDelay(errorType, retryCount = 0) {
            const baseDelays = { rate_limit: 5000, server_error: 3000, network_error: 2000, timeout: 1000, unknown_error: 2000 };
            return Math.min((baseDelays[errorType] || 2000) * Math.pow(1.5, retryCount), 30000);
        },

        async handle(error, response = null, retryCount = 0) {
            const errorType = this.categorize(error, response);
            const errorMsg = response ? `HTTP ${response.status}` : error.message;

            addLog(`❌ 请求失败: ${errorMsg}`, 'error');

            if (errorType === 'auth_error') {
                addLog(`🔐 认证失败,请检查登录状态`, 'error');
                if (isRunning) stopBooking();
                return { shouldStop: true, shouldRetry: false };
            }

            return {
                shouldStop: false,
                shouldRetry: this.shouldRetry(errorType, retryCount),
                retryDelay: this.shouldRetry(errorType, retryCount) ? this.getRetryDelay(errorType, retryCount) : 0,
                errorType
            };
        }
    };

    // ==================== 请求频率控制器 ====================
    const RequestThrottler = {
        requests: [],
        maxPerSecond: 2,
        maxConcurrent: 3,
        current: 0,

        cleanup() {
            const now = Date.now();
            this.requests = this.requests.filter(time => now - time < 1000);
        },

        canRequest() {
            this.cleanup();
            return this.requests.length < this.maxPerSecond && this.current < this.maxConcurrent;
        },

        async wait() {
            while (!this.canRequest()) {
                const waitTime = this.current >= this.maxConcurrent ? 1000 :
                    (this.requests.length >= this.maxPerSecond ? Math.max(0, 1000 - (Date.now() - Math.min(...this.requests))) : 0);
                if (waitTime > 0) await new Promise(resolve => setTimeout(resolve, waitTime));
            }
        },

        onStart() {
            this.requests.push(Date.now());
            this.current++;
        },

        onEnd() {
            this.current = Math.max(0, this.current - 1);
        },

        reset() {
            this.requests = [];
            this.current = 0;
            addLog(`🔄 请求频率已重置`, 'info');
        }
    };

    // ==================== 智能重试机制 ====================
    const SmartRetry = {
        failures: 0,
        lastSuccess: Date.now(),

        reset() {
            this.failures = 0;
            this.lastSuccess = Date.now();
        },

        onSuccess() {
            if (this.failures > 0) addLog(`✅ 恢复正常`, 'success');
            this.reset();
        },

        onFailure() {
            this.failures++;
            if (this.failures >= 15) addLog(`⚠️ 连续失败${this.failures}次`, 'warning');
        }
    };

    // ==================== 移动端优化 ====================
    const MobileOptimization = {
        wakeLock: null,

        async init() {
            if (!Device.isMobile) return;
            addLog(`📱 启用移动端优化`, 'info');

            await this.requestWakeLock();
            this.setupVisibility();
            this.optimizeScrolling();
        },

        async requestWakeLock() {
            if ('wakeLock' in navigator) {
                try {
                    this.wakeLock = await navigator.wakeLock.request('screen');
                    addLog(`🔆 屏幕保持唤醒`, 'success');
                } catch { }
            }
        },

        setupVisibility() {
            document.addEventListener('visibilitychange', () => {
                if (!document.hidden && isRunning) this.requestWakeLock();
            });
        },

        optimizeScrolling() {
            const style = document.createElement('style');
            style.textContent = `
                #status-area { -webkit-overflow-scrolling: touch; overscroll-behavior: contain; }
                * { touch-action: manipulation; }
                #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);
        },

        cleanup() {
            this.wakeLock?.release();
            this.wakeLock = null;
        }
    };

    // ==================== 企业微信推送 ====================
    const WeChatNotifier = {
        url: 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=4a1965fb-7559-4229-95ab-cc5a34066b6b',
        enabled: true,

        async sendSuccess(info) {
            if (!this.enabled || typeof GM_xmlhttpRequest === 'undefined') return false;

            const message = `🎉 深大体育场馆预约成功!

👤 ${info.userName} (${info.userId})
📅 ${info.date} | 🏟️ ${info.sport} | 🏫 ${info.campus}
📍 ${info.venueName} | ⏰ ${info.timeSlot}
📋 ${info.dhid}`;

            return new Promise(resolve => {
                GM_xmlhttpRequest({
                    method: 'POST',
                    url: this.url,
                    headers: { 'Content-Type': 'application/json' },
                    data: JSON.stringify({ msgtype: 'text', text: { content: message } }),
                    timeout: 10000,
                    onload: (res) => resolve(res.status === 200),
                    onerror: () => resolve(false),
                    ontimeout: () => resolve(false)
                });
            });
        }
    };

    // ==================== 常量定义 ====================
    const SPORT_CODES = {
        "羽毛球": "001",
        "排球": "003",
        "网球": "004",
        "篮球": "005",
        "乒乓球": "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_CONFIG = {
        "羽毛球": {
            "丽湖": {
                options: [
                    { value: '至畅', label: '🏆 至畅体育馆' },
                    { value: '至快', label: '⚡ 至快体育馆' },
                ],
                filter: (fullName, preferredVenue) => {
                    if (preferredVenue === "至畅") return fullName.includes("至畅");
                    if (preferredVenue === "至快") return fullName.includes("至快");
                    return false;
                }
            },
            "粤海": {
                options: [
                    { value: '运动广场东馆羽毛球场', label: '🏸 运动广场东馆羽毛球场' },
                ],
                filter: (fullName, preferredVenue) => {
                    if (preferredVenue === "运动广场东馆羽毛球场") return fullName.includes("东馆") || fullName.includes("运动广场");
                    return false;
                }
            }
        },
        "网球": {
            "粤海": {
                options: [
                    { value: '运动广场海边网球场', label: '🌊 运动广场海边网球场' },
                    { value: '北区网球场', label: '🎾 北区网球场' },
                ],
                filter: (fullName, preferredVenue) => {
                    if (preferredVenue === "运动广场海边网球场") return fullName.includes("海边") || fullName.includes("运动广场");
                    if (preferredVenue === "北区网球场") return fullName.includes("北区");
                    return false;
                }
            },
            "丽湖": {
                options: [
                    { value: '北区体育场网球场', label: '🏟️ 北区体育场网球场' },
                    { value: '南区室外网球场', label: '🌳 南区室外网球场' },
                ],
                filter: (fullName, preferredVenue) => {
                    if (preferredVenue === "北区体育场网球场") return fullName.includes("北区体育场");
                    if (preferredVenue === "南区室外网球场") return fullName.includes("南区室外");
                    return false;
                }
            }
        },
        "排球": {
            "粤海": {
                options: [
                    { value: '西馆排球场(包场)', label: '🏐 西馆排球场(包场)' },
                ],
                filter: (fullName, preferredVenue) => {
                    if (preferredVenue === "西馆排球场(包场)") return fullName.includes("西馆") || fullName.includes("排球场");
                    return false;
                }
            },
            "丽湖": {
                options: [
                    { value: '风雨操场排球场', label: '☔ 风雨操场排球场' },
                ],
                filter: (fullName, preferredVenue) => {
                    if (preferredVenue === "风雨操场排球场") return fullName.includes("风雨操场");
                    return false;
                }
            }
        },
        "篮球": {
            "粤海": {
                options: [
                    { value: '运动广场天台篮球场', label: '🏙️ 运动广场天台篮球场' },
                    { value: '运动广场东馆室内篮球场', label: '🏀 运动广场东馆室内篮球场' },
                ],
                filter: (fullName, preferredVenue) => {
                    if (preferredVenue === "运动广场天台篮球场") return fullName.includes("天台") || fullName.includes("运动广场");
                    if (preferredVenue === "运动广场东馆室内篮球场") return fullName.includes("东馆") || fullName.includes("室内篮球场");
                    return false;
                }
            },
            "丽湖": {
                options: [
                    { value: '风雨操场篮球场', label: '🏀 风雨操场篮球场' },
                ],
                filter: (fullName, preferredVenue) => {
                    if (preferredVenue === "风雨操场篮球场") return fullName.includes("风雨操场");
                    return false;
                }
            }
        },
        "乒乓球": {
            "粤海": {
                options: [
                    { value: '北区乒乓球馆', label: '🏓 北区乒乓球馆' },
                ],
                filter: (fullName, preferredVenue) => {
                    if (preferredVenue === "北区乒乓球馆") return fullName.includes("北区");
                    return false;
                }
            },
            "丽湖": {
                options: [
                    { value: '体育馆乒乓球室', label: '🏓 体育馆乒乓球室' },
                ],
                filter: (fullName, preferredVenue) => {
                    if (preferredVenue === "体育馆乒乓球室") return fullName.includes("体育馆");
                    return false;
                }
            }
        },
    };

    function getTomorrowDate() {
        const d = new Date();
        d.setDate(d.getDate() + 1);
        return d.toISOString().split('T')[0];
    }

    // 根据运动项目和校区获取正确的YYLX
    function getYYLX(sport, campus) {
        // 粤海篮球需要使用团体预约模式
        if (sport === "篮球" && campus === "粤海") {
            return "2.0";
        }
        // 其他情况使用单人散场模式
        return "1.0";
    }

    // 判断是否显示场馆选择
    function shouldShowVenueSelection(sport, campus) {
        return !!(VENUE_CONFIG[sport] && VENUE_CONFIG[sport][campus]);
    }

    // 获取场馆选项
    function getVenueOptions(sport, campus) {
        const baseOptions = VENUE_CONFIG[sport]?.[campus]?.options || [];
        return [
            ...baseOptions,
            { value: '全部', label: '🔄 全部场馆' }
        ];
    }

    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 loadConfig() {
        const saved = Storage.get('bookingConfig', null);
        const config = saved ? { ...DEFAULT_CONFIG, ...saved } : DEFAULT_CONFIG;
        config.TARGET_DATE = getTomorrowDate();
        // 根据当前配置更新YYLX
        config.YYLX = getYYLX(config.SPORT, config.CAMPUS);
        // 确保场馆配置有效
        if (!shouldShowVenueSelection(config.SPORT, config.CAMPUS)) {
            config.PREFERRED_VENUE = '全部';
        }
        return config;
    }

    // ==================== 定时任务管理器 ====================
    const ScheduledTask = {
        timerId: null,
        targetTime: null,

        set(targetTime) {
            this.clear();

            const now = Date.now();
            const delay = targetTime - now;

            if (delay <= 0) {
                addLog(`❌ 定时时间必须晚于当前时间`, 'error');
                return false;
            }

            this.targetTime = targetTime;
            Storage.set('scheduledTime', targetTime);

            this.timerId = setTimeout(() => {
                addLog(`⏰ 定时任务触发,开始抢票!`, 'success');
                if (!isRunning) {
                    updateConfigFromUI();
                    if (validateConfig()) startBooking();
                }
                this.clear();
            }, delay);

            const targetDate = new Date(targetTime);
            addLog(`⏰ 已设置定时任务: ${targetDate.toLocaleString('zh-CN')}`, 'success');
            addLog(`⏱️ 距离开始还有: ${this.formatRemaining()}`, 'info');
            return true;
        },

        clear() {
            if (this.timerId) {
                clearTimeout(this.timerId);
                this.timerId = null;
            }
            this.targetTime = null;
            Storage.remove('scheduledTime');
        },

        getRemaining() {
            if (!this.targetTime) return null;
            const remaining = Math.max(0, this.targetTime - Date.now());
            return remaining > 0 ? remaining : null;
        },

        formatRemaining() {
            const remaining = this.getRemaining();
            if (!remaining) return '未设置';

            const hours = Math.floor(remaining / 3600000);
            const minutes = Math.floor((remaining % 3600000) / 60000);
            const seconds = Math.floor((remaining % 60000) / 1000);

            if (hours > 0) return `${hours}时${minutes}分${seconds}秒`;
            if (minutes > 0) return `${minutes}分${seconds}秒`;
            return `${seconds}秒`;
        },

        restore() {
            const savedTime = Storage.get('scheduledTime');
            if (savedTime && savedTime > Date.now()) {
                return this.set(savedTime);
            } else if (savedTime) {
                // 清理过期的定时任务
                Storage.remove('scheduledTime');
            }
            return false;
        }
    };

    // ==================== 全局变量 ====================
    let CONFIG = loadConfig();
    let isRunning = false;
    let retryCount = 0;
    let startTime = null;
    let successfulBookings = [];
    let controlPanel = null;
    let floatingButton = null;
    let isPanelVisible = Storage.get('panelVisible', true);
    let countdownInterval = null; // 倒计时更新定时器

    function getMaxBookings() {
        return Math.min(CONFIG.PREFERRED_TIMES.length, 2);
    }

    // ==================== 交互处理器 ====================
    const Interaction = {
        bind(el, handler) {
            if (!Device.isTouch) {
                el.addEventListener('click', handler);
                return;
            }

            let pressed = false, startTime = 0;

            if (Device.hasPointer) {
                el.addEventListener('pointerdown', (e) => {
                    if (!e.isPrimary) return;
                    pressed = true;
                    startTime = Date.now();
                });
                el.addEventListener('pointerup', (e) => {
                    if (!pressed || !e.isPrimary) return;
                    if (Date.now() - startTime < 800) {
                        e.preventDefault();
                        handler();
                    }
                    pressed = false;
                });
            } else {
                el.addEventListener('touchstart', () => {
                    pressed = true;
                    startTime = Date.now();
                }, { passive: true });
                el.addEventListener('touchend', (e) => {
                    if (!pressed) return;
                    if (Date.now() - startTime < 800) {
                        e.preventDefault();
                        handler();
                    }
                    pressed = false;
                });
            }
        }
    };

    // ==================== UI 创建 ====================
    function createFloatingButton() {
        const btn = document.createElement('div');
        btn.id = 'floating-toggle-btn';

        const size = Styles.getSize('60px', '70px', '80px');
        const fontSize = Styles.getSize('24px', '28px', '32px');

        btn.style.cssText = `position:fixed;top:20px;right:20px;width:${size};height:${size};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;border:3px solid rgba(255,255,255,0.2);font-size:${fontSize};user-select:none;-webkit-tap-highlight-color:transparent;touch-action:manipulation;`;

        btn.innerHTML = '🎾';
        btn.title = '显示/隐藏抢票面板';

        Interaction.bind(btn, togglePanel);

        if (!Device.isTouch) {
            btn.addEventListener('mouseenter', () => btn.style.transform = 'scale(1.1)');
            btn.addEventListener('mouseleave', () => btn.style.transform = 'scale(1)');
        }

        document.body.appendChild(btn);
        return btn;
    }

    function createControlPanel() {
        const panel = document.createElement('div');
        panel.id = 'auto-booking-panel';

        const mobileStyles = Device.isMobile ?
            `width:calc(100vw - 30px);max-width:${Device.isIPad ? '500px' : '380px'};top:${Device.isIPad ? '120px' : '100px'};left:50%;font-size:${Device.isIPad ? '18px' : '16px'};max-height:calc(100vh - 150px);` :
            `width:400px;top:20px;right:90px;max-height:90vh;`;

        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:opacity 0.3s ease,transform 0.3s ease;-webkit-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent;`;

        const getTodayDate = () => {
            const d = new Date();
            return d.toISOString().split('T')[0];
        };

        panel.innerHTML = `
    <div style="margin-bottom:15px;text-align:center;position:relative;">
        <h3 style="margin:0;font-size:${Device.isMobile ? '20px' : '18px'};text-shadow:2px 2px 4px rgba(0,0,0,0.5);">🎾 自动抢票助手 v1.1.8</h3>
        <button id="close-panel" style="position:absolute;top:-5px;right:-5px;background:rgba(255,255,255,0.2);border:none;color:white;width:${Device.isMobile ? '35px' : '30px'};height:${Device.isMobile ? '35px' : '30px'};border-radius:50%;cursor:pointer;font-size:${Device.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:${Device.isMobile ? '8px 12px' : '5px 10px'};border-radius:5px;cursor:pointer;margin-top:5px;font-size:${Device.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;">
        <div style="margin-bottom:12px;">
            <label style="font-size:${Device.isMobile ? '14px' : '12px'};display:block;margin-bottom:3px;">👤 学号/工号:</label>
            <input id="user-id" type="text" value="${CONFIG.USER_INFO.YYRGH}" style="${Styles.input}">
        </div>
        <div style="margin-bottom:12px;">
            <label style="font-size:${Device.isMobile ? '14px' : '12px'};display:block;margin-bottom:3px;">📝 姓名:</label>
            <input id="user-name" type="text" value="${CONFIG.USER_INFO.YYRXM}" style="${Styles.input}">
        </div>
        <div style="margin-bottom:12px;">
            <label style="font-size:${Device.isMobile ? '14px' : '12px'};display:block;margin-bottom:3px;">📅 预约日期:</label>
            <input id="target-date" type="date" value="${CONFIG.TARGET_DATE}" style="${Styles.input}">
        </div>
        <div style="margin-bottom:12px;">
            <label style="font-size:${Device.isMobile ? '14px' : '12px'};display:block;margin-bottom:3px;">🏟️ 运动项目:</label>
            <select id="sport-type" style="${Styles.input}">
                ${Object.keys(SPORT_CODES).map(s => `<option value="${s}" ${s === CONFIG.SPORT ? 'selected' : ''}>${s}</option>`).join('')}
            </select>
        </div>
        <div style="margin-bottom:12px;">
            <label style="font-size:${Device.isMobile ? '14px' : '12px'};display:block;margin-bottom:3px;">🏫 校区:</label>
            <select id="campus" style="${Styles.input}">
                ${Object.keys(CAMPUS_CODES).map(c => `<option value="${c}" ${c === CONFIG.CAMPUS ? 'selected' : ''}>${c}</option>`).join('')}
            </select>
        </div>
        <div id="venue-selection" style="margin-bottom:12px;display:${shouldShowVenueSelection(CONFIG.SPORT, CONFIG.CAMPUS) ? 'block' : 'none'};">
            <label style="font-size:${Device.isMobile ? '14px' : '12px'};display:block;margin-bottom:3px;">🏟️ 优先场馆:</label>
            <select id="preferred-venue" style="${Styles.input}">
                ${getVenueOptions(CONFIG.SPORT, CONFIG.CAMPUS).map(opt =>
            `<option value="${opt.value}" ${CONFIG.PREFERRED_VENUE === opt.value ? 'selected' : ''}>${opt.label}</option>`
        ).join('')}
            </select>
        </div>
        <div style="margin-bottom:12px;">
            <label style="font-size:${Device.isMobile ? '14px' : '12px'};display:block;margin-bottom:8px;">⏰ 优先时间段:</label>
            <div id="time-slots-container" style="background:rgba(255,255,255,0.1);border-radius:4px;padding:8px;display:grid;grid-template-columns:repeat(${Device.isMobile ? '2' : '2'},1fr);gap:${Device.isMobile ? '6px' : '4px'};">
                ${TIME_SLOTS.map(slot => `<label style="display:flex;align-items:center;font-size:${Device.isMobile ? '13px' : '11px'};cursor:pointer;padding:${Device.isMobile ? '4px' : '2px'};"><input type="checkbox" value="${slot}" ${CONFIG.PREFERRED_TIMES.includes(slot) ? 'checked' : ''} style="margin-right:5px;transform:${Device.isMobile ? 'scale(1.2)' : 'scale(1)'};flex-shrink:0;"><span style="white-space:nowrap;">${slot}</span></label>`).join('')}
            </div>
        </div>
        <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:12px;">
            <div>
                <label style="font-size:${Device.isMobile ? '14px' : '12px'};display:block;margin-bottom:3px;">⏱️ 查询间隔(秒):</label>
                <input id="retry-interval" type="number" min="1" max="60" value="${CONFIG.RETRY_INTERVAL}" style="${Styles.input}">
            </div>
            <div>
                <label style="font-size:${Device.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="${Styles.input}">
            </div>
        </div>
        <div style="margin-bottom:12px;">
            <label style="font-size:${Device.isMobile ? '14px' : '12px'};display:block;margin-bottom:3px;">⏰ 请求超时(秒):</label>
            <input id="request-timeout" type="number" min="5" max="60" value="${CONFIG.REQUEST_TIMEOUT}" style="${Styles.input}">
        </div>
        <button id="save-config" style="${Styles.button}background:linear-gradient(45deg,#4caf50,#45a049);color:white;font-size:${Device.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:${Device.isMobile ? '15px' : '13px'};margin-bottom:5px;">👤 <span id="display-user">${CONFIG.USER_INFO.YYRXM} (${CONFIG.USER_INFO.YYRGH})</span></div>
        <div style="font-size:${Device.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:${Device.isMobile ? '15px' : '13px'};margin-bottom:5px;display:${shouldShowVenueSelection(CONFIG.SPORT, CONFIG.CAMPUS) ? 'block' : 'none'};">🏟️ 优先场馆: <span id="display-venue">${CONFIG.PREFERRED_VENUE}</span></div>
        <div style="font-size:${Device.isMobile ? '15px' : '13px'};margin-bottom:5px;">⏰ <span id="display-times">${CONFIG.PREFERRED_TIMES.join(', ')}</span></div>
        <div style="font-size:${Device.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:${Device.isMobile ? '15px' : '13px'};margin-top:5px;">🎯 进度: <span id="booking-progress">0/${getMaxBookings()} 个时段</span></div>
    </div>

    <div style="background:rgba(255,255,255,0.15);padding:12px;border-radius:8px;margin-bottom:15px;">
        <div style="font-size:${Device.isMobile ? '15px' : '13px'};margin-bottom:8px;font-weight:bold;">⏰ 定时抢票</div>
        <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:8px;">
            <div>
                <label style="font-size:${Device.isMobile ? '13px' : '11px'};display:block;margin-bottom:3px;">日期:</label>
                <input id="scheduled-date" type="date" value="${getTodayDate()}" style="${Styles.input}font-size:${Device.isMobile ? '14px' : '12px'};padding:${Device.isMobile ? '8px' : '6px'};">
            </div>
            <div>
                <label style="font-size:${Device.isMobile ? '13px' : '11px'};display:block;margin-bottom:3px;">时间:</label>
                <input id="scheduled-time" type="time" value="12:30" style="${Styles.input}font-size:${Device.isMobile ? '14px' : '12px'};padding:${Device.isMobile ? '8px' : '6px'};">
            </div>
        </div>
        <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;">
            <button id="set-schedule-btn" style="${Styles.button}background:linear-gradient(45deg,#ff9800,#f57c00);color:white;font-size:${Device.isMobile ? '14px' : '12px'};padding:${Device.isMobile ? '10px' : '8px'};">⏰ 设置定时</button>
            <button id="cancel-schedule-btn" style="${Styles.button}background:linear-gradient(45deg,#9e9e9e,#757575);color:white;font-size:${Device.isMobile ? '14px' : '12px'};padding:${Device.isMobile ? '10px' : '8px'};">❌ 取消定时</button>
        </div>
        <div id="countdown-display" style="font-size:${Device.isMobile ? '14px' : '12px'};margin-top:8px;text-align:center;color:#ffd700;font-weight:bold;">未设置定时任务</div>
    </div>

    <div style="margin-bottom:15px;">
        <button id="start-btn" style="${Styles.button}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:${Device.isMobile ? '14px' : '12px'};max-height:${Device.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:${Device.isMobile ? '13px' : '11px'};opacity:0.8;">${Device.isMobile ? '📱 触摸优化版本' : '⚡ 快捷键: Ctrl+Shift+S 开始/停止'}</div>
    `;

        document.body.appendChild(panel);

        const transforms = Device.isMobile ?
            { visible: 'translateX(-50%) translateY(0)', hidden: 'translateX(-50%) translateY(-30px)' } :
            { visible: 'translateX(0)', hidden: 'translateX(100%)' };

        if (isPanelVisible) {
            panel.style.display = 'block';
            panel.style.opacity = '1';
            panel.style.transform = transforms.visible;
        } else {
            panel.style.display = 'none';
            panel.style.opacity = '0';
            panel.style.transform = transforms.hidden;
        }

        bindEvents(panel);
        return panel;
    }

    function togglePanel() {
        isPanelVisible = !isPanelVisible;
        Storage.set('panelVisible', isPanelVisible);

        const transforms = Device.isMobile ?
            { visible: 'translateX(-50%) translateY(0)', hidden: 'translateX(-50%) translateY(-30px)' } :
            { visible: 'translateX(0)', hidden: 'translateX(100%)' };

        if (isPanelVisible) {
            controlPanel.style.display = 'block';
            controlPanel.style.transform = transforms.hidden;
            controlPanel.style.opacity = '0';
            setTimeout(() => {
                controlPanel.style.opacity = '1';
                controlPanel.style.transform = transforms.visible;
            }, 10);
        } else {
            controlPanel.style.opacity = '0';
            controlPanel.style.transform = transforms.hidden;
            setTimeout(() => {
                if (!isPanelVisible) controlPanel.style.display = 'none';
            }, 300);
        }

        if (floatingButton) {
            floatingButton.style.background = isPanelVisible ?
                'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' :
                'linear-gradient(135deg, #ff6b6b 0%, #ee5a52 100%)';
            floatingButton.innerHTML = isPanelVisible ? '🎾' : '📱';
        }
    }

    function bindEvents(panel) {
        Interaction.bind(panel.querySelector('#close-panel'), togglePanel);

        Interaction.bind(panel.querySelector('#toggle-config'), () => {
            const area = panel.querySelector('#config-area');
            const btn = panel.querySelector('#toggle-config');
            if (area.style.display === 'none') {
                area.style.display = 'block';
                btn.textContent = '⚙️ 隐藏配置';
            } else {
                area.style.display = 'none';
                btn.textContent = '⚙️ 显示配置';
            }
        });

        const updateVenueDisplay = () => {
            const sport = panel.querySelector('#sport-type').value;
            const campus = panel.querySelector('#campus').value;
            const venueSelection = panel.querySelector('#venue-selection');
            const venueDisplay = panel.querySelector('#venue-display');
            const preferredVenueSelect = panel.querySelector('#preferred-venue');

            const show = shouldShowVenueSelection(sport, campus);

            if (venueSelection) venueSelection.style.display = show ? 'block' : 'none';
            if (venueDisplay) venueDisplay.style.display = show ? 'block' : 'none';

            // 更新场馆选项
            if (preferredVenueSelect && show) {
                const options = getVenueOptions(sport, campus);
                preferredVenueSelect.innerHTML = options.map(opt =>
                    `<option value="${opt.value}">${opt.label}</option>`
                ).join('');
            }
        };

        panel.querySelector('#sport-type').addEventListener('change', updateVenueDisplay);
        panel.querySelector('#campus').addEventListener('change', updateVenueDisplay);


        Interaction.bind(panel.querySelector('#save-config'), () => {
            updateConfigFromUI();
            updateDisplayConfig();
            addLog('💾 配置已保存', 'success');

            const area = panel.querySelector('#config-area');
            const btn = panel.querySelector('#toggle-config');
            if (area && btn) {
                area.style.display = 'none';
                btn.textContent = '⚙️ 配置设置';
            }
        });

        Interaction.bind(panel.querySelector('#start-btn'), () => {
            if (isRunning) {
                stopBooking();
            } else {
                updateConfigFromUI();
                if (validateConfig()) startBooking();
            }
        });

        Interaction.bind(panel.querySelector('#set-schedule-btn'), () => {
            const dateInput = panel.querySelector('#scheduled-date').value;
            const timeInput = panel.querySelector('#scheduled-time').value;

            if (!dateInput || !timeInput) {
                addLog('❌ 请选择定时日期和时间', 'error');
                return;
            }

            // 构建完整的日期时间字符串
            const dateTimeString = `${dateInput}T${timeInput}:00`;
            const targetTime = new Date(dateTimeString).getTime();

            // 验证日期有效性
            if (isNaN(targetTime)) {
                addLog('❌ 日期时间格式无效', 'error');
                return;
            }

            // 检查是否是未来时间
            const now = Date.now();
            if (targetTime <= now) {
                addLog('❌ 定时时间必须晚于当前时间', 'error');
                addLog(`ℹ️ 当前时间: ${new Date(now).toLocaleString('zh-CN')}`, 'info');
                addLog(`ℹ️ 设置时间: ${new Date(targetTime).toLocaleString('zh-CN')}`, 'info');
                return;
            }

            if (ScheduledTask.set(targetTime)) {
                startCountdown();
            }
        });

        Interaction.bind(panel.querySelector('#cancel-schedule-btn'), () => {
            ScheduledTask.clear();
            stopCountdown();
            updateCountdownDisplay('未设置定时任务');
            addLog('❌ 已取消定时任务', 'info');
        });

        if (!Device.isMobile) {
            document.addEventListener('keydown', (e) => {
                if (e.ctrlKey && e.shiftKey && e.key === 'S') {
                    e.preventDefault();
                    panel.querySelector('#start-btn').click();
                } else if (e.ctrlKey && e.shiftKey && e.key === 'H') {
                    e.preventDefault();
                    togglePanel();
                }
            });
        }
    }

    function updateCountdownDisplay(text) {
        const display = document.getElementById('countdown-display');
        if (display) display.textContent = text;
    }

    function startCountdown() {
        stopCountdown();

        const updateDisplay = () => {
            const remaining = ScheduledTask.getRemaining();
            if (remaining === null) {
                stopCountdown();
                updateCountdownDisplay('未设置定时任务');
                return false;
            }

            const formatted = ScheduledTask.formatRemaining();
            updateCountdownDisplay(`⏰ 倒计时: ${formatted}`);
            return true;
        };

        // 立即更新一次
        if (!updateDisplay()) return;

        // 每秒更新一次
        countdownInterval = setInterval(() => {
            if (!updateDisplay()) {
                stopCountdown();
            }
        }, 1000);
    }

    function stopCountdown() {
        if (countdownInterval) {
            clearInterval(countdownInterval);
            countdownInterval = null;
        }
    }

    // ==================== 配置和日志 ====================
    function updateConfigFromUI() {
        const selectedTimes = Array.from(document.querySelectorAll('#time-slots-container input:checked')).map(cb => cb.value);
        const campus = document.getElementById('campus').value;
        const sport = document.getElementById('sport-type').value;

        let venue = '全部';
        if (shouldShowVenueSelection(sport, campus)) {
            venue = document.getElementById('preferred-venue')?.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: sport,
            CAMPUS: campus,
            PREFERRED_VENUE: venue,
            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: getYYLX(sport, campus)
        };

        Storage.set('bookingConfig', CONFIG);
        updateProgress();

        addLog(`⚙️ 预约模式: ${CONFIG.YYLX === "2.0" ? "团体预约" : "单人散场"}`, 'info');

        if (shouldShowVenueSelection(sport, campus) && venue !== '全部') {
            addLog(`🏟️ 优先场馆: ${venue}`, 'info');
        }
    }

    function updateDisplayConfig() {
        document.getElementById('display-user').textContent = `${CONFIG.USER_INFO.YYRXM} (${CONFIG.USER_INFO.YYRGH})`;

        // 格式化日期为 年/月/日
        const formatDate = (dateStr) => {
            const date = new Date(dateStr);
            const year = date.getFullYear();
            const month = String(date.getMonth() + 1).padStart(2, '0');
            const day = String(date.getDate()).padStart(2, '0');
            return `${year}/${month}/${day}`;
        };

        document.getElementById('display-date').textContent = formatDate(CONFIG.TARGET_DATE);
        document.getElementById('display-sport').textContent = CONFIG.SPORT;
        document.getElementById('display-campus').textContent = CONFIG.CAMPUS;
        document.getElementById('display-venue').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 = [];

        if (!CONFIG.USER_INFO.YYRGH || !CONFIG.USER_INFO.YYRXM) errors.push('请填写用户信息');
        if (!/^\d{8,12}$/.test(CONFIG.USER_INFO.YYRGH)) errors.push('学号格式不正确');
        if (!/^[\u4e00-\u9fa5]{2,10}$/.test(CONFIG.USER_INFO.YYRXM)) errors.push('姓名格式不正确');
        if (!CONFIG.TARGET_DATE) errors.push('请选择日期');
        if (!CONFIG.PREFERRED_TIMES.length) errors.push('请选择时间段');

        errors.forEach(e => addLog(`❌ ${e}`, 'error'));
        if (!errors.length) addLog(`✅ 配置验证通过`, 'success');
        return !errors.length;
    }

    function addLog(msg, type = 'info') {
        const area = document.getElementById('status-area');
        if (!area) return;

        const colors = { info: '#e3f2fd', success: '#c8e6c9', warning: '#fff3e0', error: '#ffcdd2' };
        const entry = document.createElement('div');
        entry.style.cssText = `color:${colors[type]};margin-bottom:3px;border-left:3px solid ${colors[type]};padding-left:8px;`;
        entry.innerHTML = `[${new Date().toLocaleTimeString()}] ${msg}`;

        area.appendChild(entry);
        area.scrollTop = area.scrollHeight;

        while (area.children.length > 50) area.removeChild(area.firstChild);
    }

    function updateProgress() {
        const el = document.getElementById('booking-progress');
        if (el) el.textContent = `${successfulBookings.length}/${getMaxBookings()} 个时段`;
    }

    // ==================== 网络请求 ====================
    function getBaseUrl() {
        return window.location.href.includes('webvpn') ?
            'https://ehall-443.webvpn.szu.edu.cn' :
            'https://ehall.szu.edu.cn';
    }

    async function fetchWithTimeout(url, options, timeout = CONFIG.REQUEST_TIMEOUT * 1000) {
        const startTime = Date.now();
        let retry = 0;

        while (retry <= 3) {
            await RequestThrottler.wait();

            const controller = new AbortController();
            const timeoutId = setTimeout(() => controller.abort(), timeout);

            try {
                RequestThrottler.onStart();
                const response = await fetch(url, { ...options, signal: controller.signal, credentials: 'same-origin', mode: 'cors', cache: 'no-cache' });
                clearTimeout(timeoutId);
                RequestThrottler.onEnd();

                if (!response.ok) {
                    const result = await NetworkErrorHandler.handle(new Error(`HTTP ${response.status}`), response, retry);
                    if (result.shouldStop) throw new Error('请求终止');
                    if (result.shouldRetry && retry < 3) {
                        retry++;
                        await new Promise(r => setTimeout(r, result.retryDelay));
                        continue;
                    }
                    throw new Error(`HTTP ${response.status}`);
                }

                return response;
            } catch (error) {
                clearTimeout(timeoutId);
                RequestThrottler.onEnd();

                if (retry >= 3) throw error;

                const result = await NetworkErrorHandler.handle(error, null, retry);
                if (result.shouldStop || !result.shouldRetry) throw error;

                retry++;
                await new Promise(r => setTimeout(r, result.retryDelay));
            }
        }
    }

    async function getAvailableSlots() {
        try {
            const slots = [];
            const baseUrl = getBaseUrl();
            const remaining = CONFIG.PREFERRED_TIMES.filter(t => !successfulBookings.some(b => b.timeSlot === t));

            if (!remaining.length) return [];

            for (const timeSlot of remaining) {
                const [start, end] = timeSlot.split("-");
                const payload = new URLSearchParams({
                    XMDM: SPORT_CODES[CONFIG.SPORT],
                    YYRQ: CONFIG.TARGET_DATE,
                    YYLX: CONFIG.YYLX,
                    KSSJ: start,
                    JSSJ: end,
                    XQDM: CAMPUS_CODES[CONFIG.CAMPUS]
                });

                const res = await fetchWithTimeout(`${baseUrl}/qljfwapp/sys/lwSzuCgyy/modules/sportVenue/getOpeningRoom.do`, {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
                    body: payload
                });

                const data = await res.json();
                if (data.code !== "0") {
                    addLog(`⚠️ ${timeSlot} 查询失败: ${data.msg || '未知错误'}`, 'warning');
                    continue;
                }

                const rooms = data.datas?.getOpeningRoom?.rows || [];

                rooms.forEach(room => {
                    let isAvailable = false;

                    if (room.disabled === true || room.disabled === "true") {
                        return;
                    }

                    const textValue = String(room.text || '').trim();

                    if (textValue === "可预约") {
                        isAvailable = true;
                    } else if (/^\d+\/\d+$/.test(textValue)) {
                        const [remaining, total] = textValue.split('/').map(n => parseInt(n.trim()));
                        isAvailable = remaining > 0 && remaining <= total;
                    }

                    if (!isAvailable) return;

                    const fullName = room.CGBM_DISPLAY || room.CDMC || '';

                    // 模块化场馆过滤逻辑
                    if (shouldShowVenueSelection(CONFIG.SPORT, CONFIG.CAMPUS) && CONFIG.PREFERRED_VENUE !== "全部") {
                        const venueFilter = VENUE_CONFIG[CONFIG.SPORT]?.[CONFIG.CAMPUS]?.filter;
                        if (venueFilter && !venueFilter(fullName, CONFIG.PREFERRED_VENUE)) {
                            return; // 如果不匹配优先场馆,则跳过
                        }
                    }

                    let venuePriority = 2, courtPriority = 0;

                    if (CONFIG.CAMPUS === "丽湖") {
                        if (CONFIG.SPORT === "羽毛球") {
                            if (fullName.includes("至畅")) {
                                venuePriority = 0;
                                const name = room.CDMC || '';
                                if (name.includes("5号场") || name.includes("五号场")) courtPriority = -2;
                                else if (name.includes("10号场") || name.includes("十号场")) courtPriority = -1;
                                else if (name.match(/[^0-9]1号场|^1号场|一号场/) || name.includes("6号场") || name.includes("六号场")) courtPriority = 2;
                            } else if (fullName.includes("至快")) {
                                venuePriority = 1;
                            }
                        }
                    } else if (CONFIG.CAMPUS === "粤海") {
                        if (CONFIG.SPORT === "篮球") {
                            venuePriority = 0;
                            courtPriority = 0;
                        } else if (CONFIG.SPORT === "网球") {
                            if (fullName.includes("海边")) {
                                venuePriority = 0;
                            } else if (fullName.includes("北区")) {
                                venuePriority = 1;
                            }
                        }
                    }

                    let availableCount = null;
                    if (/^\d+\/\d+$/.test(textValue)) {
                        const [remaining, total] = textValue.split('/').map(n => parseInt(n.trim()));
                        availableCount = remaining;
                    }

                    slots.push({
                        wid: room.WID,
                        timeSlot,
                        startTime: start,
                        endTime: end,
                        venueName: room.CDMC || '',
                        venueFullName: fullName,
                        venueCode: room.CGBM || '',
                        priority: CONFIG.PREFERRED_TIMES.indexOf(timeSlot),
                        venuePriority,
                        courtPriority,
                        availableCount
                    });
                });
            }

            slots.sort((a, b) => a.courtPriority - b.courtPriority || a.venuePriority - b.venuePriority || a.priority - b.priority);
            return slots;
        } catch (error) {
            addLog(`🔥 获取场地失败: ${error.message}`, 'error');
            return [];
        }
    }

    async function bookSlot(slot) {
        try {
            const { wid, timeSlot, startTime, endTime, venueName, venueCode, venueFullName } = slot;

            if (!timeSlot || !venueCode) {
                addLog(`❌ 预约参数缺失`, 'error');
                return false;
            }

            const payload = new URLSearchParams({
                DHID: "",
                YYRGH: CONFIG.USER_INFO.YYRGH,
                CYRS: "",
                YYRXM: CONFIG.USER_INFO.YYRXM,
                CGDM: venueCode,
                CDWID: wid,
                XMDM: SPORT_CODES[CONFIG.SPORT],
                XQWID: CAMPUS_CODES[CONFIG.CAMPUS],
                KYYSJD: timeSlot,
                YYRQ: CONFIG.TARGET_DATE,
                YYLX: CONFIG.YYLX,
                YYKS: `${CONFIG.TARGET_DATE} ${startTime}`,
                YYJS: `${CONFIG.TARGET_DATE} ${endTime}`,
                PC_OR_PHONE: "pc"
            });

            const res = await fetchWithTimeout(`${getBaseUrl()}/qljfwapp/sys/lwSzuCgyy/sportVenue/insertVenueBookingInfo.do`, {
                method: 'POST',
                headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
                body: payload
            });

            const contentType = res.headers.get('content-type') || '';

            if (contentType.includes('text/html')) {
                const html = await res.text();
                const errorMatch = html.match(/<h4>出错信息:<\/h4>[\s\S]*?<div class="bh-text-caption bh-color-caption">\s*(.*?)\s*<\/div>/);
                const errorMsg = errorMatch ? errorMatch[1].trim() : '系统异常';

                addLog(`⚠️ ${errorMsg}`, 'warning');

                if (errorMsg.includes('已预约该场地的相同时间段') || errorMsg.includes('已预约')) {
                    addLog(`📌 ${timeSlot} 已预约过,跳过`, 'info');
                    successfulBookings.push({
                        timeSlot,
                        venueName: '已预约',
                        dhid: 'duplicate',
                        slotName: `${timeSlot} (重复)`
                    });
                    updateProgress();
                    return 'already_booked';
                }

                return false;
            }

            const result = await res.json();

            if (result.code === "0" && result.msg === "成功") {
                const dhid = result.data?.DHID || "Unknown";
                const displayName = venueFullName ? `${venueFullName}-${venueName}` : venueName;

                addLog(`🎉 预约成功!场地:${displayName}`, 'success');
                addLog(`📋 预约单号:${dhid}`, 'success');

                successfulBookings.push({ timeSlot, venueName: displayName, dhid, slotName: displayName });
                updateProgress();

                WeChatNotifier.sendSuccess({
                    userName: CONFIG.USER_INFO.YYRXM,
                    userId: CONFIG.USER_INFO.YYRGH,
                    date: CONFIG.TARGET_DATE,
                    sport: CONFIG.SPORT,
                    campus: CONFIG.CAMPUS,
                    venueName: displayName,
                    timeSlot,
                    dhid
                });

                return true;
            } else {
                addLog(`❌ 预约失败:${result.msg}`, 'error');
                if (result.msg?.includes("只能预订2次") || result.msg?.includes("超过限制")) {
                    addLog(`🎊 已达预约上限`, 'success');
                    return 'limit_reached';
                }
                return false;
            }
        } catch (error) {
            if (error.message.includes('JSON') || error.message.includes('Unexpected token')) {
                addLog(`⚠️ 服务器返回异常格式`, 'warning');
                return false;
            }
            addLog(`💥 预约异常: ${error.message}`, 'error');
            return false;
        }
    }

    // ==================== 主流程 ====================
    async function startBooking() {
        if (isRunning) return;

        isRunning = true;
        retryCount = 0;
        startTime = new Date();
        const max = getMaxBookings();

        SmartRetry.reset();

        const btn = document.getElementById('start-btn');
        if (btn) {
            btn.textContent = '⏹️ 停止抢票';
            btn.style.background = 'linear-gradient(45deg, #f44336, #d32f2f)';
        }

        // 格式化日期显示
        const formatDate = (dateStr) => {
            const date = new Date(dateStr);
            const year = date.getFullYear();
            const month = String(date.getMonth() + 1).padStart(2, '0');
            const day = String(date.getDate()).padStart(2, '0');
            return `${year}/${month}/${day}`;
        };

        addLog(`🚀 开始抢票!`, 'success');
        addLog(`📊 ${CONFIG.SPORT} | ${CONFIG.CAMPUS} | ${formatDate(CONFIG.TARGET_DATE)}`, 'info');

        try {
            while (isRunning && retryCount < CONFIG.MAX_RETRY_TIMES) {
                if (successfulBookings.length >= max) {
                    addLog(`🎊 成功预约 ${max} 个时段`, 'success');
                    break;
                }

                retryCount++;
                if (retryCount === 1 || retryCount % 10 === 0) {
                    addLog(`🔍 第 ${retryCount} 次查询 (${successfulBookings.length}/${max})`);
                }

                try {
                    const slots = await getAvailableSlots();

                    if (slots.length) {
                        SmartRetry.onSuccess();
                        addLog(`🎉 找到 ${slots.length} 个可预约场地`, 'success');

                        const groups = {};
                        slots.forEach(s => {
                            if (!groups[s.timeSlot]) groups[s.timeSlot] = [];
                            groups[s.timeSlot].push(s);
                        });

                        for (const time of CONFIG.PREFERRED_TIMES) {
                            if (successfulBookings.length >= max) break;
                            if (successfulBookings.some(b => b.timeSlot === time)) continue;

                            if (groups[time]) {
                                groups[time].sort((a, b) => a.courtPriority - b.courtPriority || a.venuePriority - b.venuePriority);
                                const result = await bookSlot(groups[time][0]);

                                if (result === 'limit_reached') {
                                    addLog(`🏁 已达预约上限,停止抢票`, 'success');
                                    break;
                                }
                                if (result === 'already_booked') {
                                    continue;
                                }

                                await new Promise(r => setTimeout(r, 500));
                            }
                        }
                    } else {
                        SmartRetry.onFailure();
                        if (retryCount <= 3 || retryCount % 20 === 0) {
                            addLog(`🔍 暂无可预约场地`, 'warning');
                        }
                    }
                } catch (error) {
                    SmartRetry.onFailure();
                    if (NetworkErrorHandler.categorize(error) === 'auth_error') {
                        addLog(`🔐 认证错误`, 'error');
                        break;
                    }
                }

                if (successfulBookings.length < max && isRunning) {
                    const interval = CONFIG.RETRY_INTERVAL * 1000 + (Math.random() * 200 - 100);
                    await new Promise(r => setTimeout(r, Math.max(100, interval)));
                }
            }
        } finally {
            stopBooking();
        }
    }

    function stopBooking() {
        if (!isRunning) return;
        isRunning = false;

        if (Device.isMobile) MobileOptimization.cleanup();

        const btn = document.getElementById('start-btn');
        if (btn) {
            btn.textContent = '🚀 开始抢票';
            btn.style.background = 'linear-gradient(45deg, #ff6b6b, #ee5a52)';
        }

        const max = getMaxBookings();
        const realBookings = successfulBookings.filter(b => b.dhid !== 'duplicate');

        if (realBookings.length) {
            addLog(`🎉 成功预约 ${realBookings.length}/${max} 个时段`, 'success');
            realBookings.forEach((b, i) => addLog(`${i + 1}. ${b.slotName} (${b.dhid})`, 'success'));
        } else {
            addLog(`😢 未成功预约`, 'warning');
        }

        const elapsed = startTime ? Math.round((new Date() - startTime) / 1000) : 0;
        addLog(`📊 运行${elapsed}秒,查询${retryCount}次`, 'info');
    }

    // ==================== 初始化 ====================
    function init() {
        const url = window.location.href;
        if (!url.includes('ehall.szu.edu.cn/qljfwapp/sys/lwSzuCgyy') &&
            !url.includes('ehall-443.webvpn.szu.edu.cn/qljfwapp/sys/lwSzuCgyy')) return;

        const cleaned = Storage.cleanup();
        if (cleaned) addLog(`🧹 清理 ${cleaned} 个过期项`, 'info');

        const getTodayDateTime = () => {
            const d = new Date();
            const date = d.toISOString().split('T')[0];
            const hours = String(d.getHours()).padStart(2, '0');
            const minutes = String(d.getMinutes()).padStart(2, '0');
            return { date, time: `${hours}:${minutes}` };
        };

        // 设置默认定时时间为当前时间的下一个小时
        const defaultDateTime = getTodayDateTime();
        const scheduledDateInput = document.getElementById('scheduled-date');
        const scheduledTimeInput = document.getElementById('scheduled-time');

        if (scheduledDateInput && scheduledTimeInput) {
            scheduledDateInput.value = defaultDateTime.date;
            // 默认设置为下一个整点
            const nextHour = new Date();
            nextHour.setHours(nextHour.getHours() + 1, 0, 0, 0);
            scheduledTimeInput.value = `${String(nextHour.getHours()).padStart(2, '0')}:00`;
        }

        if (Device.isMobile) MobileOptimization.init();
        SmartRetry.reset();

        CONFIG.TARGET_DATE = getTomorrowDate();

        floatingButton = createFloatingButton();
        controlPanel = createControlPanel();
        updateDisplayConfig();

        document.getElementById('target-date').value = getTomorrowDate();

        // 恢复定时任务
        if (ScheduledTask.restore()) {
            startCountdown();
            addLog(`🔄 已恢复定时任务`, 'success');
        }

        addLog(`🎮 抢票助手已就绪 (${Device.isIPad ? 'iPad' : (Device.isMobile ? '移动端' : '桌面端')})`, 'success');
    }

    document.addEventListener('visibilitychange', () => {
        if (!document.hidden) {
            const newDate = getTomorrowDate();
            if (CONFIG.TARGET_DATE !== newDate) {
                CONFIG.TARGET_DATE = newDate;
                document.getElementById('target-date').value = newDate;
                updateDisplayConfig();
                Storage.set('bookingConfig', CONFIG);

                // 格式化日期显示
                const formatDate = (dateStr) => {
                    const date = new Date(dateStr);
                    const year = date.getFullYear();
                    const month = String(date.getMonth() + 1).padStart(2, '0');
                    const day = String(date.getDate()).padStart(2, '0');
                    return `${year}/${month}/${day}`;
                };

                addLog(`📅 日期已更新: ${formatDate(newDate)}`, 'info');
            }
        }
    });

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        setTimeout(init, 100);
    }
})();