// ==UserScript== // @name 货查查助手-千易 WMS // @namespace https://hcc321.com/ // @version 1.0.1 // @description 千易 WMS 验货页面商品扫描抓单上传(API 拦截方案),保留登录、工位、监控与上传能力 // @author HCCvision // @match https://*.aiyacang.com/* // @match http://*.aiyacang.com/* // @match https://gwmsth.best-inc.com/* // @match http://gwmsth.best-inc.com/* // @grant GM_getValue // @grant GM_setValue // @grant GM_deleteValue // @grant GM_addStyle // @grant GM_xmlhttpRequest // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @connect hcc321.com // @inject-into content // @run-at document-start // ==/UserScript== /** * Pack Monitor Script * 千易 WMS 验货页面商品扫描抓单上传 - API 拦截方案 * 通过拦截 fetch/XHR 获取 /service/ob/pack/prepare 与 /service/ob/pack/confirm 接口数据 */ (() => { if (window.__HCC_QY_WMS_MONITOR__) { return; } window.__HCC_QY_WMS_MONITOR__ = true; const IS_TOP = window.top === window; const API_BASE_URL = 'https://hcc321.com'; const readValue = async (key, defaultValue) => { if (typeof GM_getValue === 'function') { const value = GM_getValue(key); return value === undefined ? defaultValue : value; } if (typeof GM !== 'undefined' && typeof GM.getValue === 'function') { return GM.getValue(key, defaultValue); } return defaultValue; }; const writeValue = async (key, value) => { if (typeof GM_setValue === 'function') { GM_setValue(key, value); return; } if (typeof GM !== 'undefined' && typeof GM.setValue === 'function') { await GM.setValue(key, value); } }; const deleteValue = async (key) => { if (typeof GM_deleteValue === 'function') { GM_deleteValue(key); return; } if (typeof GM !== 'undefined' && typeof GM.deleteValue === 'function') { await GM.deleteValue(key); } }; const Storage = { async get(keys, callback) { const result = {}; if (Array.isArray(keys)) { for (const key of keys) { result[key] = await readValue(key); } } else if (typeof keys === 'string') { result[keys] = await readValue(keys); } else if (keys && typeof keys === 'object') { for (const [key, defaultValue] of Object.entries(keys)) { result[key] = await readValue(key, defaultValue); } } if (typeof callback === 'function') { callback(result); } return result; }, async set(items) { for (const [key, value] of Object.entries(items || {})) { await writeValue(key, value); } }, async remove(keys) { const keyList = Array.isArray(keys) ? keys : [keys]; for (const key of keyList) { await deleteValue(key); } } }; const parseRawHeaders = (rawHeaders) => { const headers = new Map(); for (const line of String(rawHeaders || '').split(/\r?\n/)) { const idx = line.indexOf(':'); if (idx <= 0) continue; const key = line.slice(0, idx).trim().toLowerCase(); const value = line.slice(idx + 1).trim(); if (!key) continue; headers.set(key, value); } return { get(name) { return headers.get(String(name || '').toLowerCase()) || null; } }; }; const request = async (url, options = {}) => { if (typeof GM_xmlhttpRequest !== 'function') { return fetch(url, options); } return new Promise((resolve, reject) => { const body = options.body instanceof URLSearchParams ? options.body.toString() : options.body; GM_xmlhttpRequest({ method: options.method || 'GET', url, headers: options.headers || {}, data: body, onload: (response) => { const textBody = response.responseText || ''; const headers = parseRawHeaders(response.responseHeaders); resolve({ ok: response.status >= 200 && response.status < 300, status: response.status, headers, json: async () => JSON.parse(textBody), text: async () => textBody }); }, onerror: (error) => reject(error), ontimeout: () => reject(new Error('GM_xmlhttpRequest timeout')) }); }); }; const postJson = (url, body, headers = {}) => request(url, { method: 'POST', headers: { 'content-type': 'application/json', ...headers }, body: JSON.stringify(body) }); const menuCommandIds = []; const unregisterMenuCommands = () => { if (typeof GM_unregisterMenuCommand !== 'function') return; while (menuCommandIds.length > 0) { const menuId = menuCommandIds.pop(); if (menuId != null) { GM_unregisterMenuCommand(menuId); } } }; const registerMenuCommand = (label, handler) => { if (typeof GM_registerMenuCommand !== 'function') return null; const menuId = GM_registerMenuCommand(label, handler); if (menuId != null) { menuCommandIds.push(menuId); } return menuId; }; const STORAGE_KEYS = { packMonitor: 'packMonitorData', uploadStatus: 'uploadStatus', osdMinimized: 'wmsOsdMinimized', osdPosition: 'wmsOsdPosition', isLoggedIn: 'isLoggedIn', jwtToken: 'jwtToken', userId: 'userId', userData: 'userData', currentStationId: 'currentStationId', currentStationData: 'currentStationData' }; const WMS_LABELS = { qy: '千易 WMS' }; let currentStationId = null; let activeDriver = null; let activeDriverName = 'qy'; let refreshToken = 0; const osdState = { root: null, panel: null, fab: null, tracking: null, sku: null, status: null, skuCompact: null, statusCompact: null, endOrderRow: null, endOrderBtn: null, loginView: null, loginIdInput: null, loginPasswordInput: null, loginBtn: null, loginMessage: null, stationView: null, stationList: null, stationCurrentCard: null, stationCurrentName: null, stationCurrentDetail: null, stationLoading: null, stationEmpty: null, stationPagination: null, stationPrevBtn: null, stationNextBtn: null, stationPageInfo: null, stationRefreshBtn: null, stationEmptyRefreshBtn: null, monitorView: null, logoutBtn: null, copyTrackingBtn: null, copySkuBtn: null, updateTime: null }; const pad = (n) => String(n).padStart(2, '0'); const formatDateTime = (value) => { if (!value) return '未记录'; const date = value instanceof Date ? value : new Date(value); if (Number.isNaN(date.getTime())) return '未记录'; return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`; }; const normalizeText = (value) => String(value || '').trim(); const isAiyacangOrigin = (origin) => { try { const { hostname } = new URL(origin); return hostname === 'aiyacang.com' || hostname.endsWith('.aiyacang.com'); } catch (error) { return false; } }; const debounce = (fn, delay) => { let timer = null; return function debounced(...args) { clearTimeout(timer); timer = setTimeout(() => fn.apply(this, args), delay); }; }; const clamp = (value, min, max) => Math.min(Math.max(value, min), max); const logStep = (scope, message, data) => { if (data !== undefined) { console.log(`[WMS:${scope}] ${message}`, data); return; } console.log(`[WMS:${scope}] ${message}`); }; const updateOsdStatus = (statusData) => { if (!IS_TOP || !osdState.status) return; const status = statusData?.status || 'idle'; const message = statusData?.message || '待更新'; osdState.status.textContent = message; osdState.status.setAttribute('data-status', status); if (osdState.statusCompact) { osdState.statusCompact.textContent = message; osdState.statusCompact.setAttribute('data-status', status); } logStep('osd', '上传状态更新', { status, message }); }; const updateOsdData = (data) => { if (!IS_TOP || !osdState.tracking || !osdState.sku) return; const tracking = data?.trackingNo || ''; const sku = data?.sku || ''; logStep('osd', '数据更新', { tracking, sku }); const setValue = (el, value) => { el.textContent = value || '等待中'; if (value) { el.classList.add('pulse'); window.setTimeout(() => el.classList.remove('pulse'), 220); } }; const flashSku = (el) => { if (!el) return; if (el.__skuFlashTimer) { window.clearTimeout(el.__skuFlashTimer); el.__skuFlashTimer = null; } el.classList.add('sku-hot'); el.__skuFlashTimer = window.setTimeout(() => { el.classList.remove('sku-hot'); el.__skuFlashTimer = null; }, 3000); }; setValue(osdState.tracking, tracking); setValue(osdState.sku, sku); if (osdState.skuCompact) { setValue(osdState.skuCompact, sku); } if (sku) { flashSku(osdState.sku); flashSku(osdState.skuCompact); } else { osdState.sku.classList.remove('sku-hot'); if (osdState.skuCompact) osdState.skuCompact.classList.remove('sku-hot'); } if (osdState.endOrderRow) { osdState.endOrderRow.style.display = sku ? 'flex' : 'none'; } if (osdState.updateTime && data?.timestamp) { const date = new Date(data.timestamp); const timeStr = `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}:${String(date.getSeconds()).padStart(2, '0')}`; osdState.updateTime.textContent = `${timeStr} 更新`; } }; const applyOsdMinimized = (minimized) => { if (!IS_TOP || !osdState.panel || !osdState.fab) return; osdState.panel.classList.toggle('compact', minimized); osdState.panel.classList.remove('hidden'); osdState.fab.classList.add('hidden'); logStep('osd', minimized ? '面板紧凑' : '面板展开'); }; const applyOsdPosition = (pos) => { if (!IS_TOP || !osdState.root || !pos) return; const vw = window.innerWidth; const vh = window.innerHeight; let leftPct, topPct; if (typeof pos.leftPct === 'number' && typeof pos.topPct === 'number') { leftPct = pos.leftPct; topPct = pos.topPct; } else if (typeof pos.left === 'number' && typeof pos.top === 'number') { leftPct = Math.min(100, Math.max(0, (pos.left / vw) * 100)); topPct = Math.min(100, Math.max(0, (pos.top / vh) * 100)); } else { return; } const rootWidth = osdState.root.offsetWidth || 270; const rootHeight = osdState.root.offsetHeight || 200; const maxLeft = vw - rootWidth - 8; const maxTop = vh - rootHeight - 8; const actualLeft = Math.min(Math.max(8, (leftPct / 100) * vw), Math.max(8, maxLeft)); const actualTop = Math.min(Math.max(8, (topPct / 100) * vh), Math.max(8, maxTop)); osdState.root.style.left = `${actualLeft}px`; osdState.root.style.top = `${actualTop}px`; osdState.root.style.right = 'auto'; osdState.root.style.bottom = 'auto'; osdState.root.dataset.leftPct = leftPct; osdState.root.dataset.topPct = topPct; logStep('osd', '应用面板位置', { leftPct, topPct, actualLeft, actualTop }); }; const saveOsdPosition = (pos) => { let positionToSave; if (pos && typeof pos.leftPct === 'number' && typeof pos.topPct === 'number') { positionToSave = { leftPct: pos.leftPct, topPct: pos.topPct }; } else if (pos && typeof pos.left === 'number' && typeof pos.top === 'number') { const vw = window.innerWidth; const vh = window.innerHeight; positionToSave = { leftPct: (pos.left / vw) * 100, topPct: (pos.top / vh) * 100 }; } else { return; } Storage.set({ [STORAGE_KEYS.osdPosition]: positionToSave }); logStep('osd', '保存面板位置(百分比)', positionToSave); }; const enableOsdDrag = (handle) => { if (!handle || !osdState.root) return; logStep('osd', '启用拖拽'); let dragging = false; let offsetX = 0; let offsetY = 0; const onPointerMove = (event) => { if (!dragging) return; const rect = osdState.root.getBoundingClientRect(); const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; const nextLeft = clamp(event.clientX - offsetX, 8, viewportWidth - rect.width - 8); const nextTop = clamp(event.clientY - offsetY, 8, viewportHeight - rect.height - 8); osdState.root.style.left = `${nextLeft}px`; osdState.root.style.top = `${nextTop}px`; osdState.root.style.right = 'auto'; osdState.root.style.bottom = 'auto'; }; const onPointerUp = () => { if (!dragging) return; dragging = false; const rect = osdState.root.getBoundingClientRect(); const vw = window.innerWidth; const vh = window.innerHeight; saveOsdPosition({ leftPct: (rect.left / vw) * 100, topPct: (rect.top / vh) * 100 }); window.removeEventListener('pointermove', onPointerMove); window.removeEventListener('pointerup', onPointerUp); }; handle.addEventListener('pointerdown', (event) => { if (event.target?.closest?.('button')) return; if (event.pointerType === 'mouse' && event.button !== 0) return; dragging = true; const rect = osdState.root.getBoundingClientRect(); offsetX = event.clientX - rect.left; offsetY = event.clientY - rect.top; handle.setPointerCapture?.(event.pointerId); window.addEventListener('pointermove', onPointerMove); window.addEventListener('pointerup', onPointerUp); }); }; const initOsd = () => { if (!IS_TOP) return; if (document.getElementById('hcc-wms-osd-root')) return; logStep('osd', '初始化 OSD'); const root = document.createElement('div'); root.id = 'hcc-wms-osd-root'; const shadow = root.attachShadow({ mode: 'open' }); shadow.innerHTML = `
待更新
等待中
货查查助手千易 WMS
运单号
等待中
SKU
等待中
上传状态
待更新
`; const mountTarget = document.body || document.documentElement; mountTarget.appendChild(root); logStep('osd', 'OSD 挂载完成'); osdState.root = root; osdState.panel = shadow.getElementById('osd-panel'); osdState.fab = shadow.getElementById('osd-expand'); osdState.tracking = shadow.getElementById('osd-tracking'); osdState.sku = shadow.getElementById('osd-sku'); osdState.skuCompact = shadow.getElementById('osd-sku-compact'); osdState.status = shadow.getElementById('osd-status'); osdState.statusCompact = shadow.getElementById('osd-status-compact'); osdState.endOrderRow = shadow.getElementById('end-order-row'); osdState.endOrderBtn = shadow.getElementById('osd-end-order'); osdState.loginView = shadow.getElementById('view-login'); osdState.loginIdInput = shadow.getElementById('osd-login-id'); osdState.loginPasswordInput = shadow.getElementById('osd-login-password'); osdState.loginBtn = shadow.getElementById('osd-login-btn'); osdState.loginMessage = shadow.getElementById('osd-login-message'); osdState.stationView = shadow.getElementById('view-station'); osdState.stationList = shadow.getElementById('osd-station-list'); osdState.stationCurrentCard = shadow.getElementById('osd-current-station-card'); osdState.stationCurrentName = shadow.getElementById('osd-current-station-name'); osdState.stationCurrentDetail = shadow.getElementById('osd-current-station-detail'); osdState.stationLoading = shadow.getElementById('osd-loading-state'); osdState.stationEmpty = shadow.getElementById('osd-empty-state'); osdState.stationPagination = shadow.getElementById('osd-pagination'); osdState.stationPrevBtn = shadow.getElementById('osd-prev-btn'); osdState.stationNextBtn = shadow.getElementById('osd-next-btn'); osdState.stationPageInfo = shadow.getElementById('osd-page-info'); osdState.stationRefreshBtn = shadow.getElementById('osd-refresh-station'); osdState.stationEmptyRefreshBtn = shadow.getElementById('osd-empty-refresh-btn'); osdState.monitorView = shadow.getElementById('view-monitor'); osdState.logoutBtn = shadow.getElementById('osd-logout-btn'); osdState.copyTrackingBtn = shadow.getElementById('osd-copy-tracking'); osdState.copySkuBtn = shadow.getElementById('osd-copy-sku'); osdState.updateTime = shadow.getElementById('osd-update-time'); const minimizeBtn = shadow.querySelector('.header'); // We need the actual button, not the header const minimizeButton = shadow.getElementById('osd-minimize'); const expandCompactBtn = shadow.getElementById('osd-expand-compact'); const applyMinimizeButtonState = (minimized) => { minimizeButton.textContent = minimized ? '+' : '—'; minimizeButton.title = minimized ? '展开' : '最小化'; }; minimizeButton.addEventListener('click', () => { const isMinimized = osdState.panel?.classList.contains('compact'); const nextMinimized = !isMinimized; applyOsdMinimized(nextMinimized); applyMinimizeButtonState(nextMinimized); Storage.set({ [STORAGE_KEYS.osdMinimized]: nextMinimized }); }); expandCompactBtn.addEventListener('click', () => { applyOsdMinimized(false); applyMinimizeButtonState(false); Storage.set({ [STORAGE_KEYS.osdMinimized]: false }); }); osdState.fab.addEventListener('click', () => { applyOsdMinimized(false); applyMinimizeButtonState(false); Storage.set({ [STORAGE_KEYS.osdMinimized]: false }); }); if (osdState.endOrderBtn) { osdState.endOrderBtn.addEventListener('click', () => { if (activeDriver && typeof activeDriver.forceEndOrder === 'function') { logStep('osd', '手动触发结束订单'); activeDriver.forceEndOrder(); } }); } let stationPage = 1; let stationTotalPages = 1; let selectedStationId = null; const switchView = (viewName) => { const views = ['login', 'station', 'monitor']; views.forEach((v) => { const viewEl = osdState[`${v}View`]; if (viewEl) { viewEl.classList.toggle('hidden', v !== viewName); } }); osdState.panel.classList.remove('view-login', 'view-station'); if (viewName === 'login') { osdState.panel.classList.add('view-login'); } else if (viewName === 'station') { osdState.panel.classList.add('view-station'); } logStep('osd', '切换视图', { view: viewName }); }; const showLoginMessage = (text, isError = true) => { if (!osdState.loginMessage) return; osdState.loginMessage.textContent = text; osdState.loginMessage.className = `status-message ${isError ? 'error' : 'success'}`; }; const setLoginButtonState = (isLoading) => { if (!osdState.loginBtn) return; osdState.loginBtn.disabled = isLoading; osdState.loginBtn.textContent = isLoading ? '登录中...' : '登 录'; }; const doLogin = async () => { const id = osdState.loginIdInput?.value?.trim() || ''; const password = osdState.loginPasswordInput?.value?.trim() || ''; if (!id) { showLoginMessage('请输入账号 ID'); return; } if (!password) { showLoginMessage('请输入密码'); return; } try { setLoginButtonState(true); showLoginMessage(''); const formData = new URLSearchParams({ id, password }); const jwtToken = await getJwtToken(); const headers = { 'accept': 'application/json, text/plain, */*', 'content-type': 'application/x-www-form-urlencoded' }; if (jwtToken) { headers['Authorization'] = `Bearer ${jwtToken}`; } const response = await request(`${API_BASE_URL}/user/pluginLogin`, { method: 'POST', headers, body: formData }); const contentType = response.headers.get('content-type') || ''; const data = contentType.includes('application/json') ? await response.json() : await response.text(); if (response.ok && data.code === 200) { const token = data.data?.jwtToken || ''; const userData = data.data || { id }; await Storage.set({ isLoggedIn: true, userId: id, jwtToken: token, userData, loginTime: Date.now() }); showLoginMessage('登录成功!', false); setTimeout(() => { switchView('station'); loadStations(1); }, 500); } else { const errorMsg = data.msg || data.message || '登录失败,请检查账号密码'; showLoginMessage(errorMsg); } } catch (error) { console.error('登录错误:', error); showLoginMessage('网络错误,请稍后重试'); } finally { setLoginButtonState(false); } }; const doLogout = async () => { await Storage.remove([ 'isLoggedIn', 'userId', 'jwtToken', 'userData', 'loginTime', 'currentStationId', 'currentStationData' ]); currentStationId = null; selectedStationId = null; if (osdState.loginIdInput) osdState.loginIdInput.value = ''; if (osdState.loginPasswordInput) osdState.loginPasswordInput.value = ''; if (osdState.loginMessage) osdState.loginMessage.textContent = ''; switchView('login'); logStep('osd', '用户登出'); }; const loadStations = async (page = 1) => { if (!osdState.stationList) return; osdState.stationLoading?.classList.remove('hidden'); osdState.stationList.innerHTML = ''; osdState.stationEmpty?.classList.add('hidden'); osdState.stationPagination?.classList.add('hidden'); try { const jwtToken = await getJwtToken(); const headers = { 'accept': 'application/json, text/plain, */*', 'content-type': 'application/x-www-form-urlencoded' }; if (jwtToken) { headers['Authorization'] = `Bearer ${jwtToken}`; } const formData = new URLSearchParams({ page: String(page), size: '10', sortBy: 'id', desc: false }); const response = await request(`${API_BASE_URL}/station/select`, { method: 'POST', headers, body: formData }); const contentType = response.headers.get('content-type') || ''; const result = contentType.includes('application/json') ? await response.json() : await response.text(); if (response.ok && result.code === 200) { const stations = result.data?.content || []; const pages = result.data?.pages || 1; stationPage = page; stationTotalPages = pages; if (stations.length > 0) { displayStations(stations); updatePagination(); } else { showEmptyStationState(); } } else { console.error('加载工位失败:', result); showEmptyStationState(); } } catch (error) { console.error('加载工位错误:', error); showEmptyStationState(); } finally { osdState.stationLoading?.classList.add('hidden'); } }; const displayStations = (stations) => { if (!osdState.stationList) return; osdState.stationList.innerHTML = ''; stations.forEach((station, index) => { const isSelected = station.id === selectedStationId; const isDisabled = !station.enable; const item = document.createElement('div'); item.className = `station-item${isSelected ? ' selected' : ''}${isDisabled ? ' disabled' : ''}`; item.dataset.stationId = station.id; const leftDiv = document.createElement('div'); leftDiv.className = 'station-item-left'; leftDiv.innerHTML = ` ${station.stationName || '未命名工位'} IP: ${station.stationIp || 'N/A'} `; const rightDiv = document.createElement('div'); rightDiv.className = 'station-item-right'; rightDiv.innerHTML = ` ${station.stationType || '未知'} ${isSelected ? '' : ''} `; item.appendChild(leftDiv); item.appendChild(rightDiv); if (!isDisabled) { item.addEventListener('click', () => selectStation(station)); } osdState.stationList.appendChild(item); if (index < stations.length - 1) { const divider = document.createElement('div'); divider.className = 'list-divider'; osdState.stationList.appendChild(divider); } }); }; const selectStation = async (station) => { selectedStationId = station.id; await Storage.set({ currentStationId: station.id, currentStationData: station }); currentStationId = station.id; updateCurrentStationDisplay(station); displayStations((function() { return [{ ...station }]; })()); setTimeout(() => { switchView('monitor'); refreshDriver(); }, 300); }; const updateCurrentStationDisplay = (station) => { if (!osdState.stationCurrentName || !osdState.stationCurrentDetail) return; if (station) { osdState.stationCurrentName.textContent = station.stationName || '未命名'; osdState.stationCurrentDetail.textContent = `${station.stationType || ''} · IP: ${station.stationIp || 'N/A'}`; } else { osdState.stationCurrentName.textContent = '未选择'; osdState.stationCurrentDetail.textContent = ''; } }; const updatePagination = () => { if (!osdState.stationPagination) return; if (stationTotalPages <= 1) { osdState.stationPagination.classList.add('hidden'); return; } osdState.stationPagination.classList.remove('hidden'); if (osdState.stationPageInfo) { osdState.stationPageInfo.textContent = `${stationPage} / ${stationTotalPages}`; } if (osdState.stationPrevBtn) { osdState.stationPrevBtn.disabled = stationPage <= 1; } if (osdState.stationNextBtn) { osdState.stationNextBtn.disabled = stationPage >= stationTotalPages; } }; const showEmptyStationState = () => { if (osdState.stationList) osdState.stationList.innerHTML = ''; if (osdState.stationPagination) osdState.stationPagination.classList.add('hidden'); if (osdState.stationEmpty) osdState.stationEmpty.classList.remove('hidden'); }; const initOsdView = async () => { const loginData = await Storage.get(['isLoggedIn', 'currentStationId', 'currentStationData']); if (loginData.isLoggedIn) { if (!loginData.currentStationId) { switchView('station'); loadStations(1); } else { currentStationId = loginData.currentStationId; selectedStationId = loginData.currentStationId; updateCurrentStationDisplay(loginData.currentStationData); switchView('monitor'); } } else { switchView('login'); } }; if (osdState.loginBtn) { osdState.loginBtn.addEventListener('click', doLogin); } if (osdState.loginPasswordInput) { osdState.loginPasswordInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') { doLogin(); } }); } if (osdState.logoutBtn) { osdState.logoutBtn.addEventListener('click', doLogout); } if (osdState.copyTrackingBtn) { osdState.copyTrackingBtn.addEventListener('click', async () => { const tracking = osdState.tracking?.textContent || ''; if (tracking && tracking !== '等待中') { try { await navigator.clipboard.writeText(tracking); osdState.copyTrackingBtn.style.color = 'var(--success-green)'; setTimeout(() => { osdState.copyTrackingBtn.style.color = ''; }, 1500); } catch (err) { console.error('复制失败:', err); } } }); } if (osdState.copySkuBtn) { osdState.copySkuBtn.addEventListener('click', async () => { const sku = osdState.sku?.textContent || ''; if (sku && sku !== '等待中') { try { await navigator.clipboard.writeText(sku); osdState.copySkuBtn.style.color = 'var(--success-green)'; setTimeout(() => { osdState.copySkuBtn.style.color = ''; }, 1500); } catch (err) { console.error('复制失败:', err); } } }); } if (osdState.stationRefreshBtn) { osdState.stationRefreshBtn.addEventListener('click', () => { loadStations(stationPage); }); } if (osdState.stationPrevBtn) { osdState.stationPrevBtn.addEventListener('click', () => { if (stationPage > 1) { loadStations(stationPage - 1); } }); } if (osdState.stationNextBtn) { osdState.stationNextBtn.addEventListener('click', () => { if (stationPage < stationTotalPages) { loadStations(stationPage + 1); } }); } if (osdState.stationEmptyRefreshBtn) { osdState.stationEmptyRefreshBtn.addEventListener('click', () => { loadStations(1); }); } initOsdView(); Storage.get([STORAGE_KEYS.osdMinimized, STORAGE_KEYS.osdPosition], (res) => { const minimized = Boolean(res?.[STORAGE_KEYS.osdMinimized]); applyOsdMinimized(minimized); applyMinimizeButtonState(minimized); applyOsdPosition(res?.[STORAGE_KEYS.osdPosition]); }); window.addEventListener('resize', () => { Storage.get([STORAGE_KEYS.osdPosition], (res) => { applyOsdPosition(res?.[STORAGE_KEYS.osdPosition]); }); }); const header = shadow.querySelector('.header'); const compactBar = shadow.querySelector('.compact-bar'); enableOsdDrag(header); enableOsdDrag(compactBar); }; const setOsdVisible = (visible) => { if (!IS_TOP || !osdState.root) return; osdState.root.style.display = visible ? 'block' : 'none'; logStep('osd', visible ? '显示 OSD' : '隐藏 OSD'); }; const setUploadStatus = (status, message) => { const payload = { status, message, timestamp: Date.now() }; Storage.set({ [STORAGE_KEYS.uploadStatus]: payload }); if (IS_TOP) { updateOsdStatus(payload); } else { window.top?.postMessage( { type: 'HCC_WMS_UPLOAD_STATUS', payload }, location.origin ); } logStep('upload', '状态写入', payload); }; const getJwtToken = async () => { logStep('auth', '读取 JWT'); const data = await Storage.get(['jwtToken', 'isLoggedIn']); return data.isLoggedIn ? data.jwtToken : null; }; const uploadRecord = async (record) => { logStep('upload', '开始上传', { id: record?.id, count: record?.subGoodsList?.length || 0 }); setUploadStatus('uploading', '上传中...'); const jwtToken = await getJwtToken(); const headers = { 'content-type': 'application/json' }; if (jwtToken) { headers.Authorization = `Bearer ${jwtToken}`; } const response = await postJson(`${API_BASE_URL}/goods/modeOneAddWithSubGoods`, record, headers); const contentType = response.headers.get('content-type') || ''; const data = contentType.includes('application/json') ? await response.json() : await response.text(); if (!response.ok) { console.error('[Pack Monitor] 上传失败:', response.status, data); setUploadStatus('error', `上传失败 (${response.status})`); logStep('upload', '上传失败', { status: response.status }); return { ok: false, status: response.status, data }; } console.log('[Pack Monitor] 上传成功:', data); setUploadStatus('success', '上传成功'); logStep('upload', '上传成功', { status: response.status }); return { ok: true, status: response.status, data }; }; const buildUploadRecord = ({ trackingNo, scans, startTime, endTime }) => { if (!trackingNo) return null; const stationId = currentStationId ? Number(currentStationId) : 0; logStep('upload', '构建上传结构', { trackingNo, count: scans?.length || 0 }); const subGoodsList = (scans || []).map((item) => ({ scanTime: formatDateTime(item.time), subGoodsId: item.sku, error: false })); return { id: trackingNo, station_id: stationId, subGoodsList, headerList: [], startTime: formatDateTime(startTime), endTime: formatDateTime(endTime) }; }; const finalizeAndUploadScans = async ({ trackingNo, scans, startTime, endTime, reason }) => { logStep('upload', '准备上传批次', { trackingNo, count: scans?.length || 0, reason }); if (!trackingNo) { console.warn('[Pack Monitor] 运单号为空,跳过上传', reason || ''); setUploadStatus('skipped', '运单号为空,跳过上传'); return; } if (!scans || scans.length === 0) { console.warn('[Pack Monitor] 子货列表为空,跳过上传', reason || ''); setUploadStatus('skipped', '子货列表为空,跳过上传'); return; } const record = buildUploadRecord({ trackingNo, scans, startTime, endTime }); if (!record) { setUploadStatus('skipped', '运单号为空,跳过上传'); return; } console.log('[Pack Monitor] 上传订单:', reason || ''); await uploadRecord(record); }; const updateMonitorData = (data) => { logStep('monitor', '更新监控数据', { trackingNo: data?.trackingNo, sku: data?.sku }); const payload = { ...data, wmsType: activeDriverName, wmsLabel: WMS_LABELS[activeDriverName] || '', timestamp: Date.now() }; Storage.set({ [STORAGE_KEYS.packMonitor]: payload }); if (IS_TOP) { updateOsdData(payload); } else { window.top?.postMessage( { type: 'HCC_WMS_MONITOR_UPDATE', payload }, location.origin ); } }; const ensureLogin = async () => { logStep('auth', '校验登录状态'); const loginData = await Storage.get(['isLoggedIn', 'currentStationId']); if (!loginData.isLoggedIn || !loginData.currentStationId) { currentStationId = null; logStep('auth', '未登录或未选择工位'); return false; } currentStationId = loginData.currentStationId; logStep('auth', '登录有效', { stationId: currentStationId }); return true; }; // ============================================================ // API 拦截桥接 (类似 flash.user.js 方案) // ============================================================ const QY_API_BRIDGE_MESSAGE = 'HCC_QY_API_BRIDGE'; const QY_API_BRIDGE_LISTENER_FLAG = '__HCC_QY_API_BRIDGE_LISTENER_INSTALLED__'; const QY_API_PATCH_FLAG = '__HCC_QY_API_INTERCEPTOR_INSTALLED__'; const QY_API_BRIDGE_SCRIPT_ID = 'hcc-qy-api-bridge'; const QY_API_PATHS = ['/service/ob/pack/prepare', '/service/ob/pack/confirm']; let qyApiResponseHandler = null; const installBridgeListener = () => { if (window[QY_API_BRIDGE_LISTENER_FLAG]) return; window[QY_API_BRIDGE_LISTENER_FLAG] = true; window.addEventListener('message', (event) => { if (event.origin !== location.origin) return; if (event.data?.type !== QY_API_BRIDGE_MESSAGE) return; if (typeof qyApiResponseHandler === 'function') { qyApiResponseHandler(event.data.payload || {}); } }); }; const buildBridgeSource = () => { const messageType = JSON.stringify(QY_API_BRIDGE_MESSAGE); const patchFlag = JSON.stringify(QY_API_PATCH_FLAG); const apiPaths = JSON.stringify(QY_API_PATHS); return ` (function() { if (window[${patchFlag}]) return; window[${patchFlag}] = true; var API_PATHS = ${apiPaths}; function shouldObserveUrl(url) { try { var u = new URL(String(url || ''), window.location.origin); return API_PATHS.indexOf(u.pathname) !== -1; } catch (e) { return false; } } function serializeBody(body) { if (typeof body === 'string') return body; if (body instanceof URLSearchParams) return body.toString(); if (body instanceof FormData) { try { return JSON.stringify(Array.from(body.entries())); } catch (e) { return ''; } } if (body == null) return ''; try { return String(body); } catch (e) { return ''; } } function publish(detail) { window.postMessage({ type: ${messageType}, payload: detail }, window.location.origin); } // Override fetch var origFetch = window.fetch; if (typeof origFetch === 'function') { window.fetch = async function(...args) { var response = await origFetch.apply(window, args); try { var resource = args[0]; var init = args[1]; var requestUrl = typeof resource === 'string' ? resource : (resource ? resource.url : ''); if (shouldObserveUrl(requestUrl)) { var clone = response.clone(); var text = await clone.text(); publish({ url: String(requestUrl || ''), requestBody: serializeBody(init ? init.body : undefined), responseText: text, transport: 'fetch' }); } } catch (error) { console.error('[QY] fetch bridge failed', error); } return response; }; } // Override XHR var origOpen = XMLHttpRequest.prototype.open; var origSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function(method, url) { this.__hccQyUrl = url; var rest = Array.prototype.slice.call(arguments, 2); return origOpen.apply(this, [method, url].concat(rest)); }; XMLHttpRequest.prototype.send = function(body) { this.__hccQyBody = body; if (!this.__hccQyListenerBound) { this.__hccQyListenerBound = true; this.addEventListener('loadend', () => { try { if (!shouldObserveUrl(this.__hccQyUrl)) return; publish({ url: String(this.__hccQyUrl || ''), requestBody: serializeBody(this.__hccQyBody), responseText: typeof this.responseText === 'string' ? this.responseText : '', transport: 'xhr' }); } catch (error) { console.error('[QY] xhr bridge failed', error); } }); } return origSend.apply(this, arguments); }; })(); `; }; const installApiInterceptor = () => { if (window[QY_API_PATCH_FLAG]) return; window[QY_API_PATCH_FLAG] = true; installBridgeListener(); const existingScript = document.getElementById(QY_API_BRIDGE_SCRIPT_ID); if (existingScript) return; const script = document.createElement('script'); script.id = QY_API_BRIDGE_SCRIPT_ID; script.textContent = buildBridgeSource(); const parent = document.head || document.documentElement || document.body; parent?.appendChild(script); script.remove(); logStep('qy', 'API 拦截器已注入'); }; // ============================================================ // QY(千易)API 驱动 // ============================================================ const createQyDriver = () => { const TARGET_ORIGINS = ['https://gwmsth.best-inc.com', 'http://gwmsth.best-inc.com']; const state = { active: false, // Current order context currentOrderCode: null, // WMS order code currentTrackingNo: null, // tracking/shipping number // Scan tracking for batch management lastScanTracking: null, lastScanSku: null, lastScanId: null, lastScanAt: 0, pendingScans: [], scannedSkuList: [], preparedOrderCode: null, skuAliasMap: new Map(), orderTrackingMap: new Map(), skuTrackingMap: new Map(), trackingStartTimes: new Map(), lastConfirmSignature: null }; const match = () => isAiyacangOrigin(window.location.origin) || TARGET_ORIGINS.includes(window.location.origin); const setText = (value) => normalizeText(value); const isSkuInput = (target) => { if (!target || target.tagName !== 'INPUT') return false; const placeholder = normalizeText(target.getAttribute('placeholder')); if (/LPN|WMS|出库单号|运单号/i.test(placeholder)) return false; const visibleInputs = Array.from(document.querySelectorAll('input')) .filter((input) => { const rect = input.getBoundingClientRect(); const inputPlaceholder = normalizeText(input.getAttribute('placeholder')); return rect.width > 0 && rect.height > 0 && !input.disabled && input.type !== 'hidden' && !/LPN|WMS|出库单号|运单号/i.test(inputPlaceholder); }); if (visibleInputs.includes(target)) { const scanInput = Array.from(document.querySelectorAll('input')) .find((input) => /LPN|WMS|出库单号|运单号/i.test(normalizeText(input.getAttribute('placeholder')))); if (!scanInput) return true; const scanRect = scanInput.getBoundingClientRect(); const targetRect = target.getBoundingClientRect(); if (targetRect.top >= scanRect.top) return true; } const panel = target.closest?.('.wmstool-form-item, .ant-form-item, .el-form-item, div') || target.parentElement; const labelText = normalizeText(panel?.innerText || panel?.textContent || ''); if (/SKU/i.test(labelText)) return true; return document.activeElement === target && !placeholder; }; const buildSkuAliasMap = (skuList) => { const map = new Map(); for (const sku of skuList || []) { const aliases = [sku.skuCode, ...(sku.barcodes || [])] .map((value) => normalizeText(value)) .filter(Boolean); for (const alias of aliases) { map.set(alias, sku); } } return map; }; const recordSkuScan = (scanCode, source = 'input') => { if (!state.currentTrackingNo) { logStep('qy', '忽略 SKU 输入: 缺少运单上下文', { scanCode }); return; } const normalizedCode = normalizeText(scanCode); if (!normalizedCode) return; const sku = state.skuAliasMap.get(normalizedCode); if (!sku) { logStep('qy', '忽略 SKU 输入: 未匹配 SKU/条码', { scanCode: normalizedCode, currentOrderCode: state.currentOrderCode || '', trackingNo: state.currentTrackingNo || '' }); return; } const skuCode = sku.skuCode || normalizedCode; const scanTrackingNo = normalizeText(sku?.trackingNo) || state.currentTrackingNo; if (state.lastScanTracking && scanTrackingNo && scanTrackingNo !== state.lastScanTracking) { void flushPendingScans('qy_tracking_changed'); state.pendingScans = []; state.scannedSkuList = []; } const entry = { sku: skuCode, scanId: normalizedCode, trackingNo: scanTrackingNo, time: new Date(), orderedQty: sku?.orderedQty || 0, packedQty: sku?.packedQty || 0 }; state.pendingScans.push(entry); state.scannedSkuList.push({ skuCode: skuCode, scanCode: normalizedCode, trackingNo: scanTrackingNo, time: entry.time, source }); state.lastScanSku = skuCode; state.lastScanTracking = scanTrackingNo; logStep('qy', '记录 SKU 输入', { trackingNo: state.lastScanTracking, sku: skuCode, scanId: normalizedCode, count: state.pendingScans.length, skuListCount: state.scannedSkuList.length, source }); updateMonitorData({ isTargetPage: true, url: window.location.href, batchNo: state.currentOrderCode || '', sku: state.lastScanSku || '', trackingNo: state.lastScanTracking || '' }); }; const handleSkuInputKeydown = (event) => { if (!state.active) return; if (event.key !== 'Enter') return; const target = event.target; if (!isSkuInput(target)) return; const value = target.value; window.setTimeout(() => recordSkuScan(value, 'input_enter'), 0); }; const parseConfirmRequest = (requestBody) => { let data; try { data = JSON.parse(requestBody); } catch (e) { return null; } const wrap = data?.wrapPackVo; const items = Array.isArray(wrap?.simplePackItemVoList) ? wrap.simplePackItemVoList : []; if (!wrap || items.length === 0) return null; const groups = new Map(); for (const item of items) { const detail = item.packingVolist || item.packingVoList || {}; const skuCode = normalizeText(detail.skuCode || item.skuCode || item.scanCode); if (!skuCode) continue; const orderCode = normalizeText(detail.orderHeaderCode || item.orderHeaderCode || state.currentOrderCode); const trackingNo = normalizeText( detail.trackingNo || detail.refCode || state.orderTrackingMap.get(orderCode) || state.skuTrackingMap.get(`${orderCode}||${skuCode}`) || state.skuTrackingMap.get(skuCode) || state.currentTrackingNo ); if (!trackingNo) continue; if (!groups.has(trackingNo)) { groups.set(trackingNo, { trackingNo, orderCode, startTime: state.trackingStartTimes.get(trackingNo) || null, scans: [] }); } const group = groups.get(trackingNo); const qty = Math.max(1, Math.ceil(Number(item.scanQty || item.qty || 1) || 1)); for (let i = 0; i < qty; i += 1) { group.scans.push({ sku: skuCode, scanId: normalizeText(detail.barCode || item.scanCode || skuCode), time: new Date(), orderedQty: qty, packedQty: qty }); } } const firstItem = items[0] || {}; const firstDetail = firstItem.packingVolist || firstItem.packingVoList || {}; const param = wrap.param || {}; const groupedScans = Array.from(groups.values()).filter((group) => group.scans.length > 0); return { orderCode: normalizeText(firstDetail.orderHeaderCode || firstItem.orderHeaderCode || state.currentOrderCode), trackingNo: normalizeText( firstDetail.trackingNo || firstDetail.refCode || state.orderTrackingMap.get(firstDetail.orderHeaderCode || firstItem.orderHeaderCode) || state.currentTrackingNo ), startTime: param.startTime ? new Date(param.startTime) : null, endTime: param.endTime ? new Date(param.endTime) : null, groups: groupedScans, signature: JSON.stringify({ orderHeaderId: firstItem.orderHeaderId, orderCode: firstDetail.orderHeaderCode || firstItem.orderHeaderCode, groups: groupedScans.map((group) => ({ trackingNo: group.trackingNo, skus: group.scans.map((scan) => scan.sku) })) }) }; }; const responseIsSuccess = (responseText) => { try { const data = JSON.parse(responseText); return data?.success !== false; } catch (e) { return true; } }; /** * Parse the /service/ob/pack/prepare API response. * Expected structure: * row.orderHeaderVoList[0].orderCode - WMS order number * row.orderHeaderVoList[0].trackingNo - tracking/shipping number * row.orderDetailVoList[].skuCode - SKU identifier * row.orderDetailVoList[].orderedQty - ordered quantity * row.orderDetailVoList[].packedQty - packed quantity * row.orderDetailVoList[].barCodeList[] - barcode alternatives * row.barCodeAndPackingVoList[].scanCode - scan barcodes */ const parsePrepareResponse = (responseText) => { let data; try { data = JSON.parse(responseText); } catch (e) { return null; } if (!data || typeof data !== 'object') return null; // The response might be wrapped in a standard envelope const row = data.row || data.data || data; if (!row || typeof row !== 'object') return null; // Extract order header info const headerList = row.orderHeaderVoList; if (!Array.isArray(headerList) || headerList.length === 0) return null; const header = headerList[0]; const orderCode = normalizeText(header.orderCode); const trackingNo = normalizeText(header.trackingNo || header.refCode || header.expressNo || header.waybillNo); if (!orderCode) return null; // Extract SKU list from order details const detailList = row.orderDetailVoList || []; const skuList = []; for (const detail of detailList) { const skuCode = normalizeText(detail.skuCode); if (!skuCode) continue; const orderedQty = Number(detail.orderedQty || detail.dueOutQty || detail.originExpectedQty) || 0; const packedQty = Number(detail.packedQty) || 0; const barcodes = [ detail.barCode, ...(Array.isArray(detail.barCodeList) ? detail.barCodeList : []) ].map((b) => normalizeText(b)).filter(Boolean); skuList.push({ skuCode, orderedQty, packedQty, trackingNo: normalizeText(detail.trackingNo || detail.refCode), barcodes }); } // Extract scan barcodes from packing list const scanCodes = []; const packingList = row.barCodeAndPackingVoList || []; for (const item of packingList) { const sc = normalizeText(item.scanCode); if (sc) scanCodes.push(sc); } return { orderCode, trackingNo: trackingNo || normalizeText(skuList[0]?.trackingNo), skuList, scanCodes }; }; const handlePrepareResponse = (payload) => { if (!state.active) return; const prepareTime = new Date(); const parsed = parsePrepareResponse(payload.responseText); if (!parsed) return; logStep('qy', '解析到 prepare 接口响应', { orderCode: parsed.orderCode, trackingNo: parsed.trackingNo, skuCount: parsed.skuList.length, scanCodeCount: parsed.scanCodes.length }); // If order changed, flush previous scans if (state.currentOrderCode && parsed.orderCode !== state.currentOrderCode) { void flushPendingScans('qy_order_changed'); state.preparedOrderCode = null; state.skuAliasMap.clear(); state.orderTrackingMap.clear(); state.skuTrackingMap.clear(); } // Update current order context state.currentOrderCode = parsed.orderCode; if (parsed.trackingNo && parsed.trackingNo !== state.currentTrackingNo) { state.currentTrackingNo = parsed.trackingNo; } if (parsed.trackingNo && !state.trackingStartTimes.has(parsed.trackingNo)) { state.trackingStartTimes.set(parsed.trackingNo, prepareTime); logStep('qy', '记录运单开始时间', { trackingNo: parsed.trackingNo, startTime: formatDateTime(prepareTime) }); } if (parsed.orderCode && parsed.trackingNo) { state.orderTrackingMap.set(parsed.orderCode, parsed.trackingNo); } for (const sku of parsed.skuList) { const skuTrackingNo = normalizeText(sku.trackingNo) || parsed.trackingNo; if (skuTrackingNo && !state.trackingStartTimes.has(skuTrackingNo)) { state.trackingStartTimes.set(skuTrackingNo, prepareTime); } if (skuTrackingNo) { state.skuTrackingMap.set(`${parsed.orderCode}||${sku.skuCode}`, skuTrackingNo); if (!state.skuTrackingMap.has(sku.skuCode)) { state.skuTrackingMap.set(sku.skuCode, skuTrackingNo); } } } // If tracking changed, flush if (state.lastScanTracking && state.currentTrackingNo && state.currentTrackingNo !== state.lastScanTracking) { void flushPendingScans('qy_tracking_changed'); } state.skuAliasMap = buildSkuAliasMap(parsed.skuList); state.preparedOrderCode = parsed.orderCode; updateMonitorData({ isTargetPage: true, url: window.location.href, batchNo: state.currentOrderCode || '', sku: '', trackingNo: state.currentTrackingNo || '' }); if (state.preparedOrderCode === parsed.orderCode) { updateMonitorData({ isTargetPage: true, url: window.location.href, batchNo: state.currentOrderCode || '', sku: '', trackingNo: state.currentTrackingNo || '' }); } }; const handleConfirmResponse = (payload) => { if (!state.active) return; if (!responseIsSuccess(payload.responseText)) { logStep('qy', 'confirm 接口未成功,跳过上传'); return; } const parsed = parseConfirmRequest(payload.requestBody); if (!parsed || !parsed.groups || parsed.groups.length === 0) { logStep('qy', 'confirm 接口无可上传 SKU'); return; } if (parsed.signature && parsed.signature === state.lastConfirmSignature) { logStep('qy', 'confirm 接口重复,跳过上传'); return; } state.lastConfirmSignature = parsed.signature; state.currentOrderCode = parsed.orderCode || state.currentOrderCode; state.currentTrackingNo = parsed.trackingNo || state.currentTrackingNo; const lastGroup = parsed.groups[parsed.groups.length - 1]; const lastScan = lastGroup?.scans[lastGroup.scans.length - 1]; state.lastScanTracking = lastGroup?.trackingNo || parsed.trackingNo || state.lastScanTracking; state.lastScanSku = lastScan?.sku || state.lastScanSku; logStep('qy', 'confirm 接口触发结束订单', { orderCode: state.currentOrderCode, groupCount: parsed.groups.length, count: parsed.groups.reduce((sum, group) => sum + group.scans.length, 0) }); const pendingByTracking = new Map(); const scannedSkuList = state.scannedSkuList.length > 0 ? state.scannedSkuList.map((item) => ({ sku: item.skuCode, scanId: item.scanCode, trackingNo: item.trackingNo, time: item.time })) : state.pendingScans; for (const scan of scannedSkuList) { const trackingNo = normalizeText(scan.trackingNo || state.currentTrackingNo || ''); if (!trackingNo) continue; if (!pendingByTracking.has(trackingNo)) { pendingByTracking.set(trackingNo, []); } pendingByTracking.get(trackingNo).push(scan); } updateMonitorData({ isTargetPage: true, url: window.location.href, batchNo: state.currentOrderCode || '', sku: state.lastScanSku || '', trackingNo: state.lastScanTracking || '' }); const uploadGroups = parsed.groups.map((group) => { const actualScans = pendingByTracking.get(group.trackingNo) || []; const expectedCount = group.scans.length; const scans = actualScans.slice(0, expectedCount); if (scans.length < expectedCount) { for (let i = scans.length; i < expectedCount; i += 1) { scans.push(group.scans[i]); } } return { ...group, scans }; }); state.pendingScans = []; state.scannedSkuList = []; for (const group of uploadGroups) { void finalizeAndUploadScans({ trackingNo: group.trackingNo, scans: group.scans, startTime: group.startTime || parsed.startTime || new Date(group.scans[0].time), endTime: parsed.endTime || new Date(), reason: 'qy_pack_confirm' }); state.trackingStartTimes.delete(group.trackingNo); } }; /** * Handle intercepted QY API responses. */ const handleApiResponse = (payload) => { let pathname = ''; try { pathname = new URL(String(payload.url || ''), window.location.origin).pathname; } catch (e) { pathname = ''; } if (pathname === '/service/ob/pack/prepare') { handlePrepareResponse(payload); return; } if (pathname === '/service/ob/pack/confirm') { handleConfirmResponse(payload); } }; const flushPendingScans = async (reason) => { const scans = state.scannedSkuList.length > 0 ? state.scannedSkuList.map((item) => ({ sku: item.skuCode, scanId: item.scanCode, trackingNo: item.trackingNo, time: item.time })) : state.pendingScans.slice(); if (!state.lastScanTracking || scans.length === 0) { return; } logStep('qy', '提交扫描批次', { tracking: state.lastScanTracking, orderCode: state.currentOrderCode, count: scans.length, reason }); const startTime = state.trackingStartTimes.get(state.lastScanTracking) || new Date(scans[0].time); const endTime = new Date(); await finalizeAndUploadScans({ trackingNo: state.lastScanTracking, scans, startTime, endTime, reason }); state.trackingStartTimes.delete(state.lastScanTracking); state.pendingScans = []; state.scannedSkuList = []; }; const resetState = () => { state.currentOrderCode = null; state.currentTrackingNo = null; state.lastScanTracking = null; state.lastScanSku = null; state.lastScanId = null; state.lastScanAt = 0; state.pendingScans = []; state.scannedSkuList = []; state.preparedOrderCode = null; state.skuAliasMap.clear(); state.orderTrackingMap.clear(); state.skuTrackingMap.clear(); state.trackingStartTimes.clear(); state.lastConfirmSignature = null; logStep('qy', '重置驱动状态'); }; const start = () => { if (state.active) return; logStep('qy', '启动 API 驱动'); state.active = true; qyApiResponseHandler = handleApiResponse; installApiInterceptor(); document.addEventListener('keydown', handleSkuInputKeydown, true); updateMonitorData({ isTargetPage: true, url: window.location.href, batchNo: state.currentOrderCode || '', sku: state.lastScanSku || '', trackingNo: state.lastScanTracking || '' }); }; const stop = () => { logStep('qy', '停止 API 驱动'); state.active = false; if (qyApiResponseHandler === handleApiResponse) { qyApiResponseHandler = null; } document.removeEventListener('keydown', handleSkuInputKeydown, true); resetState(); }; const getStatus = () => ({ isTargetPage: true, url: window.location.href, batchNo: state.currentOrderCode || '', sku: state.lastScanSku || '', trackingNo: state.lastScanTracking || '' }); const forceEndOrder = () => { if (!state.lastScanTracking || state.pendingScans.length === 0) { logStep('qy', '手动结束订单: 无待提交数据'); return; } logStep('qy', '手动结束订单', { tracking: state.lastScanTracking, orderCode: state.currentOrderCode, count: state.pendingScans.length }); void flushPendingScans('manual_force_end'); }; return { name: 'qy', match, start, stop, refresh: () => { updateMonitorData({ isTargetPage: true, url: window.location.href, batchNo: state.currentOrderCode || '', sku: state.lastScanSku || '', trackingNo: state.lastScanTracking || '' }); }, getStatus, forceEndOrder }; }; // ============================================================ // 驱动管理与初始化 // ============================================================ const DRIVERS = [createQyDriver()]; const stopActiveDriver = () => { if (activeDriver) { logStep('core', '停止当前驱动', { name: activeDriver.name }); activeDriver.stop(); } activeDriver = null; activeDriverName = ''; }; const refreshDriver = async () => { logStep('core', '刷新驱动'); const token = ++refreshToken; const loginReady = await ensureLogin(); if (token !== refreshToken) return; if (!loginReady) { stopActiveDriver(); logStep('core', '未登录,停止驱动'); updateMonitorData({ isTargetPage: false, needLogin: true, url: window.location.href, batchNo: '', sku: '', trackingNo: '' }); return; } const nextDriver = DRIVERS.find((driver) => driver.match()); if (!nextDriver) { stopActiveDriver(); logStep('core', '未匹配到驱动'); updateMonitorData({ isTargetPage: false, needLogin: false, url: window.location.href, batchNo: '', sku: '', trackingNo: '' }); applyOsdMinimized(true); return; } if (!activeDriver || activeDriver.name !== nextDriver.name) { stopActiveDriver(); activeDriver = nextDriver; activeDriverName = nextDriver.name; logStep('core', '切换驱动', { name: activeDriverName }); activeDriver.start(); } setOsdVisible(true); activeDriver.refresh?.(); logStep('core', '驱动刷新完成', { name: activeDriverName }); }; const watchUrlChange = () => { logStep('core', '启动 URL 监听'); let lastUrl = window.location.href; setInterval(() => { const currentUrl = window.location.href; if (currentUrl !== lastUrl) { lastUrl = currentUrl; logStep('core', 'URL 变化', { url: currentUrl }); refreshDriver(); } }, 500); window.addEventListener('popstate', refreshDriver); window.addEventListener('hashchange', refreshDriver); }; const init = () => { logStep('core', '初始化'); initOsd(); refreshDriver(); watchUrlChange(); }; if (IS_TOP) { window.addEventListener('message', (event) => { if (event.origin !== location.origin) return; const payload = event.data?.payload; if (event.data?.type === 'HCC_WMS_MONITOR_UPDATE') { updateOsdData(payload); return; } if (event.data?.type === 'HCC_WMS_UPLOAD_STATUS') { updateOsdStatus(payload); } }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } const registerTampermonkeyMenu = () => { if (!IS_TOP || typeof GM_registerMenuCommand !== 'function') return; unregisterMenuCommands(); registerMenuCommand('HCC: 强制结束当前订单', async () => { if (activeDriver && typeof activeDriver.forceEndOrder === 'function') { logStep('core', '菜单触发手动结束订单', { driver: activeDriver.name }); activeDriver.forceEndOrder(); alert('已触发结束订单'); return; } logStep('core', '菜单结束订单失败: 当前驱动不支持', { driver: activeDriver?.name }); alert('当前页面不支持结束订单'); }); registerMenuCommand('HCC: 清空登录并刷新', async () => { await Storage.remove([ 'isLoggedIn', 'userId', 'jwtToken', 'userData', 'loginTime', 'currentStationId', 'currentStationData' ]); location.reload(); }); registerMenuCommand('HCC: 清空工位并刷新', async () => { await Storage.remove(['currentStationId', 'currentStationData']); location.reload(); }); }; registerTampermonkeyMenu(); })();