// ==UserScript== // @name 12306抢票助手 // @namespace http://tampermonkey.net/ // @version 1.1 // @description 12306购票辅助工具(仅供个人学习使用) // @author You // @match https://kyfw.12306.cn/otn/leftTicket/init* // @match https://kyfw.12306.cn/otn/confirmPassenger/* // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @grant GM_xmlhttpRequest // @connect kyfw.12306.cn // ==/UserScript== (function() { 'use strict'; // 配置对象 const config = { fromStation: GM_getValue('fromStation', ''), toStation: GM_getValue('toStation', ''), trainDate: GM_getValue('trainDate', ''), trainCodes: GM_getValue('trainCodes', ''), seatTypes: GM_getValue('seatTypes', 'O'), timePeriods: GM_getValue('timePeriods', 'morning,afternoon,evening,night'), passengers: GM_getValue('passengers', ''), passengerType: GM_getValue('passengerType', 'adult'), acceptWaitlist: GM_getValue('acceptWaitlist', false), autoSubmit: false, enableNotification: GM_getValue('enableNotification', true), enableSound: GM_getValue('enableSound', true), queryInterval: 500, isRunning: false }; let queryTimer = null; // 创建控制面板 function createControlPanel() { const panel = document.createElement('div'); panel.id = 'ticket-helper-panel'; panel.innerHTML = `

🎫 12306抢票助手

状态:未启动
`; document.body.appendChild(panel); addStyles(); bindEvents(); } // 添加样式 function addStyles() { GM_addStyle(` #ticket-helper-panel { position: fixed; top: 10px; right: 10px; width: 380px; max-height: 90vh; background: white; border: 2px solid #1890ff; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); z-index: 999999; font-family: Arial, sans-serif; overflow: hidden; } .helper-header { background: #1890ff; color: white; padding: 12px; display: flex; justify-content: space-between; align-items: center; } .helper-header h3 { margin: 0; font-size: 16px; } #helper-toggle { background: transparent; border: 1px solid white; color: white; padding: 4px 12px; border-radius: 4px; cursor: pointer; } .helper-content { padding: 15px; max-height: calc(90vh - 50px); overflow-y: auto; } .config-item { margin-bottom: 12px; } .config-item label { display: block; margin-bottom: 4px; font-size: 13px; color: #333; } .config-item input[type="text"], .config-item input[type="date"], .config-item input[type="number"] { width: 100%; padding: 6px; border: 1px solid #d9d9d9; border-radius: 4px; font-size: 13px; box-sizing: border-box; } .seat-types-group, .time-periods-group { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; padding: 8px; background: #fafafa; border-radius: 4px; } .passenger-type-group { display: flex; gap: 20px; padding: 8px; background: #fff7e6; border-radius: 4px; } .checkbox-label, .radio-label { display: flex; align-items: center; cursor: pointer; font-size: 13px; } .checkbox-label input, .radio-label input { margin-right: 6px; cursor: pointer; } .button-group { display: flex; gap: 8px; margin-top: 15px; } .button-group button { flex: 1; padding: 8px; border: none; border-radius: 4px; cursor: pointer; font-size: 13px; font-weight: bold; } .btn-primary { background: #1890ff; color: white; } .btn-info { background: #13c2c2; color: white; } .btn-success { background: #52c41a; color: white; } .btn-danger { background: #ff4d4f; color: white; } button:disabled { opacity: 0.5; cursor: not-allowed; } .status-area { margin-top: 15px; padding: 10px; background: #f5f5f5; border-radius: 4px; } #status-text { font-weight: bold; margin-bottom: 8px; color: #333; } #log-area { max-height: 200px; overflow-y: auto; font-size: 12px; color: #666; } .log-item { padding: 4px 0; border-bottom: 1px solid #e8e8e8; } .log-success { color: #52c41a; } .log-error { color: #ff4d4f; } .log-info { color: #1890ff; } `); } // 绑定事件 function bindEvents() { document.getElementById('helper-toggle').addEventListener('click', function() { const content = document.querySelector('.helper-content'); if (content.style.display === 'none') { content.style.display = 'block'; this.textContent = '收起'; } else { content.style.display = 'none'; this.textContent = '展开'; } }); document.getElementById('save-config').addEventListener('click', saveConfig); document.getElementById('test-fill').addEventListener('click', testFillForm); document.getElementById('fill-from-station').addEventListener('click', fillFromStationOnly); document.getElementById('test-notification').addEventListener('click', testNotification); document.getElementById('start-query').addEventListener('click', startQuery); document.getElementById('stop-query').addEventListener('click', stopQuery); } // 只填写出发地 function fillFromStationOnly() { if (!config.fromStation) { addLog('请先在配置中填写出发站', 'error'); return; } addLog('开始填写出发地...', 'info'); try { // 清除隐藏的出发站代码 const fromStationCode = document.getElementById('fromStation'); if (fromStationCode) { fromStationCode.value = ''; } // 填充出发站 const fromStationInput = document.getElementById('fromStationText'); if (fromStationInput) { addLog(`正在设置出发站: ${config.fromStation}`, 'info'); fromStationInput.value = ''; setTimeout(() => { fromStationInput.value = config.fromStation; fromStationInput.dispatchEvent(new Event('focus', { bubbles: true })); fromStationInput.dispatchEvent(new Event('input', { bubbles: true })); fromStationInput.dispatchEvent(new Event('change', { bubbles: true })); setTimeout(() => { fromStationInput.dispatchEvent(new Event('blur', { bubbles: true })); // 验证 setTimeout(() => { const fromCode = fromStationCode ? fromStationCode.value : ''; if (fromCode) { addLog(`✓ 出发地填写成功: ${config.fromStation} (代码: ${fromCode})`, 'success'); addLog('请手动选择目的地', 'info'); } else { addLog(`[警告] 出发地已填写但站点代码未设置`, 'error'); addLog('[建议] 请手动点击出发站输入框,从下拉列表中选择', 'info'); } }, 500); }, 300); }, 100); } else { addLog('未找到出发站输入框', 'error'); } } catch (e) { addLog('填写出发地出错:' + e.message, 'error'); } } // 测试通知功能 function testNotification() { addLog('测试通知功能...', 'info'); if (config.enableSound) { playNotificationSound(); addLog('✓ 声音提醒已播放', 'success'); } if (config.enableNotification) { sendBrowserNotification('测试通知', '这是一条测试通知消息'); addLog('✓ 浏览器通知已发送', 'success'); } if (!config.enableSound && !config.enableNotification) { addLog('通知功能已全部禁用', 'info'); } } // 保存配置 function saveConfig() { config.fromStation = document.getElementById('from-station').value; config.toStation = document.getElementById('to-station').value; config.trainDate = document.getElementById('train-date').value; config.trainCodes = document.getElementById('train-codes').value; config.queryInterval = parseInt(document.getElementById('query-interval').value); config.passengers = document.getElementById('passengers').value; config.acceptWaitlist = document.getElementById('accept-waitlist').checked; config.autoSubmit = document.getElementById('auto-submit').checked; config.enableNotification = document.getElementById('enable-notification').checked; config.enableSound = document.getElementById('enable-sound').checked; const selectedSeats = []; document.querySelectorAll('input[name="seat-type"]:checked').forEach(cb => { selectedSeats.push(cb.value); }); config.seatTypes = selectedSeats.join(','); const selectedPeriods = []; document.querySelectorAll('input[name="time-period"]:checked').forEach(cb => { selectedPeriods.push(cb.value); }); config.timePeriods = selectedPeriods.join(','); const passengerTypeRadio = document.querySelector('input[name="passenger-type"]:checked'); if (passengerTypeRadio) { config.passengerType = passengerTypeRadio.value; } GM_setValue('fromStation', config.fromStation); GM_setValue('toStation', config.toStation); GM_setValue('trainDate', config.trainDate); GM_setValue('trainCodes', config.trainCodes); GM_setValue('queryInterval', config.queryInterval); GM_setValue('seatTypes', config.seatTypes); GM_setValue('timePeriods', config.timePeriods); GM_setValue('passengers', config.passengers); GM_setValue('passengerType', config.passengerType); GM_setValue('acceptWaitlist', config.acceptWaitlist); GM_setValue('enableNotification', config.enableNotification); GM_setValue('enableSound', config.enableSound); addLog('✓ 配置已保存', 'success'); // 如果启用了通知,请求权限 if (config.enableNotification && "Notification" in window && Notification.permission === "default") { Notification.requestPermission().then(function (permission) { if (permission === "granted") { addLog('✓ 浏览器通知权限已授予', 'success'); } else { addLog('浏览器通知权限被拒绝', 'error'); } }); } } // 测试填充表单 function testFillForm() { if (!config.fromStation || !config.toStation || !config.trainDate) { addLog('请先填写并保存配置', 'error'); return; } addLog('开始测试填充表单...', 'info'); if (fillPageForm()) { addLog('表单填充成功!', 'success'); } else { addLog('表单填充失败', 'error'); } } // 填充12306页面的表单 function fillPageForm() { try { addLog('[调试] 开始填充表单字段...', 'info'); // 先清除隐藏的站点代码字段,强制12306重新识别 const fromStationCode = document.getElementById('fromStation'); const toStationCode = document.getElementById('toStation'); if (fromStationCode) { fromStationCode.value = ''; addLog('[调试] 已清除出发站代码', 'info'); } if (toStationCode) { toStationCode.value = ''; addLog('[调试] 已清除到达站代码', 'info'); } // 填充出发站 const fromStationInput = document.getElementById('fromStationText'); if (fromStationInput) { addLog(`[调试] 设置出发站: ${config.fromStation}`, 'info'); fromStationInput.value = ''; // 先清空 setTimeout(() => { fromStationInput.value = config.fromStation; // 触发多个事件确保12306识别 fromStationInput.dispatchEvent(new Event('focus', { bubbles: true })); fromStationInput.dispatchEvent(new Event('input', { bubbles: true })); fromStationInput.dispatchEvent(new Event('change', { bubbles: true })); fromStationInput.dispatchEvent(new Event('blur', { bubbles: true })); }, 100); } else { addLog('[调试] 未找到出发站输入框', 'error'); } // 填充到达站 const toStationInput = document.getElementById('toStationText'); if (toStationInput) { addLog(`[调试] 设置到达站: ${config.toStation}`, 'info'); toStationInput.value = ''; // 先清空 setTimeout(() => { toStationInput.value = config.toStation; // 触发多个事件确保12306识别 toStationInput.dispatchEvent(new Event('focus', { bubbles: true })); toStationInput.dispatchEvent(new Event('input', { bubbles: true })); toStationInput.dispatchEvent(new Event('change', { bubbles: true })); toStationInput.dispatchEvent(new Event('blur', { bubbles: true })); }, 200); } else { addLog('[调试] 未找到到达站输入框', 'error'); } // 填充日期 const dateInput = document.getElementById('train_date'); if (dateInput) { addLog(`[调试] 设置日期: ${config.trainDate}`, 'info'); dateInput.value = config.trainDate; dateInput.dispatchEvent(new Event('change', { bubbles: true })); } else { addLog('[调试] 未找到日期输入框', 'error'); } // 验证填充结果 setTimeout(() => { const fromValue = fromStationInput ? fromStationInput.value : ''; const toValue = toStationInput ? toStationInput.value : ''; const dateValue = dateInput ? dateInput.value : ''; const fromCode = fromStationCode ? fromStationCode.value : ''; const toCode = toStationCode ? toStationCode.value : ''; addLog(`[验证] 出发站=${fromValue} (代码:${fromCode}), 到达站=${toValue} (代码:${toCode}), 日期=${dateValue}`, 'info'); if (fromValue === config.fromStation && toValue === config.toStation && dateValue === config.trainDate) { if (fromCode && toCode) { addLog('✓ 表单填充验证成功,站点代码已设置', 'success'); } else { addLog('[警告] 站点代码未设置,12306可能无法识别站点', 'error'); addLog('[建议] 请手动在12306页面点击出发站和到达站,从下拉列表中选择', 'info'); } } else { addLog('[警告] 表单填充可能未生效', 'error'); addLog('[建议] 请手动在12306页面填写出发站、到达站和日期', 'info'); } }, 1000); return true; } catch (e) { addLog('填充表单出错:' + e.message, 'error'); return false; } } // 开始查询 function startQuery() { config.isRunning = true; document.getElementById('start-query').disabled = true; document.getElementById('stop-query').disabled = false; document.getElementById('status-text').textContent = '状态:运行中...'; addLog('开始监控余票...', 'success'); addLog('━━━━━━━━━━━━━━━━━━━━━━━━━━━━', 'info'); addLog('📌 使用说明:', 'info'); addLog('1. 请在12306页面手动选择出发站、到达站、日期', 'info'); addLog('2. 点击12306的"查询"按钮', 'info'); addLog('3. 脚本会自动监控查询结果并抢票', 'info'); addLog('━━━━━━━━━━━━━━━━━━━━━━━━━━━━', 'info'); // 检查是否已经有查询结果 setTimeout(() => { const table = document.getElementById('queryLeftTable'); if (table && table.querySelectorAll('tbody tr').length > 0) { addLog('✓ 检测到查询结果,开始监控...', 'success'); checkTicketResults(); } else { addLog('[提示] 等待您在12306页面进行查询...', 'info'); addLog('[提示] 查询后脚本会自动开始监控', 'info'); // 定期检查是否有查询结果 waitForQueryResults(); } }, 500); } // 等待查询结果出现 function waitForQueryResults() { if (!config.isRunning) return; const table = document.getElementById('queryLeftTable'); if (table && table.querySelectorAll('tbody tr').length > 0) { addLog('✓ 检测到查询结果,开始监控余票...', 'success'); checkTicketResults(); } else { // 每2秒检查一次 setTimeout(waitForQueryResults, 2000); } } // 停止查询 function stopQuery() { config.isRunning = false; if (queryTimer) { clearTimeout(queryTimer); queryTimer = null; } document.getElementById('start-query').disabled = false; document.getElementById('stop-query').disabled = true; document.getElementById('status-text').textContent = '状态:已停止'; addLog('已停止监控', 'error'); } // 查询余票 function queryTickets() { if (!config.isRunning) return; addLog('触发查询...', 'info'); const queryButton = document.getElementById('query_ticket'); if (queryButton) { queryButton.click(); setTimeout(() => { checkTicketResults(); }, 2000); } else { checkTicketResults(); } } // 检查页面上的查询结果 function checkTicketResults() { addLog('[调试] 开始检查结果...', 'info'); // 尝试查找表格 const table = document.getElementById('queryLeftTable'); if (!table) { addLog('[调试] 未找到查询结果表格', 'error'); scheduleNextQuery(); return; } // 查找所有行 const allRows = table.querySelectorAll('tbody tr'); addLog(`[调试] 找到 ${allRows.length} 行`, 'info'); // 过滤有效行 const validRows = []; allRows.forEach((row, index) => { // 查找车次号 - 尝试多种选择器 const trainNum = row.querySelector('.train .number, .number, td:first-child a'); if (trainNum && trainNum.textContent.trim()) { validRows.push(row); } }); addLog(`[调试] 有效车次 ${validRows.length} 个`, 'info'); let foundTickets = false; let firstAvailableRow = null; validRows.forEach(row => { const trainNum = row.querySelector('.train .number, .number, td:first-child a'); const trainCode = trainNum.textContent.trim(); // 检查所有单元格,找有票的 const cells = row.querySelectorAll('td'); let hasTicket = false; cells.forEach(cell => { const text = cell.textContent.trim(); // 检查是否是数字或"有" if (text && !isNaN(text) && parseInt(text) > 0) { hasTicket = true; } else if (text === '有') { hasTicket = true; } }); if (hasTicket) { addLog(`✓ 发现有票:${trainCode} - 有票`, 'success'); foundTickets = true; // 记录第一个有票的行 if (!firstAvailableRow) { firstAvailableRow = row; } } }); // 如果发现有票且启用了自动提交 if (foundTickets && firstAvailableRow && config.autoSubmit) { const trainNum = firstAvailableRow.querySelector('.train .number, .number, td:first-child a'); const trainCode = trainNum ? trainNum.textContent.trim() : '未知'; addLog(`[调试] 准备点击预订按钮...`, 'info'); // 查找预订/购票按钮 - 尝试多种选择器 let bookButton = firstAvailableRow.querySelector('a.btn72'); if (!bookButton) { // 尝试其他选择器 bookButton = firstAvailableRow.querySelector('a[onclick*="submitOrderRequest"]'); } if (!bookButton) { // 查找最后一列的链接 const lastCell = firstAvailableRow.querySelector('td:last-child'); if (lastCell) { bookButton = lastCell.querySelector('a'); } } if (bookButton) { const buttonText = bookButton.textContent.trim(); addLog(`>>> 找到按钮"${buttonText}",自动点击:${trainCode}`, 'success'); // 标记为自动提交模式 sessionStorage.setItem('autoSubmitOrder', 'true'); // 点击按钮 bookButton.click(); // 停止查询 stopQuery(); return; } else { addLog(`[调试] 未找到预订按钮,请手动点击`, 'error'); } } else if (foundTickets && !config.autoSubmit) { addLog(`提示:发现有票但未启用自动提交,请手动预订`, 'info'); } if (!foundTickets) { addLog(`查询到 ${validRows.length} 个车次,暂无余票`, 'info'); } scheduleNextQuery(); } // 安排下一次查询 function scheduleNextQuery() { if (config.isRunning) { queryTimer = setTimeout(queryTickets, config.queryInterval); } } // 添加日志 function addLog(message, type = 'info') { const logArea = document.getElementById('log-area'); if (!logArea) return; const logItem = document.createElement('div'); logItem.className = `log-item log-${type}`; const time = new Date().toLocaleTimeString(); logItem.textContent = `[${time}] ${message}`; logArea.insertBefore(logItem, logArea.firstChild); while (logArea.children.length > 50) { logArea.removeChild(logArea.lastChild); } // 自动滚动到最新日志 logArea.scrollTop = 0; } // 自动选择乘客并提交订单 function autoSelectPassengersAndSubmit() { addLog('进入订单确认页面,开始自动处理...', 'info'); // 等待页面加载完成 - 增加等待时间确保DOM完全加载 setTimeout(() => { addLog('[调试] 页面加载完成,开始处理...', 'info'); // 1. 选择座位类型(如果有的话) selectSeatType(); // 2. 选择乘客 - 使用重试机制 setTimeout(() => { selectPassengersWithRetry(3); // 最多重试3次 }, 1500); }, 3000); // 增加到3秒 } // 带重试的乘客选择 function selectPassengersWithRetry(maxRetries, currentRetry = 0) { addLog(`[调试] 尝试选择乘客 (第 ${currentRetry + 1}/${maxRetries} 次)`, 'info'); const result = selectPassengers(); // 检查是否成功选择了乘客 setTimeout(() => { const checkedBoxes = document.querySelectorAll('#normal_passenger_id input.check[type="checkbox"]:checked'); addLog(`[调试] 当前已选中 ${checkedBoxes.length} 位乘客`, 'info'); if (checkedBoxes.length === 0 && currentRetry < maxRetries - 1) { addLog(`[警告] 未选中任何乘客,${1000}ms后重试...`, 'error'); setTimeout(() => { selectPassengersWithRetry(maxRetries, currentRetry + 1); }, 1000); } else if (checkedBoxes.length > 0) { addLog(`✓ 成功选中 ${checkedBoxes.length} 位乘客`, 'success'); // 3. 提交订单 setTimeout(() => { submitOrderWithCheck(); }, 2000); } else { addLog('[错误] 多次尝试后仍未能选择乘客,请手动操作', 'error'); // 即使失败也尝试提交,让用户手动选择 setTimeout(() => { addLog('[提示] 请手动选择乘客后点击提交', 'info'); }, 1000); } }, 500); } // 选择座位类型 function selectSeatType() { addLog('[调试] 尝试选择座位类型...', 'info'); // 查找座位类型下拉框 const seatSelects = document.querySelectorAll('select[id*="seat"], select[id*="Seat"], select[name*="seat"]'); if (seatSelects.length === 0) { addLog('[调试] 未找到座位类型下拉框', 'info'); return; } addLog(`[调试] 找到 ${seatSelects.length} 个座位选择框`, 'info'); let selectedCount = 0; seatSelects.forEach((select, index) => { addLog(`[调试] 检查座位选择框 ${index + 1}`, 'info'); const options = select.querySelectorAll('option'); let foundMatch = false; options.forEach(option => { const text = option.textContent.trim(); const value = option.value; // 根据配置选择座位类型 if (config.seatTypes.includes('3') && (text.includes('硬卧') || value === '3')) { option.selected = true; select.value = value; select.dispatchEvent(new Event('change', { bubbles: true })); addLog(`✓ 已选择:硬卧 (选择框 ${index + 1})`, 'success'); foundMatch = true; selectedCount++; } else if (config.seatTypes.includes('4') && (text.includes('软卧') || value === '4')) { option.selected = true; select.value = value; select.dispatchEvent(new Event('change', { bubbles: true })); addLog(`✓ 已选择:软卧 (选择框 ${index + 1})`, 'success'); foundMatch = true; selectedCount++; } else if (config.seatTypes.includes('O') && (text.includes('二等座') || value === 'O')) { option.selected = true; select.value = value; select.dispatchEvent(new Event('change', { bubbles: true })); addLog(`✓ 已选择:二等座 (选择框 ${index + 1})`, 'success'); foundMatch = true; selectedCount++; } else if (config.seatTypes.includes('M') && (text.includes('一等座') || value === 'M')) { option.selected = true; select.value = value; select.dispatchEvent(new Event('change', { bubbles: true })); addLog(`✓ 已选择:一等座 (选择框 ${index + 1})`, 'success'); foundMatch = true; selectedCount++; } else if (config.seatTypes.includes('1') && (text.includes('硬座') || value === '1')) { option.selected = true; select.value = value; select.dispatchEvent(new Event('change', { bubbles: true })); addLog(`✓ 已选择:硬座 (选择框 ${index + 1})`, 'success'); foundMatch = true; selectedCount++; } }); if (!foundMatch) { addLog(`[调试] 选择框 ${index + 1} 中未找到匹配的座位类型`, 'info'); } }); if (selectedCount === 0) { addLog('[提示] 未能自动选择座位类型,请手动选择', 'info'); } else { addLog(`✓ 共选择了 ${selectedCount} 个座位类型`, 'success'); } } // 选择乘客 function selectPassengers() { addLog('[调试] 尝试选择乘客...', 'info'); if (!config.passengers || config.passengers.trim() === '') { addLog('未配置乘车人,尝试选择第一个乘客', 'info'); // 如果没有配置乘客,尝试自动选择第一个 const firstCheckbox = document.querySelector('#normal_passenger_id input[type="checkbox"], input.check[id^="normalPassenger_"]'); if (firstCheckbox && !firstCheckbox.checked) { firstCheckbox.click(); addLog('已自动选择第一个乘客', 'success'); return true; } return false; } const passengerNames = config.passengers.split(',').map(name => name.trim()).filter(name => name); let selectedCount = 0; // 尝试多种选择器查找乘客列表 - 修复为正确的ID let passengerItems = document.querySelectorAll('#normal_passenger_id li'); if (passengerItems.length === 0) { passengerItems = document.querySelectorAll('#normalPassenger_list li'); } if (passengerItems.length === 0) { passengerItems = document.querySelectorAll('.passenger-list li'); } addLog(`[调试] 找到 ${passengerItems.length} 个乘客选项`, 'info'); // 如果找不到乘客列表,尝试直接查找所有复选框 if (passengerItems.length === 0) { addLog('[调试] 未找到乘客列表,尝试查找复选框...', 'info'); const allCheckboxes = document.querySelectorAll('input.check[id^="normalPassenger_"], input[type="checkbox"][id*="passenger"]'); addLog(`[调试] 找到 ${allCheckboxes.length} 个复选框`, 'info'); if (allCheckboxes.length > 0) { // 显示所有找到的复选框信息 allCheckboxes.forEach((cb, index) => { const label = document.querySelector(`label[for="${cb.id}"]`); const name = label ? label.textContent.trim() : '未知'; addLog(`[调试] 复选框 ${index}: ID=${cb.id}, 姓名=${name}, 已选=${cb.checked}`, 'info'); }); if (!allCheckboxes[0].checked) { allCheckboxes[0].click(); addLog('已选择第一个乘客(通过复选框)', 'success'); return true; } } return false; } // 显示所有找到的乘客信息 passengerItems.forEach((item, index) => { const label = item.querySelector('label'); const checkbox = item.querySelector('input[type="checkbox"]'); const name = label ? label.textContent.trim() : '未知'; const checkboxId = checkbox ? checkbox.id : '无'; addLog(`[调试] 乘客 ${index + 1}: 姓名=${name}, ID=${checkboxId}`, 'info'); }); passengerNames.forEach(targetName => { passengerItems.forEach(item => { // 查找姓名元素 - 根据实际HTML结构,姓名在label标签中 let nameElement = item.querySelector('label'); if (!nameElement) nameElement = item.querySelector('.name'); if (!nameElement) nameElement = item.querySelector('.passenger-name'); if (!nameElement) nameElement = item; const itemText = nameElement.textContent.trim(); if (itemText.includes(targetName)) { addLog(`[调试] 匹配到乘客:${targetName}`, 'info'); // 查找复选框 - 使用更精确的选择器 const checkbox = item.querySelector('input.check[type="checkbox"]') || item.querySelector('input[type="checkbox"]'); if (checkbox) { if (!checkbox.checked) { checkbox.click(); selectedCount++; addLog(`✓ 已选择乘客:${targetName}`, 'success'); } else { addLog(`乘客 ${targetName} 已经被选中`, 'info'); selectedCount++; // 已选中的也计入 } } else { addLog(`[调试] 未找到复选框`, 'error'); } } }); }); if (selectedCount === 0 && passengerNames.length > 0) { addLog('未找到匹配的乘客,尝试选择第一个', 'error'); // 尝试选择第一个乘客 - 使用更精确的选择器 const firstCheckbox = document.querySelector('#normal_passenger_id input.check[type="checkbox"]') || document.querySelector('input.check[id^="normalPassenger_"]') || document.querySelector('input[type="checkbox"]'); if (firstCheckbox && !firstCheckbox.checked) { firstCheckbox.click(); addLog('已自动选择第一个乘客', 'success'); return true; } return false; } else if (selectedCount > 0) { addLog(`✓ 成功选择 ${selectedCount} 位乘客`, 'success'); return true; } // 设置乘客类型 if (config.passengerType === 'student') { const studentRadios = document.querySelectorAll('input[type="radio"][value="3"]'); studentRadios.forEach(radio => { if (!radio.checked) { radio.click(); } }); addLog('已设置为学生票', 'success'); } return selectedCount > 0; } // 带检查的订单提交 function submitOrderWithCheck() { addLog('[调试] 准备提交订单...', 'info'); // 检查是否选择了乘客 const checkedBoxes = document.querySelectorAll('#normal_passenger_id input.check[type="checkbox"]:checked, input[type="checkbox"]:checked'); if (checkedBoxes.length === 0) { addLog('[错误] 未选择任何乘客,无法提交订单', 'error'); addLog('[提示] 请手动选择乘客', 'info'); return; } addLog(`[确认] 已选择 ${checkedBoxes.length} 位乘客,准备提交...`, 'success'); // 显示选中的乘客信息 checkedBoxes.forEach((cb, index) => { const label = document.querySelector(`label[for="${cb.id}"]`); const name = label ? label.textContent.trim() : '未知'; addLog(` 乘客 ${index + 1}: ${name}`, 'info'); }); // 等待一下再提交 setTimeout(() => { submitOrder(); }, 1000); } // 提交订单 function submitOrder() { addLog('[调试] 开始提交订单...', 'info'); // 查找提交订单按钮 - 尝试多种选择器 const submitButton = document.getElementById('submitOrder_id') || document.querySelector('#submitOrder_id') || document.querySelector('.btn_submit') || document.querySelector('a[onclick*="submitOrderRequest"]') || document.querySelector('a.btn72[onclick*="submit"]'); if (submitButton) { const buttonText = submitButton.textContent.trim(); addLog(`[确认] 找到提交按钮: "${buttonText}"`, 'success'); // 检查按钮是否可点击 if (submitButton.disabled || submitButton.classList.contains('disabled')) { addLog('[警告] 提交按钮被禁用,可能需要完成验证码', 'error'); addLog('[提示] 请完成验证码后手动点击提交', 'info'); // 播放提示音 playNotificationSound(); // 发送浏览器通知 sendBrowserNotification('请完成验证码', '订单已准备好,请完成验证码后提交'); return; } addLog('>>> 3秒后自动点击提交订单按钮...', 'success'); // 倒计时提示 let countdown = 3; const countdownInterval = setInterval(() => { addLog(`倒计时: ${countdown}秒...`, 'info'); countdown--; if (countdown < 0) { clearInterval(countdownInterval); } }, 1000); setTimeout(() => { addLog('>>> 点击提交订单按钮!', 'success'); submitButton.click(); // 播放成功提示音 playNotificationSound(); // 发送浏览器通知 sendBrowserNotification('订单已提交', '请尽快完成支付!'); setTimeout(() => { addLog('✓ 订单已提交!请尽快完成支付', 'success'); addLog('[重要] 请在30分钟内完成支付,否则订单将被取消', 'info'); }, 2000); }, 3000); } else { addLog('[错误] 未找到提交订单按钮', 'error'); addLog('[调试] 页面可能还在加载,或者需要完成验证码', 'info'); // 尝试查找验证码区域 const captchaArea = document.querySelector('.slide-verify, .nc-container, #nc_1_wrapper'); if (captchaArea) { addLog('[提示] 检测到验证码区域,请先完成验证码', 'info'); playNotificationSound(); sendBrowserNotification('需要验证码', '请完成滑块验证码'); } else { addLog('[提示] 请手动查找并点击提交按钮', 'info'); } } } // 播放通知提示音 function playNotificationSound() { if (!config.enableSound) return; try { // 创建一个简单的提示音 const audioContext = new (window.AudioContext || window.webkitAudioContext)(); const oscillator = audioContext.createOscillator(); const gainNode = audioContext.createGain(); oscillator.connect(gainNode); gainNode.connect(audioContext.destination); oscillator.frequency.value = 800; oscillator.type = 'sine'; gainNode.gain.setValueAtTime(0.3, audioContext.currentTime); gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.5); oscillator.start(audioContext.currentTime); oscillator.stop(audioContext.currentTime + 0.5); } catch (e) { // 如果音频API不可用,静默失败 } } // 发送浏览器通知 function sendBrowserNotification(title, message) { if (!config.enableNotification) return; if (!("Notification" in window)) { return; } if (Notification.permission === "granted") { new Notification(title, { body: message, icon: 'https://kyfw.12306.cn/otn/resources/images/icon_logo.png', tag: '12306-ticket-helper' }); } else if (Notification.permission !== "denied") { Notification.requestPermission().then(function (permission) { if (permission === "granted") { new Notification(title, { body: message, icon: 'https://kyfw.12306.cn/otn/resources/images/icon_logo.png', tag: '12306-ticket-helper' }); } }); } } // 初始化 function init() { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initApp); } else { initApp(); } } function initApp() { // 在购票页面显示控制面板 if (window.location.href.includes('leftTicket/init')) { createControlPanel(); } // 在订单确认页面,也显示控制面板(用于查看日志) else if (window.location.href.includes('confirmPassenger')) { // 创建简化版控制面板,只显示日志 createLogPanel(); // 检查是否是从脚本跳转过来的 if (sessionStorage.getItem('autoSubmitOrder') === 'true') { sessionStorage.removeItem('autoSubmitOrder'); addLog('自动提交模式已启用', 'success'); autoSelectPassengersAndSubmit(); } else { addLog('非自动模式,请手动操作', 'info'); } } } // 创建简化的日志面板 function createLogPanel() { const panel = document.createElement('div'); panel.id = 'ticket-helper-panel'; panel.innerHTML = `

🎫 抢票助手 - 订单确认

状态:处理中...
`; document.body.appendChild(panel); addStyles(); document.getElementById('helper-toggle').addEventListener('click', function() { const content = document.querySelector('.helper-content'); if (content.style.display === 'none') { content.style.display = 'block'; this.textContent = '收起'; } else { content.style.display = 'none'; this.textContent = '展开'; } }); } init(); })();