// ==UserScript==
// @name 运盟调度一键复制
// @namespace http://tampermonkey.net/
// @license MIT
// @version 2025-10-20-23
// @description 支持运盟系统内车签号和运单号查询复制,方便运盟调度制作每日进港车辆表。
// @author 圆通华北中心一期调度室
// @match https://diaodu.yonmen.com/*
// @grant GM_xmlhttpRequest
// @grant GM_setClipboard
// ==/UserScript==
(function() {
'use strict';
// 缓存对象
const cache = {
token: null,
tokenExpiry: 0,
transportInfo: new Map(),
detailInfo: new Map()
};
// DOM元素缓存
let uiElements = null;
// 提取车签号码的函数
function extractCarSignNumbers(inputText) {
// 车签号码的正则表达式:以AQ开头,后跟12位数字
const carSignPattern = /AQ\d{12}/g;
const matches = inputText.match(carSignPattern);
if (!matches) {
return [];
}
// 去重并保持原始顺序
const uniqueCarSigns = [];
const seen = new Set();
for (const match of matches) {
if (!seen.has(match)) {
seen.add(match);
uniqueCarSigns.push(match);
}
}
return uniqueCarSigns;
}
// 等待页面加载完成
function waitForElement(selector, timeout = 5000) {
return new Promise((resolve, reject) => {
const element = document.querySelector(selector);
if (element) {
resolve(element);
return;
}
const observer = new MutationObserver((mutations, obs) => {
const element = document.querySelector(selector);
if (element) {
obs.disconnect();
resolve(element);
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
setTimeout(() => {
observer.disconnect();
reject(new Error('Element not found within timeout'));
}, timeout);
});
}
// 创建UI界面
function createUI() {
const container = document.createElement('div');
container.style.cssText = `
position: fixed;
bottom: 20px;
left: 20px;
background: white;
border: 1px solid #007bff;
border-radius: 6px;
padding: 10px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
z-index: 10000;
font-family: Arial, sans-serif;
width: 200px;
font-size: 12px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
cursor: move;
user-select: none;
`;
container.innerHTML = `
`;
document.body.appendChild(container);
// 缓存UI元素
uiElements = {
container,
button: container.querySelector('#copyButton'),
clearButton: container.querySelector('#clearButton'),
input: container.querySelector('#queryNumberInput'),
queryTypeSelect: container.querySelector('#queryTypeSelect'),
toggleButton: container.querySelector('#toggleButton'),
contentArea: container.querySelector('#contentArea'),
titleBar: container.querySelector('#titleBar')
};
// 添加拖拽移动功能
let isDragging = false;
let dragOffset = { x: 0, y: 0 };
uiElements.titleBar.addEventListener('mousedown', function(e) {
// 如果点击的是切换按钮,不启动拖拽
if (e.target === uiElements.toggleButton) return;
isDragging = true;
const rect = uiElements.container.getBoundingClientRect();
dragOffset.x = e.clientX - rect.left;
dragOffset.y = e.clientY - rect.top;
// 暂时禁用过渡动画,避免拖拽时的延迟
uiElements.container.style.transition = 'none';
// 添加拖拽时的视觉反馈
uiElements.container.style.boxShadow = '0 4px 16px rgba(0,0,0,0.2)';
uiElements.container.style.transform = 'scale(1.02)';
e.preventDefault();
});
document.addEventListener('mousemove', function(e) {
if (!isDragging) return;
const newX = e.clientX - dragOffset.x;
const newY = e.clientY - dragOffset.y;
// 限制窗口不超出屏幕边界
const maxX = window.innerWidth - uiElements.container.offsetWidth;
const maxY = window.innerHeight - uiElements.container.offsetHeight;
const constrainedX = Math.max(0, Math.min(newX, maxX));
const constrainedY = Math.max(0, Math.min(newY, maxY));
uiElements.container.style.left = constrainedX + 'px';
uiElements.container.style.top = constrainedY + 'px';
uiElements.container.style.bottom = 'auto';
uiElements.container.style.right = 'auto';
});
document.addEventListener('mouseup', function() {
if (isDragging) {
isDragging = false;
// 恢复过渡动画和正常样式
uiElements.container.style.transition = 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)';
uiElements.container.style.boxShadow = '0 2px 8px rgba(0,0,0,0.1)';
uiElements.container.style.transform = 'scale(1)';
}
});
// 添加显示隐藏功能
let isCollapsed = false;
uiElements.toggleButton.addEventListener('click', function() {
isCollapsed = !isCollapsed;
if (isCollapsed) {
// 收起状态
uiElements.contentArea.style.height = '0';
uiElements.contentArea.style.opacity = '0';
uiElements.contentArea.style.overflow = 'hidden';
uiElements.toggleButton.textContent = '+';
uiElements.toggleButton.title = '展开';
uiElements.container.style.width = '160px';
} else {
// 展开状态
uiElements.contentArea.style.height = 'auto';
uiElements.contentArea.style.opacity = '1';
uiElements.contentArea.style.overflow = 'visible';
uiElements.toggleButton.textContent = '−';
uiElements.toggleButton.title = '收起';
uiElements.container.style.width = '200px';
}
});
// 添加切换按钮悬停效果
uiElements.toggleButton.addEventListener('mouseenter', () => {
uiElements.toggleButton.style.background = '#f8f9fa';
uiElements.toggleButton.style.color = '#0056b3';
});
uiElements.toggleButton.addEventListener('mouseleave', () => {
uiElements.toggleButton.style.background = 'none';
uiElements.toggleButton.style.color = '#007bff';
});
// 添加清空按钮点击事件监听
uiElements.clearButton.addEventListener('click', function() {
uiElements.input.value = '';
uiElements.input.focus();
showStatus('输入框已清空');
// 1.5秒后隐藏提示(由showStatus函数自动处理)
});
// 添加清空按钮悬停效果
uiElements.clearButton.addEventListener('mouseenter', () => {
uiElements.clearButton.style.background = '#5a6268';
});
uiElements.clearButton.addEventListener('mouseleave', () => {
uiElements.clearButton.style.background = '#6c757d';
});
// 添加查询类型变化事件监听
uiElements.queryTypeSelect.addEventListener('change', function() {
const queryType = this.value;
if (queryType === 'cq') {
uiElements.input.placeholder = '输入车签号码(支持单个或批量)\n批量格式示例:\nAQ771001106620\n2025-10-20 20:35:33\n上传成功\n5\nAQ701901051444\n...';
uiElements.input.style.height = '80px';
} else if (queryType === 'trans') {
uiElements.input.placeholder = '输入运单号码';
uiElements.input.style.height = '30px';
}
uiElements.input.value = ''; // 清空输入框
});
// 添加按钮悬停效果
uiElements.button.addEventListener('mouseenter', () => {
uiElements.button.style.background = '#0056b3';
});
uiElements.button.addEventListener('mouseleave', () => {
uiElements.button.style.background = '#007bff';
});
return container;
}
// 页面顶部提示函数
function showTopNotification(message, isError = false) {
// 移除已存在的提示
const existingNotification = document.getElementById('top-notification');
if (existingNotification) {
existingNotification.remove();
}
// 创建提示元素
const notification = document.createElement('div');
notification.id = 'top-notification';
notification.textContent = message;
// 设置样式
notification.style.cssText = `
position: fixed;
top: 0;
left: 50%;
transform: translateX(-50%);
z-index: 999999;
padding: 12px 24px;
border-radius: 0 0 8px 8px;
font-size: 14px;
font-weight: 500;
color: white;
background: ${isError ? 'linear-gradient(135deg, #ff6b6b, #ee5a52)' : 'linear-gradient(135deg, #4CAF50, #45a049)'};
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
opacity: 0;
transform: translateX(-50%) translateY(-100%);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
max-width: 400px;
text-align: center;
word-wrap: break-word;
`;
// 添加到页面
document.body.appendChild(notification);
// 动画显示
requestAnimationFrame(() => {
notification.style.opacity = '1';
notification.style.transform = 'translateX(-50%) translateY(0)';
});
// 3秒后渐渐消失
setTimeout(() => {
if (notification && notification.parentNode) {
notification.style.opacity = '0';
notification.style.transform = 'translateX(-50%) translateY(-100%)';
// 动画结束后移除元素
setTimeout(() => {
if (notification && notification.parentNode) {
notification.remove();
}
}, 300);
}
}, 3000);
}
// 保持原有的showStatus函数用于兼容性,但改为调用新的顶部提示
function showStatus(message, isError = false) {
showTopNotification(message, isError);
}
// 获取当前页面的token(带缓存)
function getToken() {
// 检查缓存
if (cache.token && Date.now() < cache.tokenExpiry) {
return cache.token;
}
let token = null;
// 尝试从localStorage获取token
token = localStorage.getItem('token') || sessionStorage.getItem('token');
if (token) {
token = token.replace(/['"]/g, '');
if (token.length === 32) {
cache.token = token;
cache.tokenExpiry = Date.now() + 300000; // 缓存5分钟
return token;
}
}
// 尝试从cookie获取token
const cookies = document.cookie.split(';');
for (let cookie of cookies) {
const [name, value] = cookie.trim().split('=');
if (name === 'token' && value && value.length === 32) {
cache.token = value;
cache.tokenExpiry = Date.now() + 300000;
return value;
}
}
// 尝试从页面的全局变量获取token
if (window.localStorage) {
for (let i = 0; i < window.localStorage.length; i++) {
const key = window.localStorage.key(i);
const value = window.localStorage.getItem(key);
if (key && key.toLowerCase().includes('token') && value && value.length === 32) {
token = value.replace(/['"]/g, '');
cache.token = token;
cache.tokenExpiry = Date.now() + 300000;
return token;
}
}
}
// 默认token
token = '96926dd287ca485aabff7e9300453918';
cache.token = token;
cache.tokenExpiry = Date.now() + 300000;
return token;
}
// 发送API请求获取运输信息(带缓存和超时控制,支持车签号和运单号查询)
function fetchTransportInfo(queryNumber, token, queryType = 'cq') {
// 检查缓存
const cacheKey = `${queryType}_${queryNumber}_${token}`;
if (cache.transportInfo.has(cacheKey)) {
return Promise.resolve(cache.transportInfo.get(cacheKey));
}
return new Promise((resolve, reject) => {
let url;
if (queryType === 'cq') {
// 车签号查询
url = `https://diaodu.yonmen.com/v1/transports/web?date_search_type=1&group_status=&trans_number=&cq_number=${queryNumber}&plate=&vehicle_type=&start_code=&start_name=&run_mode=&end_code=&end_name=&is_bind_driver=&safety_status=&page_index=1&page_size=10&token=${token}`;
} else if (queryType === 'trans') {
// 运单号查询
url = `https://diaodu.yonmen.com/v1/transports/web?date_search_type=1&group_status=&trans_number=${queryNumber}&cq_number=&plate=&vehicle_type=&start_code=&start_name=&run_mode=&end_code=&end_name=&is_bind_driver=&safety_status=&page_index=1&page_size=10&token=${token}`;
} else {
reject(new Error('不支持的查询类型'));
return;
}
// 设置10秒超时
const timeoutId = setTimeout(() => {
reject(new Error('请求超时'));
}, 10000);
GM_xmlhttpRequest({
method: 'GET',
url: url,
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
timeout: 10000,
onload: function(response) {
clearTimeout(timeoutId);
try {
const data = JSON.parse(response.responseText);
if (data.code === 1000 && data.data && data.data.trans && data.data.trans.length > 0) {
const result = data.data.trans[0];
// 缓存结果
cache.transportInfo.set(cacheKey, result);
resolve(result);
} else {
reject(new Error(`未找到${queryType === 'cq' ? '车签号' : '运单号'}为 ${queryNumber} 的运输信息`));
}
} catch (e) {
reject(new Error('解析响应数据失败'));
}
},
onerror: function() {
clearTimeout(timeoutId);
reject(new Error('请求失败'));
},
ontimeout: function() {
clearTimeout(timeoutId);
reject(new Error('请求超时'));
}
});
});
}
// 获取详情页面信息
function fetchDetailInfo(transNumber, token) {
// 检查缓存
const cacheKey = `${transNumber}_${token}`;
if (cache.detailInfo.has(cacheKey)) {
return Promise.resolve(cache.detailInfo.get(cacheKey));
}
return new Promise((resolve, reject) => {
const detailApiUrl = `https://diaodu.yonmen.com/v1/transports/detail?trans_number=${transNumber}&token=${token}`;
// 设置10秒超时
const timeoutId = setTimeout(() => {
// API超时后直接尝试iframe方式,但减少等待时间
fetchDetailInfoByIframe(transNumber, 8000).then(result => {
cache.detailInfo.set(cacheKey, result);
resolve(result);
}).catch(reject);
}, 10000);
GM_xmlhttpRequest({
method: 'GET',
url: detailApiUrl,
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
timeout: 10000,
onload: function(response) {
clearTimeout(timeoutId);
try {
const data = JSON.parse(response.responseText);
if (data.code === 1000 && data.data) {
let yjTime = '';
let sjTime = '';
const nodes = data.data.nodes || [];
for (let node of nodes) {
if (node.address && node.address.includes('河北省廊坊市永清县工业园区小良村一纬道')) {
yjTime = node.plan_time || node.expect_time || '';
sjTime = node.actual_time || node.reach_time || '';
break;
}
}
if (!yjTime || !sjTime) {
const detail = data.data;
yjTime = yjTime || detail.plan_reach_time || detail.expect_reach_time || '';
sjTime = sjTime || detail.actual_reach_time || detail.reach_time || '';
}
const result = { yjTime, sjTime };
cache.detailInfo.set(cacheKey, result);
resolve(result);
} else {
// API返回错误,尝试iframe
fetchDetailInfoByIframe(transNumber, 8000).then(result => {
cache.detailInfo.set(cacheKey, result);
resolve(result);
}).catch(reject);
}
} catch (e) {
// API解析失败,尝试iframe
fetchDetailInfoByIframe(transNumber, 8000).then(result => {
cache.detailInfo.set(cacheKey, result);
resolve(result);
}).catch(reject);
}
},
onerror: function() {
clearTimeout(timeoutId);
fetchDetailInfoByIframe(transNumber, 8000).then(result => {
cache.detailInfo.set(cacheKey, result);
resolve(result);
}).catch(reject);
},
ontimeout: function() {
clearTimeout(timeoutId);
fetchDetailInfoByIframe(transNumber, 8000).then(result => {
cache.detailInfo.set(cacheKey, result);
resolve(result);
}).catch(reject);
}
});
});
}
// 通过iframe获取详情页面信息(减少等待时间)
function fetchDetailInfoByIframe(transNumber, maxTimeout = 8000) {
return new Promise((resolve, reject) => {
const detailUrl = `https://diaodu.yonmen.com/#/app/orderDetail?trans_number=${transNumber}`;
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = detailUrl;
let timeoutId = setTimeout(() => {
if (iframe.parentNode) {
document.body.removeChild(iframe);
}
reject(new Error('获取详情信息超时'));
}, maxTimeout);
let checkCount = 0;
const maxChecks = 8; // 最多检查8次
const checkContent = () => {
checkCount++;
try {
const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
const elements = iframeDoc.querySelectorAll('.formItem-text');
if (elements.length > 0) {
let yjTime = '';
let sjTime = '';
let foundTarget = false;
for (let element of elements) {
const text = element.textContent || element.innerText;
if (text.includes('河北省廊坊市永清县工业园区小良村一纬道')) {
foundTarget = true;
const parent = element.closest('.k-row');
if (parent) {
const timeElements = parent.querySelectorAll('.formItem-text');
for (let timeEl of timeElements) {
const timeText = timeEl.textContent || timeEl.innerText;
if (timeText.includes('预计到车时间:')) {
yjTime = timeText.replace('预计到车时间:', '').trim();
}
if (timeText.includes('实际到车时间:')) {
sjTime = timeText.replace('实际到车时间:', '').trim();
}
}
}
break;
}
}
if (foundTarget && (yjTime || sjTime)) {
clearTimeout(timeoutId);
if (iframe.parentNode) {
document.body.removeChild(iframe);
}
resolve({ yjTime, sjTime });
return;
}
}
// 如果还没找到且未超过最大检查次数,继续检查
if (checkCount < maxChecks) {
setTimeout(checkContent, 1000);
} else {
clearTimeout(timeoutId);
if (iframe.parentNode) {
document.body.removeChild(iframe);
}
reject(new Error('未找到目标地址或时间信息'));
}
} catch (e) {
if (checkCount < maxChecks) {
setTimeout(checkContent, 1000);
} else {
clearTimeout(timeoutId);
if (iframe.parentNode) {
document.body.removeChild(iframe);
}
reject(new Error('获取详情页面信息失败: ' + e.message));
}
}
};
iframe.onload = function() {
// 页面加载完成后等待1秒再开始检查
setTimeout(checkContent, 1000);
};
iframe.onerror = function() {
clearTimeout(timeoutId);
if (iframe.parentNode) {
document.body.removeChild(iframe);
}
reject(new Error('加载详情页面失败'));
};
document.body.appendChild(iframe);
});
}
// 初始化
function init() {
waitForElement('body').then(() => {
createUI();
uiElements.button.addEventListener('click', async function() {
const inputValue = uiElements.input.value.trim();
const queryType = uiElements.queryTypeSelect.value;
if (!inputValue) {
showStatus(`请输入${queryType === 'cq' ? '车签号码' : '运单号码'}`, true);
return;
}
// 禁用按钮防止重复点击
uiElements.button.disabled = true;
uiElements.button.style.opacity = '0.6';
uiElements.button.textContent = '查询中...';
try {
const token = getToken();
console.log('当前获取的token值:', token);
if (queryType === 'cq') {
// 车签号查询模式 - 自动检测单个或批量
const carSignNumbers = extractCarSignNumbers(inputValue);
if (carSignNumbers.length === 0) {
// 没有找到车签号码,可能是单个车签号码输入
const cleanedInput = inputValue.replace(/[^\w\-]/g, '');
if (cleanedInput.match(/^AQ\d{12}$/)) {
// 单个车签号码
showStatus('正在获取运输信息...');
const transportInfo = await fetchTransportInfo(cleanedInput, token, 'cq');
const { trans_number, line_name, create_date, vehicle_type, cq_number } = transportInfo;
showStatus('正在获取详情信息...');
const detailInfo = await fetchDetailInfo(trans_number, token);
const { yjTime, sjTime } = detailInfo;
const cqNumberForClipboard = cq_number || cleanedInput;
const result = `${cqNumberForClipboard}\t${create_date}\t${line_name}\t${vehicle_type}\t${yjTime}\t${sjTime}`;
GM_setClipboard(result);
showStatus('复制成功!信息已保存到剪贴板');
} else {
showStatus('未找到有效的车签号码', true);
return;
}
} else if (carSignNumbers.length === 1) {
// 单个车签号码
const carSign = carSignNumbers[0];
showStatus('正在获取运输信息...');
const transportInfo = await fetchTransportInfo(carSign, token, 'cq');
const { trans_number, line_name, create_date, vehicle_type, cq_number } = transportInfo;
showStatus('正在获取详情信息...');
const detailInfo = await fetchDetailInfo(trans_number, token);
const { yjTime, sjTime } = detailInfo;
const cqNumberForClipboard = cq_number || carSign;
const result = `${cqNumberForClipboard}\t${create_date}\t${line_name}\t${vehicle_type}\t${yjTime}\t${sjTime}`;
GM_setClipboard(result);
showStatus('复制成功!信息已保存到剪贴板');
} else {
// 批量车签号码
showStatus(`找到 ${carSignNumbers.length} 个车签号码,正在查询...`);
const results = [];
let successCount = 0;
let failCount = 0;
// 按倒序处理(最后输入的先处理)
const reversedCarSigns = [...carSignNumbers].reverse();
for (let i = 0; i < reversedCarSigns.length; i++) {
const carSign = reversedCarSigns[i];
showStatus(`正在查询第 ${i + 1}/${reversedCarSigns.length} 个车签: ${carSign}`);
try {
const transportInfo = await fetchTransportInfo(carSign, token, 'cq');
const { trans_number, line_name, create_date, vehicle_type, cq_number } = transportInfo;
const detailInfo = await fetchDetailInfo(trans_number, token);
const { yjTime, sjTime } = detailInfo;
const cqNumberForClipboard = cq_number || carSign;
const result = `${cqNumberForClipboard}\t${create_date}\t${line_name}\t${vehicle_type}\t${yjTime}\t${sjTime}`;
results.push(result);
successCount++;
} catch (error) {
console.error(`查询车签 ${carSign} 失败:`, error);
// 即使某个车签查询失败,也添加一个错误行
results.push(`${carSign}\t查询失败\t${error.message}\t\t\t`);
failCount++;
}
}
// 将所有结果合并为多行格式
const finalResult = results.join('\n');
GM_setClipboard(finalResult);
showStatus(`批量查询完成!成功: ${successCount}, 失败: ${failCount}。结果已复制到剪贴板`);
}
} else if (queryType === 'trans') {
// 运单号查询模式
const queryNumber = inputValue.replace(/[^\w\-]/g, ''); // 去除空格和特殊符号
showStatus('正在获取数据...');
showStatus('正在获取运输信息...');
const transportInfo = await fetchTransportInfo(queryNumber, token, queryType);
console.log('获取到的运输信息:', transportInfo);
const { trans_number, line_name, create_date, vehicle_type, cq_number } = transportInfo;
showStatus('正在获取详情信息...');
const detailInfo = await fetchDetailInfo(trans_number, token);
const { yjTime, sjTime } = detailInfo;
const cqNumberForClipboard = cq_number || queryNumber;
const result = `${cqNumberForClipboard}\t${create_date}\t${line_name}\t${vehicle_type}\t${yjTime}\t${sjTime}`;
GM_setClipboard(result);
showStatus('复制成功!信息已保存到剪贴板');
}
} catch (error) {
console.error('操作失败:', error);
showStatus(`错误: ${error.message}`, true);
} finally {
// 恢复按钮状态
if (uiElements.button) {
uiElements.button.disabled = false;
uiElements.button.style.opacity = '1';
uiElements.button.textContent = '一键复制';
}
}
});
uiElements.input.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
uiElements.button.click();
}
});
console.log('永门调度一键复制工具已加载(性能优化版)');
}).catch(error => {
console.error('初始化失败:', error);
});
}
// 页面加载完成后初始化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();