// ==UserScript== // @name 12306 Ticket Helper // @namespace http://tampermonkey.net/ // @version 4.0 // @description 12306 Ticket Booking Assistant - Full Chinese Version // @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'; var config = { fromStation: GM_getValue('fromStation', ''), toStation: GM_getValue('toStation', ''), trainDate: GM_getValue('trainDate', ''), trainCodes: GM_getValue('trainCodes', ''), seatTypes: GM_getValue('seatTypes', 'O,WZ'), timePeriods: GM_getValue('timePeriods', 'morning,afternoon,evening,night'), passengers: GM_getValue('passengers', ''), passengerType: GM_getValue('passengerType', 'adult'), acceptWaitlist: GM_getValue('acceptWaitlist', false), autoSubmit: GM_getValue('autoSubmit', false), enableNotification: GM_getValue('enableNotification', true), enableSound: GM_getValue('enableSound', true), queryInterval: 500, isRunning: false }; var timer = null; function createPanel() { var div = document.createElement('div'); div.id = 'TH12306'; var html = '' + '

🎫 12306 抢票助手 v4

' + '
' + '
' + '
' + '
' + '
' + '
' + '' + '' + '' + '' + '
' + '
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
' + '
' + '
' + '' + '' + '
' + '
' + '
' + '
' + '
' + '
' + '' + '' + '' + '
' + '
' + '
状态:未启动
' + '
' + '
' + '
'; div.innerHTML = html; document.body.appendChild(div); bindEvents(); } function bindEvents() { document.getElementById('TOG12306').onclick = function() { var c = document.querySelector('.T12306'); if (c.style.display === 'none') { c.style.display = 'block'; this.textContent = '收起'; } else { c.style.display = 'none'; this.textContent = '展开'; } }; document.getElementById('SAVE12306').onclick = saveConfig; document.getElementById('GO12306').onclick = startMonitor; document.getElementById('SP12306').onclick = stopMonitor; document.getElementById('AS12306').onchange = function() { config.autoSubmit = this.checked; GM_setValue('autoSubmit', this.checked); log('⚡ 自动提交 = ' + (this.checked ? '✅ 已开启 🔥🔥🔥' : '❌ 未开启'), 'OK'); }; // 恢复保存的自动提交状态 if (config.autoSubmit) document.getElementById('AS12306').checked = true; } function saveConfig() { config.fromStation = document.getElementById('FS12306').value; config.toStation = document.getElementById('TS12306').value; config.trainDate = document.getElementById('TD12306').value; config.trainCodes = document.getElementById('TC12306').value; config.passengers = document.getElementById('PP12306').value; config.autoSubmit = document.getElementById('AS12306').checked; config.acceptWaitlist = document.getElementById('WL12306').checked; config.enableNotification = document.getElementById('EN12306').checked; config.enableSound = document.getElementById('ES12306').checked; config.queryInterval = parseInt(document.getElementById('QI12306')?.value) || 500; // 收集时间段 var tp = []; document.querySelectorAll('input[name="TP12306"]:checked').forEach(function(cb) { tp.push(cb.value); }); config.timePeriods = tp.join(','); // 收集座位类型 var st = []; document.querySelectorAll('input[name="ST12306"]:checked').forEach(function(cb) { if(cb.value) st.push(cb.value); }); config.seatTypes = st.join(','); // 乘客类型 var pt = document.querySelector('input[name="PT12306"]:checked'); if (pt) config.passengerType = pt.value; // 保存到存储 GM_setValue('fromStation', config.fromStation); GM_setValue('toStation', config.toStation); GM_setValue('trainDate', config.trainDate); GM_setValue('trainCodes', config.trainCodes); GM_setValue('passengers', config.passengers); GM_setValue('autoSubmit', config.autoSubmit); GM_setValue('acceptWaitlist', config.acceptWaitlist); GM_setValue('enableNotification', config.enableNotification); GM_setValue('enableSound', config.enableSound); GM_setValue('timePeriods', config.timePeriods); GM_setValue('seatTypes', config.seatTypes); GM_setValue('passengerType', config.passengerType); log('💾 配置已保存!', 'OK'); log(' 车次过滤: ' + (config.trainCodes || '无'), 'INFO'); log(' 自动提交: ' + (config.autoSubmit ? '✅ ON' : '❌ OFF'), config.autoSubmit ? 'OK' : 'ERR'); log(' 座位类型: ' + config.seatTypes, 'INFO'); log(' 时间段: ' + config.timePeriods, 'INFO'); } function startMonitor() { config.trainCodes = document.getElementById('TC12306').value; config.autoSubmit = document.getElementById('AS12306').checked; config.isRunning = true; document.getElementById('GO12306').disabled = true; document.getElementById('SP12306').disabled = false; document.getElementById('STX12306').textContent = '状态:🔥 运行中...'; var tf = config.trainCodes ? ' | 车次: ' + config.trainCodes : ''; log('▶️ 开始监控余票!' + tf, 'OK'); log('⚡ 自动提交 = ' + (config.autoSubmit ? '✅ ON' : '❌ OFF'), config.autoSubmit ? 'OK' : 'ERR'); doCheck(); } function stopMonitor() { config.isRunning = false; if (timer) { clearTimeout(timer); timer = null; } document.getElementById('GO12306').disabled = false; document.getElementById('SP12306').disabled = true; document.getElementById('STX12306').textContent = '状态:已停止'; log('⏹ 已停止', 'ERR'); } function doCheck() { if (!config.isRunning) return; var table = document.getElementById('queryLeftTable'); if (!table) { log('❌ 未找到查询表格', 'ERR'); scheduleNext(); return; } var rows = table.querySelectorAll('tbody tr'); var validRows = []; for (var i = 0; i < rows.length; i++) { var tn = rows[i].querySelector('.train .number, .number, td:first-child a'); if (tn && tn.textContent.trim()) validRows.push(rows[i]); } // 车次过滤 var allowed = []; if (config.trainCodes && config.trainCodes.trim()) { var parts = config.trainCodes.split(','); for (var p = 0; p < parts.length; p++) { var t = parts[p].trim().toUpperCase(); if (t) allowed.push(t); } } var foundTicket = false; var firstRow = null; var skipCount = 0; for (var r = 0; r < validRows.length; r++) { var row = validRows[r]; var trainEl = row.querySelector('.train .number, .number, td:first-child a'); var code = trainEl ? trainEl.textContent.trim().toUpperCase() : ''; // 过滤车次 if (allowed.length > 0 && allowed.indexOf(code) < 0) { skipCount++; continue; } // 检查有票 var cells = row.querySelectorAll('td'); for (var c = 0; c < cells.length; c++) { var txt = cells[c].textContent.trim(); if (txt === '\u6709' || (txt && !isNaN(txt) && parseInt(txt) > 0)) { var info = txt === '\u6709' ? '\u6709\u7968' : txt + '\u5F20'; log('\uD83C\uDFAB ' + code + ' \u6709\u7968! (' + info + ') [autoSubmit=' + (config.autoSubmit ? 'ON' : 'OFF') + ']', 'OK'); foundTicket = true; if (!firstRow) firstRow = row; break; } } } if (allowed.length > 0 && skipCount > 0) { log('[\u8FC7\u6EE4] \u8DF3\u8FC7 ' + skipCount + ' \u4E2A\u4E0D\u5339\u914D\u8F66\u6B21', 'INFO'); } // 自动点击预订 if (foundTicket && firstRow && config.autoSubmit) { var tEl = firstRow.querySelector('.train .number, .number, td:first-child a'); var tCode = tEl ? tEl.textContent.trim() : '?'; log('\uD83D\uDD25\uD83D\uDD25\uD83D\uDD25 [\u81EA\u52A8\u63D0\u4EA4] \u51C6\u5907\u70B9\u51FB: ' + tCode, 'OK'); // 找预订按钮 var bookBtn = null; var links = firstRow.querySelectorAll('a'); for (var l = 0; l < links.length; l++) { var text = links[l].textContent.trim(); if (text === '\u9884\u8BA2' || text === '\u8D2D\u4E70') { bookBtn = links[l]; break; } } if (!bookBtn) bookBtn = firstRow.querySelector('a.btn72'); if (!bookBtn) bookBtn = firstRow.querySelector('a[onclick*="submitOrderRequest"]'); if (!bookBtn) { var lastCell = firstRow.querySelector('td:last-child'); if (lastCell) bookBtn = lastCell.querySelector('a'); } if (bookBtn) { log('\uD83C\uDCAF \u627E\u5230\u6309\u94AE "' + bookBtn.textContent.trim() + '" \u2192 \u70B9\u51FB!', 'OK'); sessionStorage.setItem('autoSubmitOrder', 'true'); try { bookBtn.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window })); } catch(e) { bookBtn.click(); } stopMonitor(); return; } else { log('\u274C \u672A\u627E\u5230\u9884\u8BA2\u6309\u94AE! \u8BF7\u624B\u52A8\u70B9\u51FB', 'ERR'); } } else if (foundTicket && !config.autoSubmit) { log('\u26A0\uFE0F \u53D1\u73B0\u6709\u7968\u4F46\u81EA\u52A8\u63D0\u4EA4=[OFF]! \u8BF7\u52FE\u9009"\u81EA\u52A8\u63D0\u4EA4\u8BA2\u5355"', 'ERR'); } if (!foundTicket) { if (allowed.length > 0) log('\u7B26\u5408\u6761\u4EF6\u7684\u8F66\u6B21\u6682\u65E0\u4F59\u7968', 'INFO'); else log('\u5171 ' + validRows.length + ' \u4E2A\u8F66\u6B21\uFF0C\u6682\u65E0\u4F59\u7968', 'INFO'); } scheduleNext(); } function scheduleNext() { if (config.isRunning) { timer = setTimeout(doCheck, config.queryInterval); } } function log(msg, type) { type = type || 'INFO'; var area = document.getElementById('LOG12306'); if (!area) return; var line = document.createElement('div'); line.className = 'LOG_LINE LOG_' + type; line.textContent = '[' + new Date().toLocaleTimeString() + '] ' + msg; area.insertBefore(line, area.firstChild); while (area.children.length > 80) area.removeChild(area.lastChild); area.scrollTop = 0; } // 订单页自动处理 function autoOrder() { log('\u8FDB\u5165\u8BA2\u5355\u786E\u8BA4\u9875\uFF0C\u5F00\u59CB\u81EA\u52A8\u5904\u7406...', 'INFO'); setTimeout(function() { selectSeat(); setTimeout(function() { selectPassenger(3); }, 1500); }, 3000); } function selectSeat() { var selects = document.querySelectorAll('select[id*="seat"], select[id*="Seat"], select[name*="seat"]'); if (!selects.length) { log('\u672A\u627E\u5230\u5EA7\u4F4D\u4E0B\u62C9\u6846', 'INFO'); return; } var map = [ {codes: ['9'], kw: ['\u5546\u52A1\u5EA7', '\u7279\u7B49\u5EA7'], name: '\u5546\u52A1\u5EA7'}, {codes: ['P'], kw: ['\u4F18\u9009\u4E00\u7B49\u5EA7', '\u4F18\u9009'], name: '\u4F18\u9009\u4E00\u7B49\u5EA7'}, {codes: ['M'], kw: ['\u4E00\u7B49\u5EA7'], name: '\u4E00\u7B49\u5EA7'}, {codes: ['O'], kw: ['\u4E8C\u7B49\u5EA7', '\u4E8C\u7B49\u5305\u5EA7'], name: '\u4E8C\u7B49\u5EA7'}, {codes: ['6'], kw: ['\u9AD8\u7EA7\u8F6F\u5367'], name: '\u9AD8\u7EA7\u8F6F\u5367'}, {codes: ['F', '4'], kw: ['\u8F6F\u5367', '\u52A8\u5367\u4E00\u7B49\u5367'], name: '\u8F6F\u5367'}, {codes: ['3'], kw: ['\u786C\u5367', '\u52A8\u5367\u4E8C\u7B49\u5367'], name: '\u786C\u5367'}, {codes: ['1'], kw: ['\u786C\u5EA7'], name: '\u786C\u5EA7'}, {codes: ['WZ'], kw: ['\u65E0\u5EA7'], name: '\u65E0\u5EA7'} ]; var count = 0; var seatTypes = config.seatTypes ? config.seatTypes.split(',') : ['O', 'WZ']; for (var s = 0; s < selects.length; s++) { var opts = selects[s].querySelectorAll('option'); for (var o = 0; o < opts.length; o++) { var t = opts[o].textContent.trim(); var v = opts[o].value; for (var m = 0; m < map.length; m++) { var item = map[m]; var codeMatch = false; for (var c = 0; c < item.codes.length; c++) { if (seatTypes.indexOf(item.codes[c]) >= 0) { codeMatch = true; break; } } if (codeMatch) { for (var k = 0; k < item.kw.length; k++) { if (t.indexOf(item.kw[k]) >= 0 || v === item.kw[k]) { opts[o].selected = true; selects[s].value = v; selects[s].dispatchEvent(new Event('change', { bubbles: true })); log('\u5DF2\u9009\u5EA7\u4F4D: ' + item.name, 'OK'); count++; break; } } } } } } if (count) log('\u5171\u9009\u4E86 ' + count + ' \u79CD\u5EA7\u4F4D', 'OK'); else log('\u672A\u80FD\u81EA\u52A8\u9009\u5EA7\uFF0C\u8BF7\u624B\u52A8\u9009\u62E9', 'INFO'); } function selectPassenger(maxRetry, retry) { retry = retry || 0; log('\u5C1D\u8BD5\u9009\u62E9\u4E58\u8F66\u4EBA (' + (retry + 1) + '/' + maxRetry + ')', 'INFO'); // 尝试选择 if (!config.passengers || !config.passengers.trim()) { var firstCb = document.querySelector('#normal_passenger_id input[type="checkbox"], input.check[id^="normalPassenger_"]'); if (firstCb && !firstCb.checked) { firstCb.click(); log('\u5DF2\u9009\u7B2C\u4E00\u4E2A\u4E58\u8F66\u4EBA', 'OK'); } } else { var names = config.passengers.split(',').map(function(n) { return n.trim(); }).filter(function(n) { return n; }); var items = document.querySelectorAll('#normal_passenger_id li, #normalPassenger_list li, .passenger-list li'); for (var n = 0; n < names.length; n++) { for (var i = 0; i < items.length; i++) { var label = items[i].querySelector('label, .name, .passenger-name') || items[i]; if (label.textContent.includes(names[n])) { var cb = items[i].querySelector('input[type="checkbox"]'); if (cb && !cb.checked) { cb.click(); log('\u5DF2\u9009: ' + names[n], 'OK'); } } } } } setTimeout(function() { var checked = document.querySelectorAll('#normal_passenger_id input[type="checkbox"]:checked, input.check[type="checkbox"]:checked'); if (checked.length === 0 && retry < maxRetry - 1) { log('\u672A\u9009\u4E2D\u4E58\u8F66\u4EBA\uFF0C1\u79D2\u540E\u91CD\u8BD5...', 'ERR'); setTimeout(function() { selectPassenger(maxRetry, retry + 1); }, 1000); } else if (checked.length > 0) { log('\u2713 \u5DF2\u9009\u4E2D ' + checked.length + ' \u4F4D\u4E58\u8F66\u4EBA', 'OK'); setTimeout(submitOrder, 2000); } else { log('\u591A\u6B21\u5C1D\u8BD5\u5931\u8D25\uFF0C\u8BF7\u624B\u52A8\u9009\u62E9', 'ERR'); } }, 500); } function submitOrder() { log('\u51C6\u5907\u63D0\u4EA4\u8BA2\u5355...', 'INFO'); var btn = document.getElementById('submitOrder_id') || document.querySelector('#submitOrder_id') || document.querySelector('.btn_submit') || document.querySelector('a[onclick*="submitOrderRequest"]'); if (btn) { if (btn.disabled || btn.classList.contains('disabled')) { log('[!] \u6309\u94AE\u88AB\u7981\u7528\uFF0C\u9700\u8981\u9A8C\u8BC1\u7801', 'ERR'); playSound(); return; } log('>>> 3\u79D2\u540E\u81EA\u52A8\u63D0\u4EA4...', 'OK'); setTimeout(function() { btn.click(); playSound(); log('\u2713 \u8BA2\u5355\u5DF2\u63D0\u4EA4\uFF01\u8BF7\u572830\u5206\u949F\u5185\u5B8C\u6210\u652F\u4ED8', 'OK'); }, 3000); } else { log('\u672A\u627E\u5230\u63D0\u4EA4\u6309\u94AE', 'ERR'); } } function playSound() { if (!config.enableSound) return; try { var ac = new (window.AudioContext || window.webkitAudioContext)(); var o = ac.createOscillator(), g = ac.createGain(); o.connect(g); g.connect(ac.destination); o.frequency.value = 800; o.type = 'sine'; g.gain.setValueAtTime(0.3, ac.currentTime); g.gain.exponentialRampToValueAtTime(0.01, ac.currentTime + 0.5); o.start(ac.currentTime); o.stop(ac.currentTime + 0.5); } catch(e) {} } function init() { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initApp); } else { initApp(); } } function initApp() { if (location.href.indexOf('leftTicket/init') !== -1) { createPanel(); } else if (location.href.indexOf('confirmPassenger') !== -1) { // 订单确认页 var div = document.createElement('div'); div.id = 'TH12306'; div.innerHTML = '

🎫 订单确认页

状态:处理中...
'; document.body.appendChild(div); if (sessionStorage.getItem('autoSubmitOrder') === 'true') { sessionStorage.removeItem('autoSubmitOrder'); log('🔥 自动提交模式已启用!', 'OK'); autoOrder(); } else { log('手动模式,请自行操作', 'INFO'); } } } init(); })();