// ==UserScript== // @name 货查查助手(Tampermonkey) // @namespace https://hcc321.com/ // @version 2.0.0 // @description 多 WMS 抓取与上传(PB / QY / JF),含登录、工位、监控与上传能力 // @author HCCvision // @match https://cnwms.aiyacang.com/* // @match https://wms.aiyacang.com/* // @match http://wms.aiyacang.com/* // @match https://gwmsth.best-inc.com/* // @match http://gwmsth.best-inc.com/* // @match https://oms.prorclub.com/* // @match http://oms.prorclub.com/* // @match https://*.jfwms.com/* // @match http://*.jfwms.com/* // @grant GM_getValue // @grant GM_setValue // @grant GM_deleteValue // @grant GM_listValues // @grant GM_xmlhttpRequest // @grant GM_registerMenuCommand // @connect hcc321.com // @run-at document-start // ==/UserScript== /** * Pack Monitor - Tampermonkey Script * 多 WMS 抓取与上传(PB / QY / JF) */ (() => { if (window.__HCC_MULTI_WMS_MONITOR__) { return; } window.__HCC_MULTI_WMS_MONITOR__ = true; const IS_TOP = window.top === window; const API_BASE_URL = 'https://hcc321.com'; const gmApi = { getValue: 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; }, setValue: 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); } }, deleteValue: async (key) => { if (typeof GM_deleteValue === 'function') { GM_deleteValue(key); return; } if (typeof GM !== 'undefined' && typeof GM.deleteValue === 'function') { await GM.deleteValue(key); } }, listValues: async () => { if (typeof GM_listValues === 'function') { return GM_listValues(); } if (typeof GM !== 'undefined' && typeof GM.listValues === 'function') { return GM.listValues(); } return []; } }; const createStorageArea = () => { const listeners = new Set(); const emitChanges = (changes) => { for (const listener of listeners) { try { listener(changes, 'local'); } catch (error) { console.error('[TM storage.onChanged] listener error:', error); } } }; const normalizeGetInput = async (keys) => { const out = {}; if (keys == null) { const allKeys = await gmApi.listValues(); for (const key of allKeys) { out[key] = await gmApi.getValue(key); } return out; } if (Array.isArray(keys)) { for (const key of keys) { out[key] = await gmApi.getValue(key); } return out; } if (typeof keys === 'string') { out[keys] = await gmApi.getValue(keys); return out; } if (typeof keys === 'object') { for (const [key, defaultValue] of Object.entries(keys)) { out[key] = await gmApi.getValue(key, defaultValue); } } return out; }; return { async get(keys, callback) { const result = await normalizeGetInput(keys); if (typeof callback === 'function') { callback(result); } return result; }, async set(items) { const changes = {}; for (const [key, value] of Object.entries(items || {})) { const oldValue = await gmApi.getValue(key); await gmApi.setValue(key, value); changes[key] = { oldValue, newValue: value }; } if (Object.keys(changes).length > 0) { emitChanges(changes); } }, async remove(keys) { const keyList = Array.isArray(keys) ? keys : [keys]; const changes = {}; for (const key of keyList) { const oldValue = await gmApi.getValue(key); await gmApi.deleteValue(key); changes[key] = { oldValue, newValue: undefined }; } if (Object.keys(changes).length > 0) { emitChanges(changes); } } }; }; const createRuntimeApi = () => { const listeners = new Set(); return { sendMessage(message) { let firstResponse; for (const listener of listeners) { let syncResponse; const sendResponse = (payload) => { if (firstResponse === undefined) { firstResponse = payload; } syncResponse = payload; }; try { const maybeResult = listener(message, null, sendResponse); if (firstResponse === undefined && maybeResult !== undefined) { firstResponse = maybeResult; } else if (firstResponse === undefined && syncResponse !== undefined) { firstResponse = syncResponse; } } catch (error) { console.error('[TM runtime.sendMessage] listener error:', error); } } return Promise.resolve(firstResponse); }, onMessage: { addListener(listener) { listeners.add(listener); }, removeListener(listener) { listeners.delete(listener); } } }; }; const chrome = { storage: { local: createStorageArea(), onChanged: { addListener() {}, removeListener() {} } }, runtime: createRuntimeApi() }; 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 platformFetch = 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 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 = { pb: '朋博 WMS', qy: '千易 WMS', jf: '极风 WMS' }; let currentStationId = null; let activeDriver = null; let activeDriverName = ''; 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'); // 时间格式化为 yyyy-MM-dd HH:mm:ss。 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}`); }; // 更新 OSD 上传状态显示。 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 }); }; // 更新 OSD 运单号与 SKU 显示。 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'); } // 显示/隐藏结束订单按钮(有SKU时显示) 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} 更新`; } }; // 切换 OSD 最小化/展开状态。 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 ? '面板紧凑' : '面板展开'); }; // 应用已保存的 OSD 面板位置(支持百分比相对位置)。 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') { // 旧格式:转换为百分比(基于当前视口) const rect = osdState.root.getBoundingClientRect(); const width = rect.width || 270; const height = rect.height || 200; 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 }); }; // 保存 OSD 面板位置到存储(使用百分比相对位置)。 const saveOsdPosition = (pos) => { if (!chrome?.storage?.local) return; 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; } chrome.storage.local.set({ [STORAGE_KEYS.osdPosition]: positionToSave }); logStep('osd', '保存面板位置(百分比)', positionToSave); }; // 启用 OSD 拖拽移动。 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); }); }; // 创建并挂载 OSD 界面。 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
等待中
上传状态
待更新
`; document.documentElement.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 tag = shadow.getElementById('osd-tag'); if (tag && activeDriverName) { tag.textContent = WMS_LABELS[activeDriverName] || 'WMS'; } const minimizeBtn = shadow.getElementById('osd-minimize'); const expandCompactBtn = shadow.getElementById('osd-expand-compact'); const applyMinimizeButtonState = (minimized) => { minimizeBtn.textContent = minimized ? '+' : '—'; minimizeBtn.title = minimized ? '展开' : '最小化'; }; minimizeBtn.addEventListener('click', () => { const isMinimized = osdState.panel?.classList.contains('compact'); const nextMinimized = !isMinimized; applyOsdMinimized(nextMinimized); applyMinimizeButtonState(nextMinimized); chrome.storage.local.set({ [STORAGE_KEYS.osdMinimized]: nextMinimized }); }); expandCompactBtn.addEventListener('click', () => { applyOsdMinimized(false); applyMinimizeButtonState(false); chrome.storage.local.set({ [STORAGE_KEYS.osdMinimized]: false }); }); osdState.fab.addEventListener('click', () => { applyOsdMinimized(false); applyMinimizeButtonState(false); chrome.storage.local.set({ [STORAGE_KEYS.osdMinimized]: false }); }); // 绑定结束订单按钮事件 if (osdState.endOrderBtn) { osdState.endOrderBtn.addEventListener('click', () => { if (activeDriver && typeof activeDriver.forceEndOrder === 'function') { logStep('osd', '手动触发结束订单'); activeDriver.forceEndOrder(); } }); } // ============ OSD 视图切换和登录/工位选择逻辑 ============ // 工位选择状态 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); } }); // 更新面板 class 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 ? '登录中...' : '登 录'; }; // 登录 API 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 platformFetch(`${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 chrome.storage.local.set({ isLoggedIn: true, userId: id, jwtToken: token, userData, loginTime: Date.now() }); showLoginMessage('登录成功!', false); chrome.runtime.sendMessage({ type: 'LOGIN_SUCCESS', data: { userId: id, token, userData } }); 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 chrome.storage.local.remove([ 'isLoggedIn', 'userId', 'jwtToken', 'userData', 'loginTime', 'currentStationId', 'currentStationData' ]); chrome.runtime.sendMessage({ type: 'LOGOUT' }); 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 platformFetch(`${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 chrome.storage.local.set({ currentStationId: station.id, currentStationData: station }); currentStationId = station.id; updateCurrentStationDisplay(station); displayStations((function() { // 重新渲染列表以显示选中状态 return [{ ...station }]; })()); chrome.runtime.sendMessage({ type: 'STATION_SELECTED', data: { stationId: station.id, stationData: 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 chrome.storage.local.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); } } }); } // 绑定复制 SKU 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(); if (chrome?.storage?.local) { chrome.storage.local.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', () => { chrome.storage.local.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); }; // 根据匹配状态显示或隐藏 OSD。 const setOsdVisible = (visible) => { if (!IS_TOP || !osdState.root) return; osdState.root.style.display = visible ? 'block' : 'none'; logStep('osd', visible ? '显示 OSD' : '隐藏 OSD'); const tag = osdState.root.shadowRoot?.getElementById('osd-tag'); if (tag) { tag.textContent = WMS_LABELS[activeDriverName] || 'WMS'; } }; // 保存上传状态供弹窗/OSD 使用。 const setUploadStatus = (status, message) => { const payload = { status, message, timestamp: Date.now() }; chrome.storage.local.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); }; // 从扩展存储读取 JWT。 const getJwtToken = async () => { logStep('auth', '读取 JWT'); const data = await chrome.storage.local.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 platformFetch(`${API_BASE_URL}/goods/modeOneAddWithSubGoods`, { method: 'POST', headers, body: JSON.stringify(record) }); 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 }); // 构建 subGoodsList 列表。 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); }; // 保存监控数据供弹窗/OSD 使用。 const updateMonitorData = (data) => { logStep('monitor', '更新监控数据', { trackingNo: data?.trackingNo, sku: data?.sku }); const payload = { ...data, wmsType: activeDriverName, wmsLabel: WMS_LABELS[activeDriverName] || '', timestamp: Date.now() }; chrome.storage.local.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 chrome.storage.local.get(['isLoggedIn', 'currentStationId']); if (!loginData.isLoggedIn || !loginData.currentStationId) { currentStationId = null; logStep('auth', '未登录或未选择工位'); return false; } currentStationId = loginData.currentStationId; logStep('auth', '登录有效', { stationId: currentStationId }); return true; }; // 创建 PB(朋博)页面驱动。 const createPbDriver = () => { const TABLE_IDS = { scanBody: 'scanData1', scanCountBody: 'scanData' }; const SECTION_IDS = { skuBody: 'SkuBody' }; const STATE = { tracking: '', sku: '', initialized: false }; const SCAN_STATE = { pendingScans: [], activeStartTime: null, activeTracking: '', expectedSkuCounts: new Map(), scanCountBySku: new Map(), scanTableSnapshot: new Map(), lastScanTableSnapshot: '', lastScanCountSnapshot: '', skuBodyDebounceTimer: null, lastSkuBodySnapshot: '' }; let pageObserver = null; let scanObserver = null; let scanObserverTarget = null; let scanCountObserver = null; let scanCountObserverTarget = null; let skuObserver = null; let skuObserverTarget = null; // 判断是否匹配当前驱动页面。 const match = () => window.location.hostname === 'oms.prorclub.com'; // 从 SkuBody 解析 SKU 计数。 const buildExpectedSkuMap = (body) => { const nextMap = new Map(); if (!body) return nextMap; body.querySelectorAll('div.label').forEach((label) => { const text = normalizeText(label.textContent); if (!text) return; const matchText = text.match(/([0-9A-Za-z._-]+)\s*X\s*(\d+)/); if (!matchText) return; const sku = matchText[1]; const qty = parseInt(matchText[2], 10); if (!Number.isFinite(qty)) return; nextMap.set(sku, qty); }); return nextMap; }; // 刷新 SKU 计数映射。 const updateExpectedSkus = () => { const body = document.getElementById(SECTION_IDS.skuBody); const nextMap = buildExpectedSkuMap(body); SCAN_STATE.expectedSkuCounts = nextMap; logStep('pb', '刷新 SKU 计数映射', { size: nextMap.size }); return Boolean(body); }; // 解析 scanData 表为 SKU 扫描数汇总。 const buildScanCountMap = (body) => { const nextMap = new Map(); if (!body) return nextMap; body.querySelectorAll('tr').forEach((row) => { const cells = row.querySelectorAll('td'); if (cells.length < 6) return; const sku = normalizeText(cells[2]?.textContent || ''); if (!sku) return; const scanCountText = normalizeText(cells[5]?.textContent || ''); const scanCountValue = parseInt(scanCountText, 10); const scanCount = Number.isFinite(scanCountValue) ? scanCountValue : 0; const prev = nextMap.get(sku) || 0; nextMap.set(sku, prev + scanCount); }); return nextMap; }; // 刷新 scanData 扫描数缓存。 const updateScanCounts = () => { const body = document.getElementById(TABLE_IDS.scanCountBody); const nextMap = buildScanCountMap(body); SCAN_STATE.scanCountBySku = nextMap; return Boolean(body); }; // 批次变化时重置扫描状态。 const resetScanState = (reason) => { logStep('pb', '重置扫描状态', { reason }); SCAN_STATE.pendingScans = []; SCAN_STATE.activeStartTime = null; SCAN_STATE.activeTracking = ''; SCAN_STATE.expectedSkuCounts.clear(); SCAN_STATE.scanCountBySku.clear(); SCAN_STATE.scanTableSnapshot.clear(); SCAN_STATE.lastScanTableSnapshot = ''; SCAN_STATE.lastScanCountSnapshot = ''; SCAN_STATE.lastSkuBodySnapshot = ''; if (SCAN_STATE.skuBodyDebounceTimer) { window.clearTimeout(SCAN_STATE.skuBodyDebounceTimer); SCAN_STATE.skuBodyDebounceTimer = null; } updateExpectedSkus(); if (reason) { console.log(`[PB] 批次变更,已重置扫描记录 (${reason})`); } }; // 清空已扫描 SKU 缓存。 const clearSkuSession = (reason) => { logStep('pb', '清空已扫 SKU', { reason }); SCAN_STATE.pendingScans = []; SCAN_STATE.activeStartTime = null; SCAN_STATE.expectedSkuCounts.clear(); SCAN_STATE.lastSkuBodySnapshot = ''; if (reason) { console.log(`[PB] 已清空扫描SKU记录 (${reason})`); } }; // 记录来自 SkuBody 的扫描。 const recordSkuScan = (sku) => { if (!sku) return; logStep('pb', '记录 SKU 扫描(SkuBody)', { sku }); const time = new Date(); SCAN_STATE.pendingScans.push({ sku, time }); if (!SCAN_STATE.activeStartTime) { SCAN_STATE.activeStartTime = time; } handleStateUpdate(STATE.tracking, sku); }; // 读取表格单元格文本。 const getCellText = (cell) => normalizeText(cell?.textContent || ''); // 解析扫描表格行。 const parseScanRow = (row) => { if (!row || row.nodeType !== 1) return null; const cells = row.querySelectorAll('td'); if (!cells.length) return null; const trackingNo = getCellText(cells[1]); const sku = getCellText(cells[2]); return { row, trackingNo, sku }; }; // 生成 scanData1 快照(tracking+sku -> {count,...})。 const buildScanTableMap = (body) => { const nextMap = new Map(); if (!body) return nextMap; body.querySelectorAll('tr').forEach((row) => { const data = parseScanRow(row); if (!data?.trackingNo || !data.sku) return; const key = `${data.trackingNo}||${data.sku}`; const prev = nextMap.get(key); if (prev) { prev.count += 1; } else { nextMap.set(key, { trackingNo: data.trackingNo, sku: data.sku, count: 1 }); } }); return nextMap; }; // 设置当前识别到的运单号并同步监控数据。 const setActiveTracking = (trackingNo, reason) => { if (!trackingNo) return; SCAN_STATE.activeTracking = trackingNo; if (STATE.tracking === trackingNo) return; STATE.tracking = trackingNo; logStep('pb', '更新运单号', { trackingNo, reason }); updateMonitorData({ isTargetPage: true, url: window.location.href, batchNo: '', sku: STATE.sku, trackingNo }); }; // 以当前 tracking 输出 pending 扫描并清空。 const flushPendingWithTracking = async (reason) => { const trackingNo = SCAN_STATE.activeTracking || ''; if (!trackingNo || SCAN_STATE.pendingScans.length === 0) return; const scans = SCAN_STATE.pendingScans.slice(); const startTime = SCAN_STATE.activeStartTime || scans[0]?.time || new Date(); const endTime = new Date(); SCAN_STATE.pendingScans = []; SCAN_STATE.activeStartTime = null; logStep('pb', '表格变更触发上传', { trackingNo, count: scans.length, reason }); await finalizeAndUploadScans({ trackingNo, scans, startTime, endTime, reason }); }; // scanData1 变化时刷新快照,并尝试为 pendingScans 绑定 tracking。 const handleScanTableMutation = (body) => { if (!body) return; const snapshot = normalizeText(body.textContent); if (snapshot && snapshot === SCAN_STATE.lastScanTableSnapshot) return; SCAN_STATE.lastScanTableSnapshot = snapshot; const prevMap = SCAN_STATE.scanTableSnapshot; const nextMap = buildScanTableMap(body); SCAN_STATE.scanTableSnapshot = nextMap; const newRows = []; nextMap.forEach((next) => { const key = `${next.trackingNo}||${next.sku}`; const prev = prevMap.get(key); const prevCount = prev?.count || 0; const delta = next.count - prevCount; if (delta > 0) { for (let i = 0; i < delta; i += 1) { newRows.push({ trackingNo: next.trackingNo, sku: next.sku }); } } }); if (!newRows.length) return; logStep('pb', '检测到新表格行', { count: newRows.length }); const trackingSet = new Set(); newRows.forEach((row) => { if (row.trackingNo) trackingSet.add(row.trackingNo); }); if (trackingSet.size === 1) { setActiveTracking(Array.from(trackingSet)[0], 'scanTable_delta'); } void flushPendingWithTracking('pb_scan_table_delta'); }; // 绑定扫描表格观察器。 const connectScanTable = () => { const body = document.getElementById(TABLE_IDS.scanBody); if (!body) return; if (scanObserverTarget === body) return; if (scanObserver) { scanObserver.disconnect(); } scanObserverTarget = body; SCAN_STATE.scanTableSnapshot = buildScanTableMap(body); SCAN_STATE.lastScanTableSnapshot = normalizeText(body.textContent); void flushPendingWithTracking('pb_scan_table_init'); const Observer = window.MutationObserver || MutationObserver; scanObserver = new Observer(() => handleScanTableMutation(body)); scanObserver.observe(body, { childList: true, subtree: true, characterData: true }); logStep('pb', '绑定扫描表格观察器'); }; // 监听 scanData 扫描数变化(增量视为扫描)。 const connectScanCountTable = () => { const body = document.getElementById(TABLE_IDS.scanCountBody); if (!body) return; if (scanCountObserverTarget === body) return; if (scanCountObserver) { scanCountObserver.disconnect(); } scanCountObserverTarget = body; updateScanCounts(); const handleScanCountMutation = () => { const snapshot = normalizeText(body.textContent); if (snapshot && snapshot === SCAN_STATE.lastScanCountSnapshot) return; SCAN_STATE.lastScanCountSnapshot = snapshot; const prevMap = SCAN_STATE.scanCountBySku; const nextMap = buildScanCountMap(body); SCAN_STATE.scanCountBySku = nextMap; nextMap.forEach((qty, sku) => { const prevQty = prevMap.get(sku) || 0; const delta = qty - prevQty; if (delta > 0) { for (let i = 0; i < delta; i += 1) { recordSkuScan(sku); } } }); }; const Observer = window.MutationObserver || MutationObserver; scanCountObserver = new Observer(handleScanCountMutation); scanCountObserver.observe(body, { childList: true, subtree: true, characterData: true }); logStep('pb', '绑定 scanData 观察器'); }; // 筛选与 SKU 计数相关的变动。 const mutationTouchesSkuCount = (mutations) => { for (const mutation of mutations) { if (mutation.type === 'characterData') { const parent = mutation.target?.parentElement; if (parent && parent.id && parent.id.startsWith('sku')) { return true; } } if (mutation.type === 'childList') { const targetEl = mutation.target?.nodeType === 1 ? mutation.target : mutation.target?.parentElement; if (targetEl?.id && targetEl.id.startsWith('sku')) return true; if (targetEl?.querySelector?.("span[id^='sku']")) return true; const nodes = [...mutation.addedNodes, ...mutation.removedNodes]; for (const node of nodes) { if (node.nodeType === 1) { const el = node; if (el.id && el.id.startsWith('sku')) return true; if (el.querySelector?.("span[id^='sku']")) return true; } if (node.nodeType === 3) { const parent = node.parentElement; if (parent && parent.id && parent.id.startsWith('sku')) return true; } } } } return false; }; // 绑定 SkuBody 观察器。 const connectSkuBody = () => { const body = document.getElementById(SECTION_IDS.skuBody); if (!body) return; if (skuObserverTarget === body) return; if (skuObserver) { skuObserver.disconnect(); } skuObserverTarget = body; updateExpectedSkus(); logStep('pb', '绑定 SkuBody 观察器'); // 处理 SKU 计数变动。 const handleSkuBodyMutation = (mutations) => { if (!mutationTouchesSkuCount(mutations)) return; if (SCAN_STATE.skuBodyDebounceTimer) { window.clearTimeout(SCAN_STATE.skuBodyDebounceTimer); } SCAN_STATE.skuBodyDebounceTimer = window.setTimeout(() => { SCAN_STATE.skuBodyDebounceTimer = null; const snapshot = normalizeText(body.textContent); if (snapshot && snapshot === SCAN_STATE.lastSkuBodySnapshot) return; SCAN_STATE.lastSkuBodySnapshot = snapshot; const prevMap = SCAN_STATE.expectedSkuCounts; const nextMap = buildExpectedSkuMap(body); SCAN_STATE.expectedSkuCounts = nextMap; let hasIncrease = false; nextMap.forEach((qty, sku) => { const prevQty = prevMap.get(sku) || 0; const delta = qty - prevQty; if (delta > 0) { hasIncrease = true; for (let i = 0; i < delta; i += 1) { recordSkuScan(sku); } } }); if (!hasIncrease) { return; } }, 60); }; const Observer = window.MutationObserver || MutationObserver; skuObserver = new Observer(handleSkuBodyMutation); skuObserver.observe(body, { childList: true, subtree: true, characterData: true }); }; // 监听清空已扫 SKU 按钮。 const connectClearSkuButton = () => { if (document.body?.dataset?.pbClearSkuBound === '1') return; if (document.body) { document.body.dataset.pbClearSkuBound = '1'; } logStep('pb', '绑定清空 SKU 按钮'); document.addEventListener( 'click', (event) => { const target = event.target; if (!(target instanceof Element)) return; const button = target.closest('button'); if (!button) return; const onclick = (button.getAttribute('onclick') || '').toLowerCase(); const label = normalizeText(button.textContent || ''); if (onclick.includes('btnclearsku') || onclick.includes('btnclare') || label.includes('清空已扫SKU')) { clearSkuSession('清空已扫SKU'); } }, true ); }; // 同步状态并在运单变化时重置。 const handleStateUpdate = (nextTracking, nextSku) => { const prevTracking = STATE.tracking; let changed = false; if (nextTracking !== STATE.tracking) { STATE.tracking = nextTracking; changed = true; } if (nextSku !== STATE.sku) { STATE.sku = nextSku; changed = true; } if (changed || !STATE.initialized) { updateMonitorData({ isTargetPage: true, url: window.location.href, batchNo: '', sku: STATE.sku, trackingNo: STATE.tracking }); } if (prevTracking !== nextTracking) { resetScanState(prevTracking ? '运单号更新' : ''); } STATE.initialized = true; logStep('pb', '状态更新', { trackingNo: STATE.tracking, sku: STATE.sku }); }; // 确保朋博观察器生效。 const ensureObservers = () => { // 避免在频繁 DOM 变动时刷屏日志。 connectScanTable(); connectScanCountTable(); connectSkuBody(); connectClearSkuButton(); }; // 启动驱动监听。 const start = () => { logStep('pb', '启动驱动'); ensureObservers(); if (pageObserver) { pageObserver.disconnect(); } pageObserver = new MutationObserver(() => { ensureObservers(); }); pageObserver.observe(document.documentElement, { childList: true, subtree: true }); }; // 停止驱动监听并清理状态。 const stop = () => { logStep('pb', '停止驱动'); if (pageObserver) { pageObserver.disconnect(); pageObserver = null; } if (scanObserver) { scanObserver.disconnect(); scanObserver = null; } if (scanCountObserver) { scanCountObserver.disconnect(); scanCountObserver = null; } if (skuObserver) { skuObserver.disconnect(); skuObserver = null; } scanObserverTarget = null; scanCountObserverTarget = null; skuObserverTarget = null; SCAN_STATE.pendingScans = []; SCAN_STATE.activeStartTime = null; SCAN_STATE.activeTracking = ''; SCAN_STATE.expectedSkuCounts.clear(); SCAN_STATE.scanCountBySku.clear(); SCAN_STATE.scanTableSnapshot.clear(); SCAN_STATE.lastScanTableSnapshot = ''; SCAN_STATE.lastScanCountSnapshot = ''; SCAN_STATE.lastSkuBodySnapshot = ''; if (SCAN_STATE.skuBodyDebounceTimer) { window.clearTimeout(SCAN_STATE.skuBodyDebounceTimer); SCAN_STATE.skuBodyDebounceTimer = null; } STATE.sku = ''; STATE.tracking = ''; STATE.initialized = false; }; // 返回当前驱动状态。 const getStatus = () => ({ isTargetPage: true, url: window.location.href, batchNo: '', sku: STATE.sku, trackingNo: STATE.tracking }); // 手动结束订单 const forceEndOrder = () => { if (!SCAN_STATE.activeTracking || SCAN_STATE.pendingScans.length === 0) { logStep('pb', '手动结束订单: 无待提交数据'); return; } logStep('pb', '手动结束订单', { tracking: SCAN_STATE.activeTracking, count: SCAN_STATE.pendingScans.length }); void flushPendingWithTracking('manual_force_end'); }; return { name: 'pb', match, start, stop, refresh: () => { updateMonitorData({ isTargetPage: true, url: window.location.href, batchNo: '', sku: STATE.sku, trackingNo: STATE.tracking }); }, getStatus, forceEndOrder }; }; // 创建 QY(千易)页面驱动。 const createQyDriver = () => { const TARGET_ORIGINS = ['https://cnwms.aiyacang.com', 'https://wms.aiyacang.com', 'http://wms.aiyacang.com', 'https://gwmsth.best-inc.com', 'http://gwmsth.best-inc.com']; const state = { active: false, tableEl: null, tableObserver: null, pageObserver: null, lastSkuSignature: null, prevRowMap: null, lastScanTracking: null, lastScanSku: null, pendingScans: [] }; // 判断是否匹配当前驱动页面。 const match = () => TARGET_ORIGINS.includes(window.location.origin); // 规范化千易表格文本。 const setText = (value) => normalizeText(value); // 解析表格单元格中的数值。 const toNumber = (value) => { const cleaned = String(value || '').replace(/[^\d.-]/g, ''); const parsed = Number(cleaned); return Number.isFinite(parsed) ? parsed : 0; }; // 计算表格签名用于差异检测。 const hashSku = (value) => { let hash = 5381; const str = String(value || ''); for (let i = 0; i < str.length; i += 1) { hash = ((hash << 5) + hash) + str.charCodeAt(i); hash &= 0xffffffff; } return hash; }; // 计算千易表格签名。 const computeSkuSignature = (rows, indexes) => { let sum = 0; let count = 0; rows.forEach((row) => { const sku = row.cells[indexes.skuIndex] || ''; const tracking = Number.isInteger(indexes.trackingIndex) ? (row.cells[indexes.trackingIndex] || '') : ''; const wms = Number.isInteger(indexes.wmsIndex) ? (row.cells[indexes.wmsIndex] || '') : ''; const composite = `${tracking}|${sku}|${wms}`; sum += hashSku(composite); count += 1; }); return `${count}:${sum}`; }; // 构建行唯一键用于对比。 const buildRowKey = ({ sku, tracking, wms, index }) => { const parts = []; if (wms) parts.push(`wms:${wms}`); if (tracking) parts.push(`tracking:${tracking}`); if (sku) parts.push(`sku:${sku}`); if (parts.length === 0) parts.push(`row:${index}`); return parts.join('|'); }; // 构建行数据映射用于对比。 const buildRowMap = (rows, indexes) => { const map = new Map(); rows.forEach((row, index) => { const sku = row.cells[indexes.skuIndex] || ''; const tracking = Number.isInteger(indexes.trackingIndex) ? (row.cells[indexes.trackingIndex] || '') : ''; const wms = Number.isInteger(indexes.wmsIndex) ? (row.cells[indexes.wmsIndex] || '') : ''; const scannedRaw = row.cells[indexes.scannedIndex] || '0'; const scanned = toNumber(scannedRaw); const pendingRaw = Number.isInteger(indexes.pendingIndex) ? (row.cells[indexes.pendingIndex] || '0') : '0'; const pending = toNumber(pendingRaw); const key = buildRowKey({ sku, tracking, wms, index }); map.set(key, { sku, tracking, wms, scanned, pending }); }); return map; }; // 表头文字映射到列索引。 const buildIndexMap = (headers) => { if (!headers || headers.length === 0) return null; const normalized = headers.map((text) => text.toLowerCase()); // 根据候选表头查找列索引。 const findIndex = (candidates) => { for (const candidate of candidates) { const needle = candidate.toLowerCase(); const exactIndex = normalized.indexOf(needle); if (exactIndex !== -1) return exactIndex; const includesIndex = normalized.findIndex((text) => text.includes(needle)); if (includesIndex !== -1) return includesIndex; } return -1; }; const map = {}; const lookup = { wms: ['WMS出库单号', 'wms出库单号', '出库单号', 'wms'], erp: ['ERP单号', 'erp单号', 'erp'], tracking: ['运单号', 'tracking', '运单'], sku: ['SKU', 'sku'], pending: ['待验货', '待验'], scanned: ['扫描量', '扫描'] }; Object.keys(lookup).forEach((key) => { const index = findIndex(lookup[key]); if (index !== -1) { map[key] = index; } }); logStep('qy', '表头索引映射', map); return map; }; // 读取表头文字。 const getHeaderTexts = (table) => { const headerTable = getHeaderTable(table); const headers = Array.from(headerTable.querySelectorAll('thead th')); if (headers.length === 0) return []; return headers.map((th) => setText(th.innerText || th.textContent || '')); }; // 定位千易表头表格。 const getHeaderTable = (table) => { if (table.querySelectorAll('thead th').length > 0) { return table; } const container = table.closest('div.wmstool-table-content') || document.querySelector('div.wmstool-table-content') || table.parentElement || document; const candidates = Array.from(container.querySelectorAll('table.wmstool-table-fixed')); const withHeaders = candidates.find((candidate) => candidate.querySelectorAll('thead th').length > 0); return withHeaders || table; }; // 定位千易活动表格。 const getActiveTable = () => { const content = document.querySelector('div.wmstool-table-content'); if (content) { const scopedTables = Array.from(content.querySelectorAll('table.wmstool-table-fixed')); if (scopedTables.length >= 2) { return scopedTables[1]; } } const tables = Array.from(document.querySelectorAll('table.wmstool-table-fixed')); if (tables.length === 0) return null; const withRows = tables.find((table) => table.querySelectorAll('tbody tr').length > 0); if (withRows) return withRows; const withBody = tables.find((table) => table.querySelector('tbody')); return withBody || tables[tables.length - 1]; }; // 生成朋博表格快照。 const getTableSnapshot = () => { const table = getActiveTable(); if (!table) { return { rows: [], indexMap: null }; } const headers = getHeaderTexts(table); const indexMap = buildIndexMap(headers); const rows = Array.from(table.querySelectorAll('tbody tr')); const mappedRows = rows.map((row) => { const cells = Array.from(row.querySelectorAll('td')).map((td) => setText(td.innerText)); return { cells }; }); logStep('qy', '表格快照', { rows: mappedRows.length }); return { rows: mappedRows, indexMap }; }; // 提交待上传扫描并触发上传。 const flushPendingScans = async (reason) => { if (!state.lastScanTracking || state.pendingScans.length === 0) { return; } logStep('qy', '提交扫描批次', { tracking: state.lastScanTracking, count: state.pendingScans.length, reason }); const scans = state.pendingScans.slice(); const startTime = new Date(scans[0].time); const endTime = new Date(); await finalizeAndUploadScans({ trackingNo: state.lastScanTracking, scans, startTime, endTime, reason }); state.pendingScans = []; }; // 记录千易表格增量扫描。 const recordScan = (row) => { const tracking = row.tracking || ''; if (state.lastScanTracking && tracking && tracking !== state.lastScanTracking) { void flushPendingScans('qy_tracking_changed'); } if (tracking && tracking !== state.lastScanTracking) { state.lastScanTracking = tracking; } const entry = { sku: row.sku, time: new Date() }; state.pendingScans.push(entry); state.lastScanSku = entry.sku; logStep('qy', '记录表格扫描', { tracking, sku: entry.sku }); updateMonitorData({ isTargetPage: true, url: window.location.href, batchNo: '', sku: state.lastScanSku || '', trackingNo: state.lastScanTracking || '' }); }; // 处理表格差异并记录扫描。 const processTableSnapshot = (snapshot) => { if (!snapshot || !snapshot.indexMap) return; const skuIndex = snapshot.indexMap.sku; const scannedIndex = snapshot.indexMap.scanned; const pendingIndex = snapshot.indexMap.pending; const trackingIndex = snapshot.indexMap.tracking; const wmsIndex = snapshot.indexMap.wms; if (!Number.isInteger(skuIndex) || !Number.isInteger(scannedIndex)) { return; } const signature = computeSkuSignature(snapshot.rows, { skuIndex, trackingIndex, wmsIndex }); if (state.lastSkuSignature === null) { state.lastSkuSignature = signature; state.prevRowMap = buildRowMap(snapshot.rows, { skuIndex, scannedIndex, trackingIndex, wmsIndex, pendingIndex }); return; } if (signature !== state.lastSkuSignature) { void flushPendingScans('qy_table_changed'); state.lastSkuSignature = signature; state.prevRowMap = buildRowMap(snapshot.rows, { skuIndex, scannedIndex, trackingIndex, wmsIndex, pendingIndex }); state.pendingScans = []; state.lastScanTracking = null; logStep('qy', '表格变化重置扫描'); return; } const prevMap = state.prevRowMap || new Map(); const currentMap = buildRowMap(snapshot.rows, { skuIndex, scannedIndex, pendingIndex, trackingIndex, wmsIndex }); let pendingChanged = false; currentMap.forEach((row, key) => { const prev = prevMap.get(key); const prevScanned = prev ? prev.scanned : 0; if (row.scanned > prevScanned) { recordScan({ sku: row.sku, tracking: row.tracking }); } if (Number.isInteger(pendingIndex)) { if (prev && row.pending < prev.pending) { pendingChanged = true; } } }); if (pendingChanged) { void flushPendingScans('qy_pending_changed'); state.pendingScans = []; state.lastScanTracking = null; logStep('qy', '待验货变化清空扫描'); } state.prevRowMap = currentMap; state.lastSkuSignature = signature; }; // 刷新千易表格快照。 const updateSnapshot = () => { logStep('qy', '刷新表格快照'); const snapshot = getTableSnapshot(); processTableSnapshot(snapshot); }; // 监听千易表格变动。 const attachTableObserver = (table) => { if (!table || table === state.tableEl) return; state.tableEl = table; if (state.tableObserver) { state.tableObserver.disconnect(); } state.tableObserver = new MutationObserver(() => { updateSnapshot(); }); const body = table.querySelector('tbody') || table; state.tableObserver.observe(body, { childList: true, subtree: true, characterData: true }); logStep('qy', '绑定表格观察器'); updateSnapshot(); }; // 定位千易表格元素。 const scanForElements = () => { if (!state.active) return; const table = getActiveTable(); if (table) { attachTableObserver(table); } else if (state.tableObserver) { state.tableObserver.disconnect(); state.tableObserver = null; state.tableEl = null; } logStep('qy', '扫描页面元素'); }; // 监听页面变动以重连。 const ensurePageObserver = () => { if (state.pageObserver || !document.body) return; state.pageObserver = new MutationObserver(() => { if (!state.active) return; scanForElements(); }); state.pageObserver.observe(document.body, { childList: true, subtree: true }); logStep('qy', '绑定页面观察器'); }; // 启动驱动监听。 const start = () => { if (state.active) return; logStep('qy', '启动驱动'); state.active = true; ensurePageObserver(); scanForElements(); updateMonitorData({ isTargetPage: true, url: window.location.href, batchNo: '', sku: state.lastScanSku || '', trackingNo: state.lastScanTracking || '' }); }; // 停止驱动监听并清理状态。 const stop = () => { logStep('qy', '停止驱动'); state.active = false; state.tableEl = null; if (state.tableObserver) { state.tableObserver.disconnect(); state.tableObserver = null; } if (state.pageObserver) { state.pageObserver.disconnect(); state.pageObserver = null; } state.pendingScans = []; state.lastScanTracking = null; state.lastScanSku = null; state.prevRowMap = null; state.lastSkuSignature = null; }; // 返回当前驱动状态。 const getStatus = () => ({ isTargetPage: true, url: window.location.href, batchNo: '', sku: state.lastScanSku || '', trackingNo: state.lastScanTracking || '' }); // 手动结束订单 const forceEndOrder = () => { if (!state.lastScanTracking || state.pendingScans.length === 0) { logStep('qy', '手动结束订单: 无待提交数据'); return; } logStep('qy', '手动结束订单', { tracking: state.lastScanTracking, count: state.pendingScans.length }); void flushPendingScans('manual_force_end'); }; return { name: 'qy', match, start, stop, refresh: () => { updateMonitorData({ isTargetPage: true, url: window.location.href, batchNo: '', sku: state.lastScanSku || '', trackingNo: state.lastScanTracking || '' }); }, getStatus, forceEndOrder }; }; // 创建 JF(极风)页面驱动。 const createJfDriver = () => { const state = { active: false, tableEl: null, tableObserver: null, pageObserver: null, prevRowMap: null, activeTrackingNo: null, pendingScans: [], activeStartTime: null }; // 判断是否匹配当前驱动页面。 const match = () => { return window.location.hostname.endsWith('.jfwms.com') && window.location.pathname.includes('/web/out/package/inspection'); }; // 解析表格行数据(支持一行多SKU)。 const parseTableRow = (row) => { if (!row || row.nodeType !== 1) return []; // 第二列:运单号 const trackingCol = row.querySelector('td:nth-child(2)'); if (!trackingCol) return []; const firstP = trackingCol.querySelector('p'); const trackingNo = normalizeText(firstP?.textContent || ''); if (!trackingNo) return []; // 第三列:SKU和扫描数 const skuCol = row.querySelector('td:nth-child(3)'); if (!skuCol) return []; const result = []; // 获取所有 sku_flex 和 flex_135 对 const skuFlexes = Array.from(skuCol.querySelectorAll('.flex.sku_flex')); const flex135s = Array.from(skuCol.querySelectorAll('.flex_135')); // 遍历配对 skuFlexes.forEach((skuFlex, index) => { const skuEl = skuFlex.querySelector('.t_name'); const sku = normalizeText(skuEl?.textContent || ''); if (!sku) return; const flex135 = flex135s[index]; const scannedSpan = flex135?.querySelector('span'); const scannedText = normalizeText(scannedSpan?.textContent || '0'); const scannedCount = parseInt(scannedText, 10); result.push({ trackingNo, sku, scannedCount: Number.isFinite(scannedCount) ? scannedCount : 0 }); }); return result; }; // 生成行唯一键。 const getRowKey = (rowData) => { if (!rowData) return ''; return `${rowData.trackingNo}|${rowData.sku}`; }; // 构建行数据映射。 const buildRowMap = (tableEl) => { const map = new Map(); if (!tableEl) return map; const rows = tableEl.querySelectorAll('tr.el-table__row'); rows.forEach((row) => { const items = parseTableRow(row); items.forEach((data) => { if (data) { const key = getRowKey(data); map.set(key, data); } }); }); return map; }; // 提交待上传扫描并触发上传。 const flushPendingScans = async (trackingNo) => { if (!trackingNo || state.pendingScans.length === 0) { return; } logStep('jf', '提交扫描批次', { tracking: trackingNo, count: state.pendingScans.length }); const scans = state.pendingScans.slice(); const startTime = state.activeStartTime || new Date(scans[0].time); const endTime = new Date(); state.pendingScans = []; state.activeStartTime = null; await finalizeAndUploadScans({ trackingNo, scans, startTime, endTime, reason: 'jf_scan_complete' }); }; // 记录扫描。 const recordScan = ({ trackingNo, sku }) => { // 运单号切换时提交上一个批次 if (state.activeTrackingNo && trackingNo && trackingNo !== state.activeTrackingNo) { void flushPendingScans(state.activeTrackingNo); } if (trackingNo && trackingNo !== state.activeTrackingNo) { state.activeTrackingNo = trackingNo; } const entry = { sku, time: new Date() }; state.pendingScans.push(entry); if (!state.activeStartTime) { state.activeStartTime = entry.time; } logStep('jf', '记录扫描', { trackingNo, sku }); updateMonitorData({ isTargetPage: true, url: window.location.href, batchNo: '', sku: entry.sku, trackingNo: state.activeTrackingNo || '' }); }; // 处理表格更新。 const processTableUpdate = () => { if (!state.tableEl) return; const currentMap = buildRowMap(state.tableEl); if (state.prevRowMap === null) { state.prevRowMap = currentMap; return; } const prevMap = state.prevRowMap; // 检测 scannedCount +1 的行 currentMap.forEach((current, key) => { const prev = prevMap.get(key); const prevScanned = prev ? prev.scannedCount : 0; if (current.scannedCount > prevScanned) { // 每次 +1 记录一次扫描 const delta = current.scannedCount - prevScanned; for (let i = 0; i < delta; i++) { recordScan({ trackingNo: current.trackingNo, sku: current.sku }); } } }); state.prevRowMap = currentMap; }; // 定位表格元素。 const getTableElement = () => { return document.querySelector('.el-table .el-table__body'); }; // 绑定表格观察器。 const attachTableObserver = (table) => { if (!table || table === state.tableEl) return; state.tableEl = table; if (state.tableObserver) { state.tableObserver.disconnect(); } state.tableObserver = new MutationObserver(() => { processTableUpdate(); }); state.tableObserver.observe(table, { childList: true, subtree: true, characterData: true }); logStep('jf', '绑定表格观察器'); state.prevRowMap = buildRowMap(table); }; // 扫描页面元素。 const scanForElements = () => { if (!state.active) return; const table = getTableElement(); if (table) { attachTableObserver(table); } else if (state.tableObserver) { state.tableObserver.disconnect(); state.tableObserver = null; state.tableEl = null; } logStep('jf', '扫描页面元素'); }; // 监听页面变动以重连。 const ensurePageObserver = () => { if (state.pageObserver || !document.body) return; state.pageObserver = new MutationObserver(() => { if (!state.active) return; scanForElements(); }); state.pageObserver.observe(document.body, { childList: true, subtree: true }); logStep('jf', '绑定页面观察器'); }; // 启动驱动监听。 const start = () => { if (state.active) return; logStep('jf', '启动驱动'); state.active = true; ensurePageObserver(); scanForElements(); updateMonitorData({ isTargetPage: true, url: window.location.href, batchNo: '', sku: '', trackingNo: '' }); }; // 停止驱动监听并清理状态。 const stop = () => { logStep('jf', '停止驱动'); state.active = false; state.tableEl = null; state.prevRowMap = null; state.activeTrackingNo = null; state.activeStartTime = null; if (state.tableObserver) { state.tableObserver.disconnect(); state.tableObserver = null; } if (state.pageObserver) { state.pageObserver.disconnect(); state.pageObserver = null; } // 停止前尝试提交剩余扫描 if (state.activeTrackingNo && state.pendingScans.length > 0) { void flushPendingScans(state.activeTrackingNo); } state.pendingScans = []; }; // 返回当前驱动状态。 const getStatus = () => ({ isTargetPage: true, url: window.location.href, batchNo: '', sku: state.pendingScans.length > 0 ? state.pendingScans[state.pendingScans.length - 1].sku : '', trackingNo: state.activeTrackingNo || '' }); // 手动结束订单 const forceEndOrder = () => { if (!state.activeTrackingNo || state.pendingScans.length === 0) { logStep('jf', '手动结束订单: 无待提交数据'); return; } logStep('jf', '手动结束订单', { tracking: state.activeTrackingNo, count: state.pendingScans.length }); void flushPendingScans(state.activeTrackingNo); }; return { name: 'jf', match, start, stop, refresh: () => { updateMonitorData({ isTargetPage: true, url: window.location.href, batchNo: '', sku: state.pendingScans.length > 0 ? state.pendingScans[state.pendingScans.length - 1].sku : '', trackingNo: state.activeTrackingNo || '' }); }, getStatus, forceEndOrder }; }; const DRIVERS = [createPbDriver(), createQyDriver(), createJfDriver()]; // 停止当前激活的驱动。 const stopActiveDriver = () => { if (activeDriver) { logStep('core', '停止当前驱动', { name: activeDriver.name }); activeDriver.stop(); } activeDriver = null; activeDriverName = ''; }; // 根据 URL/登录状态重选驱动。 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: '' }); // 未登录时切换到登录视图,而不是隐藏 OSD switchView('login'); 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: '' }); // 未匹配到驱动时最小化 OSD,而不是隐藏 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 }); }; // 监听 SPA 路由变化。 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); }; // 初始化 OSD 与驱动路由。 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(); } chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { if (request.action === 'getStatus') { if (activeDriver) { sendResponse(activeDriver.getStatus()); } else { sendResponse({ isTargetPage: false, url: window.location.href, batchNo: '', sku: '', trackingNo: '' }); } } else if (request.action === 'forceEndOrder') { // 处理手动结束订单请求 if (activeDriver && typeof activeDriver.forceEndOrder === 'function') { logStep('core', '收到手动结束订单请求', { driver: activeDriver.name }); activeDriver.forceEndOrder(); sendResponse({ success: true, message: '已触发结束订单' }); } else { logStep('core', '手动结束订单失败: 当前驱动不支持', { driver: activeDriver?.name }); sendResponse({ success: false, message: '当前页面不支持结束订单' }); } } return true; }); const registerTampermonkeyMenu = () => { if (!IS_TOP || typeof GM_registerMenuCommand !== 'function') return; GM_registerMenuCommand('HCC: 强制结束当前订单', async () => { const result = await chrome.runtime.sendMessage({ action: 'forceEndOrder' }); if (result?.message) { alert(result.message); } }); GM_registerMenuCommand('HCC: 查看当前状态', async () => { const status = await chrome.runtime.sendMessage({ action: 'getStatus' }); alert(JSON.stringify(status || {}, null, 2)); }); GM_registerMenuCommand('HCC: 清空登录并刷新', async () => { await chrome.storage.local.remove([ 'isLoggedIn', 'userId', 'jwtToken', 'userData', 'loginTime', 'currentStationId', 'currentStationData' ]); location.reload(); }); GM_registerMenuCommand('HCC: 清空工位并刷新', async () => { await chrome.storage.local.remove(['currentStationId', 'currentStationData']); location.reload(); }); }; registerTampermonkeyMenu(); })();