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