Greasy Fork is available in English.
支持所有主流浏览器的携程航班信息提取器,一键复制航班信息
// ==UserScript==
// @name 携程航班信息提取器
// @name:en Ctrip Flight Info Extractor
// @namespace http://greasyfork.icu/users/[your-username]
// @version 1.2
// @description 支持所有主流浏览器的携程航班信息提取器,一键复制航班信息
// @description:en Extract and copy flight information from Ctrip with one click
// @author Senpou
// @license MIT
// @match https://flights.ctrip.com/online/list/*
// @match http://flights.ctrip.com/online/list/*
// @match https://m.ctrip.com/html5/flight/swift/domestic/*
// @grant GM_addStyle
// @grant GM_setClipboard
// @grant GM_xmlhttpRequest
// @run-at document-end
// ==/UserScript==
(function() {
'use strict';
// 添加样式
GM_addStyle(`
.flight-extractor-btn {
position: fixed;
top: 50%;
right: 80px;
transform: translateY(-50%);
z-index: 9999;
padding: 10px 20px;
background-color: #2681ff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
box-shadow: 0 2px 6px rgba(0,0,0,0.1);
}
.flight-extractor-btn:hover {
background-color: #1666d4;
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
.copy-flight-btn {
margin: 5px 0;
padding: 4px 6px;
background-color: #2681ff;
color: white;
border: none;
border-radius: 2px;
cursor: pointer;
font-size: 12px;
width: auto;
min-width: 40px;
height: 24px;
line-height: 16px;
display: inline-block;
}
.copy-flight-btn:hover {
background-color: #1666d4;
}
`);
// 跨浏览器复制函数
function copyToClipboard(text) {
// 方法1: 使用 GM_setClipboard (Tampermonkey API)
if (typeof GM_setClipboard !== 'undefined') {
try {
GM_setClipboard(text);
return true;
} catch (error) {
console.error('GM_setClipboard 失败:', error);
}
}
// 方法2: 使用 navigator.clipboard API
if (navigator.clipboard && window.isSecureContext) {
try {
navigator.clipboard.writeText(text);
return true;
} catch (error) {
console.error('Clipboard API 失败:', error);
}
}
// 方法3: 传统的 execCommand 方法
try {
const textarea = document.createElement('textarea');
textarea.value = text;
// 确保在所有浏览器中都不可见
textarea.style.cssText = 'position:fixed;pointer-events:none;z-index:-9999;opacity:0;';
document.body.appendChild(textarea);
// 适配移动设备
if (navigator.userAgent.match(/ipad|iphone/i)) {
textarea.contentEditable = true;
textarea.readOnly = false;
const range = document.createRange();
range.selectNodeContents(textarea);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
textarea.setSelectionRange(0, 999999);
} else {
textarea.select();
}
const successful = document.execCommand('copy');
document.body.removeChild(textarea);
return successful;
} catch (error) {
console.error('execCommand 失败:', error);
return false;
}
}
// 提取航班信息的主函数
async function extractFlightInfo() {
try {
const flightCards = document.querySelectorAll('.flight-item');
flightCards.forEach(card => {
addCopyButton(card);
});
} catch (error) {
console.error('提取航班信息时出错:', error);
}
}
// 添加复制按钮到单个航班卡片
function addCopyButton(card) {
if (safeQuerySelector(card, '.copy-flight-btn')) {
return;
}
try {
const flightNoText = safeQuerySelector(card, '.plane-No')?.textContent.trim();
const flightNo = flightNoText?.match(/^[A-Z0-9]+/)?.[0];
const departTime = safeQuerySelector(card, '.depart-box .time')?.textContent.trim();
const arriveTime = safeQuerySelector(card, '.arrive-box .time')?.textContent.trim()
.replace(/\s*\+\d+天\s*/, '');
const departAirport = safeQuerySelector(card, '.depart-box .airport')?.textContent.trim();
const arriveAirport = safeQuerySelector(card, '.arrive-box .airport')?.textContent.trim();
if (flightNo && departAirport && arriveAirport) {
const info = `${flightNo} ${departTime}-${arriveTime} ${departAirport}-${arriveAirport}`;
const priceArea = safeQuerySelector(card, '.flight-price');
if (priceArea) {
const copyBtn = document.createElement('button');
copyBtn.className = 'copy-flight-btn';
copyBtn.textContent = '复制信息';
// 使用 touchend 事件支持移动设备
const handleCopy = (e) => {
e.preventDefault();
e.stopPropagation();
if (copyToClipboard(info)) {
copyBtn.textContent = '已复制';
setTimeout(() => {
copyBtn.textContent = '复制信息';
}, 1000);
} else {
copyBtn.textContent = '复制失败';
setTimeout(() => {
copyBtn.textContent = '复制信息';
}, 1000);
}
};
// 同时支持点击和触摸
copyBtn.addEventListener('click', handleCopy);
copyBtn.addEventListener('touchend', handleCopy);
priceArea.insertAdjacentElement('beforebegin', copyBtn);
}
}
} catch (error) {
console.error('处理航班卡片时出错:', error);
}
}
// 监听页面变化
function observePageChanges() {
const observer = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
mutation.addedNodes.forEach(node => {
if (node.nodeType === 1) { // 元素节点
if (node.classList?.contains('flight-item')) {
addCopyButton(node);
}
// 检查子元素
const flightCards = node.querySelectorAll?.('.flight-item');
if (flightCards) {
flightCards.forEach(card => addCopyButton(card));
}
}
});
});
});
observer.observe(document.body, {
childList: true,
subtree: true
});
}
// 处理滚动事件
function handleScroll() {
extractFlightInfo();
}
// 添加兼容性检查和降级处理
function checkBrowserCompatibility() {
// 检查可选链操作符
if (typeof window.MutationObserver === 'undefined') {
console.warn('当前浏览器不支持 MutationObserver,将使用轮询方式');
// 使用 setInterval 作为降级方案
setInterval(extractFlightInfo, 2000);
return false;
}
return true;
}
// 修改 initialize 函数
function initialize() {
// 添加兼容性检查
const isModernBrowser = checkBrowserCompatibility();
// 检查并记录可用的复制方法
console.log('复制功能支持情况:', {
'GM_setClipboard': typeof GM_setClipboard !== 'undefined',
'Clipboard API': !!(navigator.clipboard && window.isSecureContext),
'execCommand': typeof document.execCommand === 'function'
});
// 初始处理已有的航班卡片
extractFlightInfo();
// 根据浏览器支持情况选择监听方式
if (isModernBrowser) {
// 监听页面变化
observePageChanges();
// 添加滚动监听(使用 passive 选项提高性能)
window.addEventListener('scroll', debounce(handleScroll, 200), { passive: true });
}
}
// 防抖函数
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// 添加安全的选择器查询
function safeQuerySelector(element, selector) {
try {
return element.querySelector(selector);
} catch (error) {
console.error('选择器查询失败:', error);
return null;
}
}
// 确保在 DOM 准备好后初始化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initialize);
} else {
initialize();
}
})();