12306火车查询脚本

尝试征服世界!

// ==UserScript==
// @name         12306火车查询脚本
// @namespace    http://tampermonkey.net/
// @version      2025-09-11
// @description  尝试征服世界!
// @author       Dean
// @match        https://kyfw.12306.cn/otn/leftTicket/init*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=tampermonkey.net
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
	"use strict";

	// 定义需要查询的火车列表
	var train_list = ["K179", "K180"];
	var train_date = "2025-09-26";

	// 默认查询间隔时长为10分钟
	var DEFAULT_INTERVAL = 300000;

	// 刷新延迟时间为3秒
	var REFRESH_DELAY = 3000;
	// 内部属性
	var intervalId = null;
	var logIndex = 0;

	// 检查火车信息
	var checkTrain = function (train_list) {
		var t_list = document.getElementById("t-list");
		var rows = t_list.getElementsByTagName("tr");
		log("查询到" + rows.length + "辆车次,尝试获取目标车次状态...");
		for (var i = 0; i < rows.length; i++) {
			var cells = rows[i].cells;
			var letterText = "";
			var emit = false;
			var booking = false;
			var stationFromTo = "";
			for (var j = 0; j < cells.length; j++) {
				if (j === 0) {
					const trainResult = checkTrainNumber(cells[j], train_list);
					emit = trainResult.checkTrainStatus;
					if (emit) {
						rows[i].className = "";
						rows[i].style.backgroundColor = "lightgreen";
					}
					letterText = trainResult.letterText;
					stationFromTo = trainResult.stationFromTo;
					letterText = `${stationFromTo}  ${letterText}`;
				} else if (j == 1 && emit) {
				} else if (j === 12 && emit) {
					const trainStatus = checkTrainStatus(cells[j], letterText);
					booking = trainStatus.booking;
					letterText += trainStatus.letterText;
					// break trainLoop
				}
			}

			if (letterText && emit) {
				log(letterText + "--->预订状态:" + booking);
				if (booking) {
					// if (bookingBtn) {
					// 开始预定
					// log(`开始预定 ---->${letterText}`)
					// log(bookingBtn)
					// bookingBtn.getElementsByTagName('a')[0].click()
					// setTimeout(() => {
					// checkUser()
					// }, 1000);
					// } else {
					sendMessage("12306火车查询", letterText);
					sendStrongMessage(letterText);
					log("已查询到指定车次,定时逻辑关闭。如需重新开启,请刷新页面...");
					clearInterval(intervalId);
					// }
				}
			}
		}
	};

	var checkUser = function () {
		var loginModal = document.getElementById("login");
		if (loginModal) {
			// 未登录,输入账号密码
			// var J_userName = document.getElementById('J-userName')
			// var J_password = document.getElementById('J-password')
			// if (J_userName && J_password) {
			// 	J_userName.value = username
			// 	J_password.value = password
			// 	log(`密码输入完成...✅✅✅`)
			// 	document.getElementById('J-login').click()
			// }
		}
	};

	// 检查火车车次是否在查询列表中
	var checkTrainNumber = function (cell, train_list) {
		var numberTag = cell.getElementsByClassName("number");
		var number = numberTag[0]?.innerText;
		if (number) {
			if (train_list.includes(number)) {
				var cdzTags = cell.getElementsByClassName("cdz");
				var stationFromTo = "";
				if (cdzTags) {
					var from = cdzTags[0].getElementsByTagName("strong")[0];
					var to = cdzTags[0].getElementsByTagName("strong")[1];
					stationFromTo = `${from.innerHTML} -> ${to.innerHTML}`;
				}
				return { letterText: number, checkTrainStatus: true, stationFromTo };
			} else {
				return { letterText: "未查询到车次", checkTrainStatus: false };
			}
		} else {
			return {};
		}
	};

	// 检查火车状态并发送消息
	var checkTrainStatus = function (cell) {
		var a_link = cell.getElementsByTagName("a");
		if (a_link && a_link.length > 0 && a_link[0].tagName === "A") {
			const letterText = "[" + cell.innerText + "]\n可以预定了,赶快进入12306预定吧!!!";
			return { letterText, booking: true };
		} else {
			const letterText = "[" + cell.innerHTML + "]";
			return { letterText, booking: false };
		}
	};

	var shotToast = function (message) {
		var toastContainer = document.getElementById("message-container");
		if (toastContainer) {
			toastContainer.updateContentAndScrollToTop(
				`<div>${message}</div><br/>` + toastContainer.innerHTML
			);
		}
	};
	var log = function (log, showData = false) {
		console.log(new Date().toTimeString() + ": ", log);
		shotToast(showData ? new Date().toTimeString() + ": " + log : log);
	};

	var sendStrongMessage = function (message) {
		setInterval(() => sendMessage("12306强提醒", message), 10000);
	};

	// 发送消息
	var sendMessage = function (train, letterText) {
		log(letterText);
		// 检查浏览器是否支持Notification API
		if ("Notification" in window) {
			// 请求用户权限
			Notification.requestPermission().then((permission) => {
				if (permission === "granted") {
					// 如果用户授权,则创建通知
					const notification = new Notification(train, {
						body: letterText,
						icon: "https://www.google.com/s2/favicons?sz=64&domain=tampermonkey.net",
						sound: "system",
					});

					// 可以添加事件监听器来处理点击事件等
					notification.onclick = function (event) {
						window.focus();
					};
				}
			});
		} else {
			alert("你的浏览器不支持通知功能");
		}
	};

	var createToastContainer = function () {
		var toastContainer = document.createElement("div");
		toastContainer.id = "toast-container";
		toastContainer.style.position = "fixed";
		toastContainer.style.width = "500px";
		toastContainer.style.top = "20px";
		toastContainer.style.right = "20px";
		toastContainer.style.zIndex = "9999";
		toastContainer.style.padding = "10px";
		toastContainer.style.boxShadow = "0 4px 8px rgba(0,0,0,0.1)";
		toastContainer.style.borderRadius = "5px";
		toastContainer.style.backgroundColor = "rgba(60,60,60,0.9)";
		toastContainer.style.color = "#fff";
		toastContainer.style.fontFamily = "Arial, sans-serif";
		toastContainer.style.fontSize = "14px";
		toastContainer.innerHTML = `<div><H1>12306火车票监控预警已启动<H1><br/></div>`;
		document.body.appendChild(toastContainer);
	};

	var createMessageContainer = function () {
		var toastContainer = document.createElement("div");
		toastContainer.id = "message-container";
		toastContainer.style.position = "fixed";
		toastContainer.style.width = "500px";
		toastContainer.style.top = "160px";
		toastContainer.style.right = "20px";
		toastContainer.style.zIndex = "9999";
		toastContainer.style.padding = "10px";
		toastContainer.style.boxShadow = "0 4px 8px rgba(0,0,0,0.1)";
		toastContainer.style.borderRadius = "5px";
		toastContainer.style.backgroundColor = "rgba(60,60,60,0.9)";
		toastContainer.style.color = "#fff";
		toastContainer.style.fontFamily = "Arial, sans-serif";
		toastContainer.style.fontSize = "14px";
		toastContainer.style.overflowY = "auto"; // 添加这一行来实现垂直滚动
		toastContainer.style.maxHeight = "80%"; // 设置最大高度,超过这个高度就会显示滚动条
		document.body.appendChild(toastContainer);

		// 定义一个方法来更新innerHTML并滚动到顶部
		toastContainer.updateContentAndScrollToTop = function (htmlContent) {
			this.innerHTML = htmlContent;
			this.scrollTop = 0; // 滚动到顶部
		};
	};

	var startCheckSelDate = function () {
		document.title = "12306火车票监控预警已启动";
		document.querySelector("link[rel*='icon']").type = "image/svg+xml";
		document.querySelector("link[rel*='icon']").href =
			'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><circle cx="50" cy="50" r="40" stroke="green" stroke-width="4" fill="yellow" /></svg>';
		createToastContainer();
		createMessageContainer();

		var fromStationText = document.getElementById("fromStationText");
		var toStationText = document.getElementById("toStationText");
		var train_date_input = document.getElementById("train_date");
		if (train_date_input && fromStationText && toStationText) {
			train_date_input.value = train_date;
			var value = train_date_input.value;
			var toastContainer = document.getElementById('toast-container')
			if (toastContainer) {
				toastContainer.innerHTML = toastContainer.innerHTML + `监控车次: 由${fromStationText.value} 发往 ${toStationText.value}的${train_list}<br/> 监听日期: ${value}<br/><p>间隔时长: ${DEFAULT_INTERVAL / 60000}分钟</p>`
			}
		} else {
			log("监控车次的日期错误,请检查后重试...❌❌❌");
			sendMessage("12306监控车次异常", "监控车次的日期错误,请检查后重试...❌❌❌");
		}
	};

	const validCheckTrainStart = function () {
		var currentHour = new Date().getHours();
		if (currentHour < 24 && currentHour > 6) {
			return startCheckTrain;
		} else {
			log("12306封禁期,暂不做任何处理...✊✊✊");
			return () => { };
		}
	};

	// 创建一个更直观的监控信息面板
	function createMonitorPanel() {
		const panel = document.createElement("div");
		panel.id = "monitor-panel";
		panel.style.cssText = `
			position: fixed;
			top: 20px;
			right: 20px;
			width: 350px;
			padding: 20px;
			background-color: rgba(33, 33, 33, 0.95);
			color: #fff;
			border-radius: 12px;
			z-index: 9999;
			font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
			max-height: 85vh;
			overflow-y: auto;
			box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1), 0 1px 3px rgba(0, 0, 0, 0.08);
			backdrop-filter: blur(10px);
			border: 1px solid rgba(255, 255, 255, 0.1);
		`;
		panel.innerHTML = `
			<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
				<h2 style="margin: 0; color: #4CAF50; font-size: 1.5em;">12306 监控面板</h2>
				<div style="width: 10px; height: 10px; background: #4CAF50; border-radius: 50%; animation: pulse 2s infinite;"></div>
			</div>
			<style>
				@keyframes pulse {
					0% { transform: scale(1); opacity: 1; }
					50% { transform: scale(1.2); opacity: 0.5; }
					100% { transform: scale(1); opacity: 1; }
				}
				.monitor-section {
					background: rgba(255, 255, 255, 0.05);
					padding: 12px;
					border-radius: 8px;
					margin-bottom: 12px;
				}
				.monitor-section h3 {
					margin: 0 0 8px 0;
					color: #4CAF50;
					font-size: 1.1em;
				}
				.train-tag {
					display: inline-block;
					background-color: #4CAF50;
					color: white;
					padding: 4px 8px;
					margin: 2px;
					border-radius: 4px;
					font-size: 0.9em;
					transition: all 0.3s ease;
				}
				.train-tag:hover {
					transform: translateY(-2px);
					box-shadow: 0 2px 4px rgba(0,0,0,0.2);
				}
				.log-entry {
					padding: 8px;
					border-bottom: 1px solid rgba(255, 255, 255, 0.1);
					font-size: 0.9em;
					transition: background-color 0.3s ease;
				}
				.log-entry:hover {
					background-color: rgba(255, 255, 255, 0.05);
				}
				.timestamp {
					color: #888;
					font-size: 0.8em;
				}
			</style>
			<div id="monitor-info" class="monitor-section"></div>
			<div id="train-list" class="monitor-section"></div>
			<div id="next-refresh" class="monitor-section"></div>
			<div id="log-container" class="monitor-section" style="max-height: 300px; overflow-y: auto;"></div>
			<button id="reset-config" style="
				width: 100%;
				padding: 10px;
				background-color: #f44336;
				color: white;
				border: none;
				border-radius: 6px;
				cursor: pointer;
				font-weight: bold;
				transition: all 0.3s ease;
				margin-top: 10px;
			">重置配置</button>
		`;
		document.body.appendChild(panel);

		// 添加按钮悬停效果
		const resetButton = document.getElementById("reset-config");
		resetButton.addEventListener("mouseenter", () => {
			resetButton.style.backgroundColor = "#d32f2f";
		});
		resetButton.addEventListener("mouseleave", () => {
			resetButton.style.backgroundColor = "#f44336";
		});

		// 添加重置配置按钮的事件监听
		resetButton.addEventListener("click", resetConfig);
	}

	// 创建日期选择提醒
	function createDateReminder() {
		const reminder = document.createElement("div");
		reminder.id = "date-reminder";
		reminder.style.cssText = `
		position: fixed;
		top: 50%;
		left: 50%;
		transform: translate(-50%, -50%);
		background-color: rgba(0, 0, 0, 0.8);
		color: #fff;
		padding: 20px;
		border-radius: 10px;
		text-align: center;
		z-index: 10000;
	  `;
		reminder.innerHTML = `
		<h3>请选择查询日期</h3>
		<p>点击日期输入框选择您要查询的日期</p>
		<button id="close-reminder" style="
		  margin-top: 10px;
		  padding: 5px 10px;
		  background-color: #4CAF50;
		  color: white;
		  border: none;
		  border-radius: 5px;
		  cursor: pointer;
		">我知道了</button>
	  `;
		document.body.appendChild(reminder);

		document.getElementById("close-reminder").addEventListener("click", () => {
			reminder.style.display = "none";
		});
	}

	// 触发日期选择器并设置监听
	function triggerDatePicker() {
		const dateInput = document.getElementById("train_date");
		if (dateInput) {
			dateInput.focus();
			// 模拟点击事件以打开日期选择器
			const event = new MouseEvent('click', {
				view: window,
				bubbles: true,
				cancelable: true
			});
			dateInput.dispatchEvent(event);

			// 添加日期变更监听
			dateInput.addEventListener('change', function () {
				updateMonitorDate(this.value);
			});
		}
	}

	// 更新监控日期并刷新页面
	function updateMonitorDate(newDate) {
		train_date = newDate; // 更新全局变量

		// 更新localStorage中的日期
		localStorage.setItem("train_date", newDate);

		// 更新监控信息显示
		const fromStationText = document.getElementById("fromStationText");
		const toStationText = document.getElementById("toStationText");
		updateMonitorInfo(fromStationText.value, toStationText.value, newDate);

		// 立即执行一次查询
		log(`日期已更新为 ${newDate},正在刷新查询...`);
		validCheckTrainStart()(JSON.parse(localStorage.getItem("train_list")) || train_list);

		// 更新下次刷新时间
		updateNextRefreshTime();

		// 可选:如果你想重新加载整个页面,可以取消下面这行的注释
		// window.location.href = new URL(window.location.href).searchParams.set('date', newDate).toString();
	}

	// 更新监控信息
	function updateMonitorInfo(fromStation, toStation, date) {
		const infoDiv = document.getElementById("monitor-info");
		infoDiv.innerHTML = `
			<h3>监控信息</h3>
			<div style="display: grid; grid-template-columns: auto 1fr; gap: 8px;">
				<span style="color: #888;">出发站:</span>
				<span style="color: #fff;">${fromStation}</span>
				<span style="color: #888;">到达站:</span>
				<span style="color: #fff;">${toStation}</span>
				<span style="color: #888;">日期:</span>
				<span style="color: #fff;">${date}</span>
				<span style="color: #888;">刷新间隔:</span>
				<span style="color: #fff;">${DEFAULT_INTERVAL / 60000}分钟</span>
			</div>
		`;
	}

	// 更新列车列表
	function updateTrainList(trainList) {
		const listDiv = document.getElementById("train-list");
		listDiv.innerHTML = "<h3>监控车次</h3><div style='display: flex; flex-wrap: wrap; gap: 4px;'>";
		trainList.forEach(train => {
			listDiv.innerHTML += `<span class="train-tag">${train}</span>`;
		});
		listDiv.innerHTML += "</div>";
	}

	// 更新下次刷新时间
	function updateNextRefreshTime() {
		const nextRefreshDiv = document.getElementById("next-refresh");
		const nextRefreshTime = new Date(Date.now() + DEFAULT_INTERVAL);
		nextRefreshDiv.innerHTML = `
			<h3>下次刷新</h3>
			<div style="display: flex; align-items: center; gap: 8px;">
				<span style="color: #4CAF50;">${nextRefreshTime.toLocaleTimeString()}</span>
				<div style="flex-grow: 1; height: 2px; background: linear-gradient(to right, #4CAF50, transparent);"></div>
			</div>
		`;
	}

	// 优化日志显示
	function log(message, showTimestamp = true) {
		const logContainer = document.getElementById("log-container");
		if (!logContainer) {
			console.error("Log container not found");
			return;
		}

		const logEntry = document.createElement("div");
		logEntry.className = "log-entry";

		if (showTimestamp) {
			const timestamp = new Date().toLocaleTimeString();
			logEntry.innerHTML = `
				<span class="timestamp">[${timestamp}]</span>
				<span style="margin-left: 8px;">${message}</span>
			`;
		} else {
			logEntry.textContent = message;
		}

		// 添加新日志时的动画效果
		logEntry.style.opacity = "0";
		logEntry.style.transform = "translateY(-10px)";
		logContainer.insertBefore(logEntry, logContainer.firstChild);

		// 触发动画
		setTimeout(() => {
			logEntry.style.transition = "all 0.3s ease";
			logEntry.style.opacity = "1";
			logEntry.style.transform = "translateY(0)";
		}, 50);

		// 限制日志条目数量,保持最新的20条
		while (logContainer.children.length > 20) {
			logContainer.removeChild(logContainer.lastChild);
		}

		// 自动滚动到顶部
		logContainer.scrollTop = 0;

		console.log(message);
	}

	// 重置配置函数
	function resetConfig() {
		localStorage.removeItem("12306_first_visit");
		localStorage.removeItem("train def setup_web_channel_js(self");
		log("配置已重置,请刷新页面以应用更改。");
	}

	// 开始查询火车信息
	function startCheckTrain(trainList) {
		const queryTicket = document.getElementById("query_ticket");
		log("尝试刷新车次列表...");
		if (queryTicket) {
			queryTicket.click();
			log("刷新车次列表成功 ✅");
			setTimeout(() => {
				checkTrain(trainList);
			}, REFRESH_DELAY);
		} else {
			log("刷新车次列表失败,请检查后重试 ❌", true);
			sendMessage("12306刷新车次失败", "刷新车次列表失败,请检查后重试");
		}
		updateNextRefreshTime();
	}

	// 主函数
	function main(trainList) {
		createMonitorPanel();
		const fromStationText = document.getElementById("fromStationText");
		const toStationText = document.getElementById("toStationText");
		const trainDateInput = document.getElementById("train_date");

		if (fromStationText && toStationText && trainDateInput) {
			// 检查是否是首次访问或配置已重置
			if (!localStorage.getItem("12306_first_visit")) {
				createDateReminder();
				triggerDatePicker();
				localStorage.setItem("12306_first_visit", "true");

				// 保存初始配置
				localStorage.setItem("train_list", JSON.stringify(trainList));
				localStorage.setItem("train_date", trainDateInput.value);
			} else {
				// 从 localStorage 加载配置
				trainList = JSON.parse(localStorage.getItem("train_list")) || trainList;
				train_date = localStorage.getItem("train_date") || train_date;
				trainDateInput.value = train_date;
			}

			updateMonitorInfo(fromStationText.value, toStationText.value, trainDateInput.value);
			updateTrainList(trainList);
			log(`监控已启动,正在查询指定车次`, true);
			validCheckTrainStart()(trainList);
			intervalId = setInterval(() => {
				log(`定时刷新,重新查询车次`, true);
				validCheckTrainStart()(trainList);
			}, DEFAULT_INTERVAL);
		} else {
			log("获取车站信息失败,请检查页面是否正确加载", true);
		}
	}

	// 确保在页面加载完成后再执行脚本
	window.addEventListener('load', () => {
		main(train_list);
	});
})();