// ==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();
})();