Greasy Fork

Greasy Fork is available in English.

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

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

当前为 2025-10-30 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         深圳大学体育场馆自动抢票
// @namespace    http://tampermonkey.net/
// @version      1.1.7
// @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.7',

        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"];

    // ==================== 配置管理 ====================
    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";
    }

    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);
        return config;
    }

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

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

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

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

                const targetDate = new Date(targetTime);
                addLog(`⏰ 已设置定时任务: ${targetDate.toLocaleString()}`, 'success');
                return true;
            } else {
                addLog(`❌ 定时时间必须晚于当前时间`, 'error');
                return false;
            }
        },

        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;
        },

        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()) {
                this.set(savedTime);
                return true;
            }
            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.7</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:${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}">
                <option value="至畅" ${CONFIG.PREFERRED_VENUE === '至畅' ? 'selected' : ''}>🏆 至畅体育馆</option>
                <option value="至快" ${CONFIG.PREFERRED_VENUE === '至快' ? 'selected' : ''}>⚡ 至快体育馆</option>
                <option value="全部" ${CONFIG.PREFERRED_VENUE === '全部' ? 'selected' : ''}>🔄 全部场馆</option>
            </select>
        </div>
        <div style="margin-bottom:12px;">
            <label style="font-size:${Device.isMobile ? '14px' : '12px'};display:block;margin-bottom:3px;">⏰ 优先时间段:</label>
            <div id="time-slots-container" style="max-height:${Device.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:${Device.isMobile ? '14px' : '11px'};margin:${Device.isMobile ? '5px 0' : '2px 0'};cursor:pointer;"><input type="checkbox" value="${slot}" ${CONFIG.PREFERRED_TIMES.includes(slot) ? 'checked' : ''} style="margin-right:5px;transform:${Device.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:${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:${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 show = sport === '羽毛球' && campus === '丽湖';
            if (venueSelection) venueSelection.style.display = show ? 'block' : 'none';
            if (venueDisplay) venueDisplay.style.display = show ? 'block' : 'none';
        };

        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');
            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 targetTime = new Date(`${dateInput} ${timeInput}`).getTime();

            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();
        countdownInterval = setInterval(() => {
            const remaining = ScheduledTask.formatRemaining();
            if (remaining === '未设置') {
                stopCountdown();
                updateCountdownDisplay('未设置定时任务');
            } else {
                updateCountdownDisplay(`⏰ 倒计时: ${remaining}`);
            }
        }, 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 (sport === '羽毛球' && campus === '丽湖') {
            venue = document.getElementById('preferred-venue')?.value || '至畅';
        } else if (sport === '羽毛球' && campus === '粤海') {
            venue = '全部';
        }

        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) // 使用动态获取的YYLX值
        };

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

        // 添加日志显示当前使用的YYLX
        addLog(`⚙️ 预约模式: ${CONFIG.YYLX === "2.0" ? "团体预约" : "单人散场"}`, 'info');
    }

    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;
        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 (CONFIG.SPORT === "羽毛球" && CONFIG.CAMPUS === "丽湖" && CONFIG.PREFERRED_VENUE !== "全部") {
                        if ((CONFIG.PREFERRED_VENUE === "至畅" && !fullName.includes("至畅")) ||
                            (CONFIG.PREFERRED_VENUE === "至快" && !fullName.includes("至快"))) {
                            return;
                        }
                    }

                    let venuePriority = 2, courtPriority = 0;

                    if (CONFIG.CAMPUS === "丽湖" && 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.SPORT === "篮球") {
                        venuePriority = 0;
                        courtPriority = 0;
                    }

                    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') || '';

            // 检查是否返回HTML错误页面
            if (contentType.includes('text/html')) {
                const html = await res.text();

                // 尝试从HTML中提取错误信息
                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;
            }

            // 正常JSON响应
            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) {
            // 捕获JSON解析错误
            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)';
        }

        addLog(`🚀 开始抢票!`, 'success');
        addLog(`📊 ${CONFIG.SPORT} | ${CONFIG.CAMPUS} | ${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');

        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(`🎮 抢票助手已就绪 (${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);
                addLog(`📅 日期已更新: ${newDate}`, 'info');
            }
        }
    });

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