// ==UserScript== // @name 货查查助手-领星 WMS // @namespace https://hcc321.com/ // @version 1.0.0 // @description 领星 WMS 复核验货抓单上传(一单一件/一单多件),保留登录、工位、监控与上传能力 // @author HCCvision // @match https://*.xlwms.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 // @license All Rights Reserved // ==/UserScript== /** * Pack Monitor Script * 领星 WMS 复核验货/包裹复核抓单上传 - API 拦截方案 * 通过拦截 fetch/XHR 获取 /review/* 复核接口的请求与响应数据 */ (() => { const HCC_XL_BUILD_ID = '20260811-1650-compact-sku-v12'; if (window.__HCC_XL_WMS_MONITOR__) { console.log(`[HCC XL] skipped build=${HCC_XL_BUILD_ID} existing=${window.__HCC_XL_WMS_MONITOR__}`); return; } window.__HCC_XL_WMS_MONITOR__ = HCC_XL_BUILD_ID; console.log(`[HCC XL] loaded build=${HCC_XL_BUILD_ID}`); document.documentElement.dataset.hccXlBuild = HCC_XL_BUILD_ID; const IS_TOP = window.top === window; const API_BASE_URL = 'https://hcc321.com'; const DEV_STORAGE_PREFIX = '__hcc_xl_debug__:'; const readValue = async (key, defaultValue) => { if (typeof GM_getValue === 'function') { const value = GM_getValue(key); if (value !== undefined) return value; } if (typeof GM !== 'undefined' && typeof GM.getValue === 'function') { const value = await GM.getValue(key); if (value !== undefined) return value; } const devValue = localStorage.getItem(`${DEV_STORAGE_PREFIX}${key}`); if (devValue != null) { try { return JSON.parse(devValue); } catch (error) { return devValue; } } 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); return; } localStorage.setItem(`${DEV_STORAGE_PREFIX}${key}`, JSON.stringify(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); return; } localStorage.removeItem(`${DEV_STORAGE_PREFIX}${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 = { xl: '领星 WMS' }; let currentStationId = null; let activeDriver = null; let activeDriverName = 'xl'; let refreshToken = 0; const osdState = { root: null, panel: null, fab: null, sku: null, orderNo: null, status: null, skuCompact: null, statusCompact: 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, 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 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.sku) return; const sku = data?.sku || ''; const orderNo = data?.batchNo || ''; logStep('osd', '数据更新', { 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.sku, sku); if (osdState.orderNo) setValue(osdState.orderNo, orderNo); 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.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; const existingRoot = document.getElementById('hcc-wms-osd-root'); if (existingRoot) existingRoot.remove(); logStep('osd', '初始化 OSD'); const root = document.createElement('div'); root.id = 'hcc-wms-osd-root'; root.dataset.hccXlBuild = HCC_XL_BUILD_ID; 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.sku = shadow.getElementById('osd-sku'); osdState.skuCompact = shadow.getElementById('osd-sku-compact'); osdState.orderNo = shadow.getElementById('osd-order'); osdState.status = shadow.getElementById('osd-status'); osdState.statusCompact = shadow.getElementById('osd-status-compact'); 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.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 }); }); 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.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']); document.documentElement.dataset.hccXlAuth = !loginData.isLoggedIn ? 'missing-login' : (!loginData.currentStationId ? 'missing-station' : 'ready'); if (!loginData.isLoggedIn || !loginData.currentStationId) { currentStationId = null; logStep('auth', '未登录或未选择工位'); return false; } currentStationId = loginData.currentStationId; logStep('auth', '登录有效', { stationId: currentStationId }); return true; }; // ============================================================ // 领星复核核心(保持为纯函数,供 Node 测试提取) // ============================================================ /* HCC_XL_CORE_START */ const createXlCore = () => { const API_PATHS = [ '/review/get4OnePiece', '/review/onePiece', '/review/onePieceByProductSn', '/review/onePieceByProductSnValid', '/review/getCustomerCode4More', '/review/get4MorePiece', '/review/morePiece', '/review/getSnEnabled4MorePieceByBarcode', '/review/morePieceWithProductSn', '/review/morePieceWithProductSnValid', '/review/package/scanWave', '/review/package/scanBarcode', '/review/package/reviewByProductSnValid', '/review/package/reset' ]; const text = (value) => String(value == null ? '' : value).trim(); const array = (value) => Array.isArray(value) ? value : []; const parseJson = (value) => { if (value && typeof value === 'object') return value; if (typeof value !== 'string' || !value.trim()) return null; try { return JSON.parse(value); } catch (error) { return null; } }; const apiPath = (url) => { let pathname = ''; try { pathname = new URL(String(url || ''), 'https://xlwms.invalid').pathname; } catch (error) { return ''; } return API_PATHS.find((path) => pathname.endsWith(path)) || ''; }; const parseRequest = (payload) => { const result = {}; try { const url = new URL(String(payload?.url || ''), 'https://xlwms.invalid'); for (const [key, value] of url.searchParams.entries()) result[key] = value; } catch (error) { // Ignore malformed URLs and continue with the request body. } const body = payload?.requestBody; const json = parseJson(body); if (json && typeof json === 'object' && !Array.isArray(json)) { Object.assign(result, json); } else if (typeof body === 'string' && body) { try { const params = new URLSearchParams(body); for (const [key, value] of params.entries()) result[key] = value; } catch (error) { // Unsupported request body; URL parameters are still usable. } } return result; }; const unwrapResponse = (responseText) => { const envelope = parseJson(responseText); if (!envelope || typeof envelope !== 'object') return null; if (envelope.success === false) return null; if (envelope.code != null && ![0, 200, '0', '200'].includes(envelope.code)) return null; return envelope.data ?? envelope.row ?? envelope.result ?? envelope; }; const normalizeOrder = (value) => { if (!value || typeof value !== 'object') return null; const order = value.delivery || value.order || value.package || value; const trackingNo = text( order.expressNo || order.trackingNo || order.waybillNo || order.logisticsNo ); const deliveryNo = text(order.deliveryNo || order.deliveryId); const sourceNo = text(order.sourceNo || order.orderNo || order.outboundNo); const customerCode = text(order.customerCode); if (!trackingNo && !deliveryNo && !sourceNo) return null; return { trackingNo, deliveryNo, sourceNo, customerCode }; }; const uniqueOrders = (values) => { const seen = new Set(); const result = []; for (const value of values) { const order = normalizeOrder(value); if (!order) continue; const key = [order.trackingNo, order.deliveryNo, order.sourceNo, order.customerCode].join('|'); if (seen.has(key)) continue; seen.add(key); result.push(order); } return result; }; const collectOrders = (data, includeMain = true) => { if (!data || typeof data !== 'object') return []; const candidates = []; if (includeMain) candidates.push(data); for (const key of ['needPrintPackList', 'deliveryList', 'packageList', 'items', 'list']) { candidates.push(...array(data[key])); } return uniqueOrders(candidates); }; const normalizePackage = (value) => { const order = normalizeOrder(value); if (!order) return null; const item = value?.package || value || {}; return { ...order, packNo: text(item.packNo || item.wmsPackNo), reviewStatus: Number(item.reviewStatus), scanQty: Math.max(0, Number(item.scanQty) || 0), allQty: Math.max(0, Number(item.allQty || item.qty) || 0) }; }; const uniquePackages = (values) => { const seen = new Set(); const result = []; for (const value of values) { const item = normalizePackage(value); if (!item) continue; const key = item.packNo || item.deliveryNo || item.sourceNo || item.trackingNo; if (!key || seen.has(key)) continue; seen.add(key); result.push(item); } return result; }; const parseProductSnList = (value) => { if (Array.isArray(value)) return value.map(text).filter(Boolean); const parsed = parseJson(value); if (Array.isArray(parsed)) return parsed.map(text).filter(Boolean); const normalized = text(value); return normalized ? normalized.split(/[\n,]/).map(text).filter(Boolean) : []; }; const scanCodes = (params, repeatQty = false) => { const serials = parseProductSnList(params.productSnList); if (serials.length) return serials; const productSn = text(params.productSn); if (productSn) return [productSn]; const barcode = text(params.barcode); if (!barcode) return []; const qty = repeatQty ? Math.max(1, Math.floor(Number(params.qty) || 1)) : 1; return Array.from({ length: qty }, () => barcode); }; const lookupCandidates = (data) => { if (!data || typeof data !== 'object') return []; return uniqueOrders([ data.delivery, ...array(data.express), ...array(data.outBoundList), ...array(data.deliveryList), ...array(data.list) ]); }; const parseApiEvent = (payload) => { const path = apiPath(payload?.url); if (!path) return null; const params = parseRequest(payload); const data = unwrapResponse(payload?.responseText); if (!data) return null; const occurredAt = new Date(Number(payload?.timestamp) || Date.now()).toISOString(); if (path === '/review/get4OnePiece') { return { type: 'oneContext', waveNo: text(params.waveNo || data.waveNo), orders: uniqueOrders([ ...collectOrders(data), ...array(data.skuList), ...array(data.orderList) ]), occurredAt }; } if (path === '/review/onePiece' || path === '/review/onePieceByProductSn') { const waveStatus = Number(data.waveStatus); if (waveStatus !== 0 && waveStatus !== 2) return null; return { type: 'oneComplete', waveNo: text(params.waveNo || data.waveNo), waveStatus, orders: collectOrders(data), deliveryNos: array(data.deliveryNos).map(text).filter(Boolean), scanCodes: scanCodes(params), batchSize: Math.max(1, Math.floor(Number(params.batchSize) || 1)), occurredAt }; } if (path === '/review/getCustomerCode4More') { return { type: 'multiLookup', inputNo: text(params.deliveryNo), candidates: lookupCandidates(data), occurredAt }; } if (path === '/review/get4MorePiece') { return { type: 'multiContext', order: normalizeOrder(data), requestedDeliveryNo: text(params.deliveryNo), customerCode: text(params.customerCode || data.customerCode), occurredAt }; } if (path === '/review/morePiece' || path === '/review/morePieceWithProductSn') { const orderStatus = Number(data.orderStatus); if (orderStatus !== 2 && orderStatus !== 4) return null; return { type: 'multiScan', orderStatus, order: normalizeOrder(data), requestedDeliveryNo: text(params.deliveryNo), customerCode: text(params.customerCode || data.customerCode), scanCodes: scanCodes(params, true), occurredAt }; } if (path === '/review/package/scanWave') { return { type: 'boxContext', waveNo: text(params.waveNo || data.waveNo), isRefresh: params.waveStatus != null, waveStatus: Number(params.waveStatus ?? data.waveStatus), packages: uniquePackages(array(data.packageList)), occurredAt }; } if (path === '/review/package/scanBarcode') { // The first response only opens the serial-number dialog. The second // scanBarcode request (with productSn) is the actual review event. if (data.resultStatus && !text(params.productSn)) return null; const primary = normalizePackage({ ...data, ...array(data.skuList)[0] }); const batchPackages = uniquePackages([ ...array(data.needPrintPackList), ...array(array(data.skuList)[0]?.needPrintPackList) ]); return { type: 'boxScan', waveNo: text(params.waveNo || data.waveNo), waveStatus: Number(data.waveStatus), primary, batchPackages, scanCodes: scanCodes(params), occurredAt }; } if (path === '/review/package/reset') { return { type: 'boxReset', waveNo: text(params.waveNo), occurredAt }; } // Product-SN validation endpoints only prepare the real scan request. return null; }; const signature = (payload) => { const raw = [ apiPath(payload?.url), text(payload?.method), text(payload?.requestBody), text(payload?.responseText) ].join('\n'); let hash = 2166136261; for (let index = 0; index < raw.length; index += 1) { hash ^= raw.charCodeAt(index); hash = Math.imul(hash, 16777619); } return `${raw.length}:${hash >>> 0}`; }; const expandMultiScans = (params) => scanCodes(params, true); const sameOrder = (left, right, requestedDeliveryNo = '') => { const leftKeys = [left?.deliveryNo, left?.sourceNo, left?.trackingNo] .map(text) .filter(Boolean); const rightKeys = [right?.deliveryNo, right?.sourceNo, right?.trackingNo, requestedDeliveryNo] .map(text) .filter(Boolean); return leftKeys.some((key) => rightKeys.includes(key)); }; return { API_PATHS: API_PATHS.slice(), apiPath, parseRequest, unwrapResponse, normalizeOrder, normalizePackage, parseApiEvent, expandMultiScans, sameOrder, signature }; }; /* HCC_XL_CORE_END */ // ============================================================ // 领星页面 API 拦截桥 // ============================================================ const xlCore = createXlCore(); const XL_REVIEW_BRIDGE_MESSAGE = 'HCC_XL_REVIEW_API_BRIDGE'; const XL_REVIEW_LISTENER_FLAG = '__HCC_XL_REVIEW_LISTENER_INSTALLED__'; const XL_REVIEW_INSTALL_FLAG = '__HCC_XL_REVIEW_INSTALL_REQUESTED__'; const XL_REVIEW_PAGE_PATCH_FLAG = '__HCC_XL_REVIEW_PAGE_PATCHED__'; const XL_REVIEW_SCRIPT_ID = 'hcc-xl-review-api-bridge'; let xlReviewResponseHandler = null; const installXlBridgeListener = () => { if (window[XL_REVIEW_LISTENER_FLAG]) return; window[XL_REVIEW_LISTENER_FLAG] = true; window.addEventListener('message', (event) => { if (event.origin !== location.origin) return; if (event.data?.type !== XL_REVIEW_BRIDGE_MESSAGE) return; if (typeof xlReviewResponseHandler === 'function') { xlReviewResponseHandler(event.data.payload || {}); } }); }; const buildXlBridgeSource = () => { const messageType = JSON.stringify(XL_REVIEW_BRIDGE_MESSAGE); const pageFlag = JSON.stringify(XL_REVIEW_PAGE_PATCH_FLAG); const apiPaths = JSON.stringify(xlCore.API_PATHS); return ` (function() { if (window[${pageFlag}]) return; window[${pageFlag}] = true; var API_PATHS = ${apiPaths}; function observedPath(url) { try { var pathname = new URL(String(url || ''), window.location.origin).pathname; return API_PATHS.some(function(path) { return pathname.endsWith(path); }); } catch (error) { return false; } } function serialize(value) { if (typeof value === 'string') return value; if (value instanceof URLSearchParams) return value.toString(); if (value instanceof FormData) { try { return JSON.stringify(Array.from(value.entries())); } catch (error) { return ''; } } if (value == null) return ''; try { return JSON.stringify(value); } catch (error) { return String(value); } } function publish(detail) { detail.timestamp = Date.now(); window.postMessage({ type: ${messageType}, payload: detail }, window.location.origin); } var originalFetch = window.fetch; if (typeof originalFetch === 'function') { window.fetch = async function() { var args = Array.prototype.slice.call(arguments); var response = await originalFetch.apply(this, args); try { var resource = args[0]; var init = args[1] || {}; var requestUrl = typeof resource === 'string' ? resource : (resource && resource.url) || ''; if (observedPath(requestUrl)) { publish({ url: String(requestUrl), method: String(init.method || (resource && resource.method) || 'GET').toUpperCase(), requestBody: serialize(init.body), responseText: await response.clone().text(), transport: 'fetch' }); } } catch (error) { console.error('[XL] fetch bridge failed', error); } return response; }; } var originalOpen = XMLHttpRequest.prototype.open; var originalSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function(method, url) { this.__hccXlMethod = method; this.__hccXlUrl = url; return originalOpen.apply(this, arguments); }; XMLHttpRequest.prototype.send = function(body) { this.__hccXlBody = body; if (!this.__hccXlListenerBound) { this.__hccXlListenerBound = true; this.addEventListener('loadend', function() { try { if (!observedPath(this.__hccXlUrl)) return; var responseText = ''; if (!this.responseType || this.responseType === 'text') responseText = this.responseText || ''; else responseText = serialize(this.response); publish({ url: String(this.__hccXlUrl || ''), method: String(this.__hccXlMethod || 'GET').toUpperCase(), requestBody: serialize(this.__hccXlBody), responseText: responseText, transport: 'xhr' }); } catch (error) { console.error('[XL] xhr bridge failed', error); } }); } return originalSend.apply(this, arguments); }; })(); `; }; const installXlApiInterceptor = () => { if (window[XL_REVIEW_INSTALL_FLAG]) return; window[XL_REVIEW_INSTALL_FLAG] = true; installXlBridgeListener(); const script = document.createElement('script'); script.id = XL_REVIEW_SCRIPT_ID; script.textContent = buildXlBridgeSource(); const parent = document.head || document.documentElement || document.body; parent?.appendChild(script); script.remove(); document.documentElement.dataset.hccXlBridgeRequested = HCC_XL_BUILD_ID; logStep('xl', '领星复核 API 拦截器已注入'); }; // ============================================================ // 领星 WMS 驱动 // ============================================================ const createXlDriver = () => { const state = { active: false, waveNo: '', oneStartedAt: null, lastOneEndedAt: null, oneOrders: new Map(), lookupOrders: new Map(), multiOrder: null, multiScans: [], lastOrderNo: '', boxWaveNo: '', boxPackages: new Map(), boxScans: new Map(), boxStartedAt: new Map(), lastScanSku: '', seenSignatures: new Map(), completedTrackingNos: new Set(), processingQueue: Promise.resolve() }; const match = () => ( /(^|\.)xlwms\.com$/i.test(window.location.hostname) && /^\/wms\/outbound\/(?:review|boxReview)(?:\/|$)/.test(window.location.pathname) ); const rememberOrder = (map, order) => { if (!order) return; for (const key of [order.deliveryNo, order.sourceNo]) { if (key) map.set(key, order); } }; const uploadIdFor = (order, fallback = '') => ( order?.trackingNo || order?.sourceNo || order?.deliveryNo || fallback || '' ); const trackingFor = (order, fallbackMap, requestedDeliveryNo = '') => { if (order?.trackingNo) return order.trackingNo; for (const key of [order?.deliveryNo, order?.sourceNo, requestedDeliveryNo]) { if (key && fallbackMap.has(key)) return uploadIdFor(fallbackMap.get(key)); } return order?.sourceNo || order?.deliveryNo || requestedDeliveryNo || ''; }; const packageKey = (item) => ( item?.packNo || item?.deliveryNo || item?.sourceNo || item?.trackingNo || '' ); const trimSeen = () => { if (state.seenSignatures.size <= 500) return; const oldest = state.seenSignatures.keys().next().value; state.seenSignatures.delete(oldest); }; const trimCompleted = () => { if (state.completedTrackingNos.size <= 1000) return; const oldest = state.completedTrackingNos.values().next().value; state.completedTrackingNos.delete(oldest); }; const asDate = (value) => { const date = new Date(value || Date.now()); return Number.isNaN(date.getTime()) ? new Date() : date; }; const uploadCompletedOrder = async ({ trackingNo, batchNo, scans, startTime, endTime, reason }) => { state.lastOrderNo = batchNo || state.lastOrderNo; if (scans.length) state.lastScanSku = scans[scans.length - 1].sku; updateMonitorData({ isTargetPage: true, url: window.location.href, batchNo: batchNo || '', sku: state.lastScanSku, trackingNo: trackingNo || '' }); if (!trackingNo) { setUploadStatus('skipped', '未获取到物流跟踪号或出库单号,跳过上传'); logStep('xl', '缺少物流跟踪号和出库单号,跳过订单', { batchNo, reason }); return; } if (state.completedTrackingNos.has(trackingNo)) { logStep('xl', '订单已处理,忽略重复完成事件', { trackingNo, reason }); return; } if (!scans.length) { setUploadStatus('skipped', '未记录到商品扫描,跳过上传'); return; } state.completedTrackingNos.add(trackingNo); trimCompleted(); await finalizeAndUploadScans({ trackingNo, scans, startTime, endTime, reason }); }; const handleOneContext = (event) => { state.waveNo = event.waveNo; state.oneStartedAt = asDate(event.occurredAt); state.lastOneEndedAt = null; state.oneOrders.clear(); for (const order of event.orders) rememberOrder(state.oneOrders, order); setUploadStatus('idle', '等待扫描商品'); updateMonitorData({ isTargetPage: true, url: window.location.href, batchNo: '', sku: '', trackingNo: '' }); }; const handleOneComplete = async (event) => { const completedAt = asDate(event.occurredAt); let orders = event.orders.slice(); if (!orders.length && event.deliveryNos.length) { orders = event.deliveryNos.map((deliveryNo) => state.oneOrders.get(deliveryNo)).filter(Boolean); } if (!orders.length) orders = [null]; const unique = new Map(); for (const order of orders) { const trackingNo = trackingFor(order, state.oneOrders); const key = trackingNo || order?.deliveryNo || order?.sourceNo || ''; if (key && !unique.has(key)) unique.set(key, { order, trackingNo }); } if (!unique.size) unique.set('missing', { order: orders[0], trackingNo: '' }); const scanCode = event.scanCodes[0] || ''; let cursor = state.lastOneEndedAt || state.oneStartedAt || completedAt; for (const { order, trackingNo } of unique.values()) { const scans = scanCode ? [{ sku: scanCode, scanId: scanCode, time: completedAt }] : []; await uploadCompletedOrder({ trackingNo, batchNo: order?.sourceNo || order?.deliveryNo || event.waveNo || state.waveNo, scans, startTime: cursor, endTime: completedAt, reason: 'xl_one_piece_complete' }); cursor = completedAt; } state.lastOneEndedAt = completedAt; }; const handleMultiLookup = (event) => { state.lookupOrders.clear(); for (const order of event.candidates) rememberOrder(state.lookupOrders, order); }; const handleMultiContext = (event) => { const fallback = state.lookupOrders.get(event.requestedDeliveryNo); const order = { ...(fallback || {}), ...(event.order || {}) }; order.trackingNo = trackingFor(order, state.lookupOrders, event.requestedDeliveryNo); order.deliveryNo = order.deliveryNo || event.requestedDeliveryNo; order.customerCode = order.customerCode || event.customerCode; if (state.multiOrder && xlCore.sameOrder(state.multiOrder, order, event.requestedDeliveryNo)) { const startedAt = state.multiOrder.startedAt; state.multiOrder = { ...state.multiOrder, ...order, startedAt }; setUploadStatus('collecting', `已累计 ${state.multiScans.length} 件`); updateMonitorData({ isTargetPage: true, url: window.location.href, batchNo: state.multiOrder.sourceNo || state.multiOrder.deliveryNo || '', sku: state.lastScanSku, trackingNo: state.multiOrder.trackingNo || '' }); return; } if (state.multiOrder && state.multiScans.length) { logStep('xl', '新订单覆盖未完成批次', { previous: state.multiOrder.sourceNo || state.multiOrder.deliveryNo, count: state.multiScans.length }); setUploadStatus('skipped', '上一单未完成,已丢弃未上传扫描'); } state.multiOrder = { ...order, startedAt: asDate(event.occurredAt) }; state.multiScans = []; setUploadStatus('collecting', '已累计 0 件'); updateMonitorData({ isTargetPage: true, url: window.location.href, batchNo: order.sourceNo || order.deliveryNo || '', sku: '', trackingNo: order.trackingNo || '' }); }; const handleMultiScan = async (event) => { if (!state.multiOrder) { const fallback = state.lookupOrders.get(event.requestedDeliveryNo); const order = { ...(fallback || {}), ...(event.order || {}) }; order.trackingNo = trackingFor(order, state.lookupOrders, event.requestedDeliveryNo); order.deliveryNo = order.deliveryNo || event.requestedDeliveryNo; state.multiOrder = { ...order, startedAt: asDate(event.occurredAt) }; } const occurredAt = asDate(event.occurredAt); for (const code of event.scanCodes) { state.multiScans.push({ sku: code, scanId: code, time: occurredAt }); } state.lastScanSku = event.scanCodes[event.scanCodes.length - 1] || state.lastScanSku; setUploadStatus('collecting', `已累计 ${state.multiScans.length} 件`); if (event.orderStatus !== 4) { updateMonitorData({ isTargetPage: true, url: window.location.href, batchNo: state.multiOrder.sourceNo || state.multiOrder.deliveryNo || '', sku: state.lastScanSku, trackingNo: state.multiOrder.trackingNo || '' }); return; } const trackingNo = trackingFor(event.order, state.lookupOrders, event.requestedDeliveryNo) || state.multiOrder.trackingNo; const orderSnapshot = state.multiOrder; const scansSnapshot = state.multiScans.slice(); state.multiOrder = null; state.multiScans = []; await uploadCompletedOrder({ trackingNo, batchNo: orderSnapshot.sourceNo || orderSnapshot.deliveryNo, scans: scansSnapshot, startTime: orderSnapshot.startedAt, endTime: occurredAt, reason: 'xl_more_piece_complete' }); }; const clearBoxState = () => { state.boxWaveNo = ''; state.boxPackages.clear(); state.boxScans.clear(); state.boxStartedAt.clear(); state.lastOrderNo = ''; }; const handleBoxContext = (event) => { if (event.isRefresh && event.waveStatus === 40) { clearBoxState(); return; } if (!event.isRefresh || (state.boxWaveNo && state.boxWaveNo !== event.waveNo)) { clearBoxState(); } state.boxWaveNo = event.waveNo; const startedAt = asDate(event.occurredAt); for (const item of event.packages) { const key = packageKey(item); if (!key) continue; state.boxPackages.set(key, { ...(state.boxPackages.get(key) || {}), ...item }); if (!state.boxStartedAt.has(key)) state.boxStartedAt.set(key, startedAt); } // Lingxing refreshes scanWave after every barcode. Keep the latest // barcode/order/status visible instead of resetting the OSD to wave data. if (event.isRefresh) return; setUploadStatus('idle', `包裹复核:等待扫描(${event.packages.length} 包裹)`); updateMonitorData({ isTargetPage: true, url: window.location.href, batchNo: '', sku: '', trackingNo: '' }); }; const mergedBoxPackage = (item) => { const key = packageKey(item); const prior = state.boxPackages.get(key) || {}; const merged = { ...prior, ...(item || {}) }; if (key) state.boxPackages.set(key, merged); return merged; }; const uploadBoxPackage = async (item, scans, occurredAt, reason) => { const order = mergedBoxPackage(item); const key = packageKey(order); const startedAt = state.boxStartedAt.get(key) || occurredAt; await uploadCompletedOrder({ trackingNo: uploadIdFor(order), batchNo: order.sourceNo || order.deliveryNo || state.boxWaveNo, scans, startTime: startedAt, endTime: occurredAt, reason }); if (key) { state.boxScans.delete(key); state.boxStartedAt.delete(key); } }; const handleBoxScan = async (event) => { const occurredAt = asDate(event.occurredAt); const primary = mergedBoxPackage(event.primary); const primaryKey = packageKey(primary); if (primaryKey && !state.boxStartedAt.has(primaryKey)) { state.boxStartedAt.set(primaryKey, occurredAt); } if (primaryKey && event.scanCodes.length) { const scans = state.boxScans.get(primaryKey) || []; for (const code of event.scanCodes) { scans.push({ sku: code, scanId: code, time: occurredAt }); } state.boxScans.set(primaryKey, scans); } state.lastScanSku = event.scanCodes[event.scanCodes.length - 1] || state.lastScanSku; state.lastOrderNo = primary?.sourceNo || primary?.deliveryNo || state.lastOrderNo; const primaryScans = primaryKey ? (state.boxScans.get(primaryKey) || []) : []; let missingTrackingOnComplete = false; if (primary?.reviewStatus === 1) { missingTrackingOnComplete = !primary.trackingNo; await uploadBoxPackage(primary, primaryScans, occurredAt, 'xl_box_review_complete'); } // Automatic same-SKU package review completes each listed one-item // package with the same physical scan. const batchScanCode = event.scanCodes[0] || ''; for (const item of event.batchPackages) { if (item.reviewStatus !== 1 || packageKey(item) === primaryKey) continue; if (!item.trackingNo) missingTrackingOnComplete = true; const scans = batchScanCode ? [{ sku: batchScanCode, scanId: batchScanCode, time: occurredAt }] : []; await uploadBoxPackage(item, scans, occurredAt, 'xl_box_review_batch_complete'); } const collected = Array.from(state.boxScans.values()).reduce((sum, scans) => sum + scans.length, 0); if (collected > 0) setUploadStatus('collecting', `包裹复核:已累计 ${collected} 件`); else if (!missingTrackingOnComplete) setUploadStatus('idle', '包裹复核:等待下一件'); updateMonitorData({ isTargetPage: true, url: window.location.href, batchNo: primary?.sourceNo || primary?.deliveryNo || event.waveNo || state.boxWaveNo, sku: state.lastScanSku, trackingNo: primary?.trackingNo || '' }); if (event.waveStatus === 40) clearBoxState(); }; const processPayload = async (payload) => { const signature = xlCore.signature(payload); const occurredAt = Number(payload?.timestamp) || Date.now(); const previousAt = state.seenSignatures.get(signature); if (previousAt != null && Math.abs(occurredAt - previousAt) <= 100) { logStep('xl', '忽略重复接口事件', { path: xlCore.apiPath(payload.url) }); return; } state.seenSignatures.set(signature, occurredAt); trimSeen(); const event = xlCore.parseApiEvent(payload); if (!event) return; logStep('xl', '解析领星复核事件', { type: event.type }); if (event.type === 'oneContext') handleOneContext(event); else if (event.type === 'oneComplete') await handleOneComplete(event); else if (event.type === 'multiLookup') handleMultiLookup(event); else if (event.type === 'multiContext') handleMultiContext(event); else if (event.type === 'multiScan') await handleMultiScan(event); else if (event.type === 'boxContext') handleBoxContext(event); else if (event.type === 'boxScan') await handleBoxScan(event); else if (event.type === 'boxReset') clearBoxState(); }; const handleApiResponse = (payload) => { document.documentElement.dataset.hccXlLastApi = xlCore.apiPath(payload?.url) || ''; state.processingQueue = state.processingQueue .then(() => processPayload(payload)) .catch((error) => { console.error('[XL] 处理复核接口失败', error); setUploadStatus('error', '领星抓单解析失败'); }); }; const resetState = () => { state.waveNo = ''; state.oneStartedAt = null; state.lastOneEndedAt = null; state.oneOrders.clear(); state.lookupOrders.clear(); state.multiOrder = null; state.multiScans = []; state.lastOrderNo = ''; clearBoxState(); state.lastScanSku = ''; state.seenSignatures.clear(); state.processingQueue = Promise.resolve(); }; const start = () => { if (state.active) return; state.active = true; document.documentElement.dataset.hccXlDriver = 'xl'; xlReviewResponseHandler = handleApiResponse; installXlApiInterceptor(); setUploadStatus('idle', '等待扫描'); updateMonitorData({ isTargetPage: true, url: window.location.href, batchNo: '', sku: '', trackingNo: '' }); }; const stop = () => { state.active = false; delete document.documentElement.dataset.hccXlDriver; if (xlReviewResponseHandler === handleApiResponse) xlReviewResponseHandler = null; resetState(); }; const getStatus = () => ({ isTargetPage: true, url: window.location.href, batchNo: state.lastOrderNo || state.multiOrder?.sourceNo || state.multiOrder?.deliveryNo || '', sku: state.lastScanSku, trackingNo: state.multiOrder?.trackingNo || '' }); return { name: 'xl', match, start, stop, refresh: () => updateMonitorData(getStatus()), getStatus }; }; // ============================================================ // 驱动管理与初始化 // ============================================================ const DRIVERS = [createXlDriver()]; 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', '初始化'); document.documentElement.dataset.hccXlPhase = 'init'; initOsd(); // Install the page-world bridge independently from HCC authentication so // early Lingxing requests cannot be missed while login/station state loads. installXlApiInterceptor(); 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 () => { await Storage.remove([ 'isLoggedIn', 'userId', 'jwtToken', 'userData', 'loginTime', 'currentStationId', 'currentStationData' ]); location.reload(); }); registerMenuCommand('HCC: 清空工位并刷新', async () => { await Storage.remove(['currentStationId', 'currentStationData']); location.reload(); }); }; registerTampermonkeyMenu(); })();