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