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