// ==UserScript== // @name ASTP/ServiceBox→CMIP 选中快查 v2.9.20【新增批量查询+导出功能】 // @namespace cmip-qq-v2920 // @version 2.9.20 // @description ★★★功能增强版:①新增批量查询功能(支持手动输入多编号、粘贴Excel/CSV表格导入);②结果表格展示,一键导出CSV文件;③并发控制(2个并发)避免请求过快;④来源标注字体调小到8px;⑤保留v2.9.19所有致命bug修复:彻底解决跨站污染、事件监听器清理、性能优化。 // @match https://astp.dpca.com.cn/* // @match https://servicebox.mpsa.com/* // @match https://cmip.dpca.com.cn/* // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant unsafeWindow // @run-at document-idle // @license MIT // ==/UserScript== (function () { 'use strict'; // ★ 修复1:全局防重复执行标记(防止SPA导航导致脚本重复执行造成多层hook/事件泄漏) if (window.__cmip_qq_main_loaded) { console.log('[CMIP快查] 脚本已加载,跳过重复执行'); return; } window.__cmip_qq_main_loaded = true; // ★ 修复2:统一资源清理注册表(页面卸载时清理所有事件监听器和定时器) var _cleanupHandlers = []; function addCleanup(fn) { _cleanupHandlers.push(fn); } function doCleanup() { for (var i = 0; i < _cleanupHandlers.length; i++) { try { _cleanupHandlers[i](); } catch (e) { } } _cleanupHandlers = []; } window.addEventListener('beforeunload', doCleanup); window.addEventListener('unload', doCleanup); var HOST = location.host; var IS_ASTP = /astp\.dpca\.com\.cn/.test(HOST); var IS_SERVICEBOX = /servicebox\.mpsa\.com/.test(HOST); var IS_QUERY_SITE = IS_ASTP || IS_SERVICEBOX; var IS_CMIP = /cmip\.dpca\.com\.cn/.test(HOST); var TOKEN_KEY = 'cmip_usertoken_v2'; var HEADER_KEY = 'cmip_req_headers_v2'; var AUTH_FMT_KEY = 'cmip_auth_fmt_v2'; var DOT_POS_KEY = 'cmip_dot_pos_v2'; var REPLACEMENT_KEY = 'cmip_replacement_v2'; var LAST_CODE_KEY = 'cmip_last_code_v2'; var REPL_API_KEY = 'cmip_repl_api_v2'; // 存替换接口的 URL + 请求体模板 var CODE_RE = /^[A-Za-z0-9]{4,30}$/; var CMIP_API_URL = 'https://cmip.dpca.com.cn/pac/iPaDbBasePartInfo/getBasePartInfoList.do?pageSize=500&pageIndex=1'; // ============================================================ // 首次使用密钥验证(152___382) // ============================================================ var ACTIVATED_KEY = 'cmip_qq_activated_v1'; function isActivated() { try { return GM_getValue(ACTIVATED_KEY, '') === 'yes'; } catch (e) { return false; } } function showKeyGate() { function createGate() { if (document.getElementById('cmip-key-gate')) return; var overlay = document.createElement('div'); overlay.id = 'cmip-key-gate'; overlay.style.cssText = 'position:fixed;top:0;left:0;width:100vw;height:100vh;background:rgba(0,0,0,0.92);z-index:2147483647;display:flex;align-items:center;justify-content:center;font-family:Microsoft YaHei,sans-serif;'; var card = document.createElement('div'); card.style.cssText = 'background:#fff;border-radius:10px;padding:30px 24px;box-shadow:0 8px 30px rgba(0,0,0,0.4);'; var row = document.createElement('div'); row.style.cssText = 'display:flex;gap:8px;'; var inputs = []; var preFilled = ['1', '5', '2']; for (var i = 0; i < 6; i++) { var inp = document.createElement('input'); inp.type = 'text'; inp.maxLength = 1; inp.style.cssText = 'width:42px;height:52px;text-align:center;font-size:26px;font-weight:700;border:2px solid #dcdfe6;border-radius:6px;outline:none;' + (i < 3 ? 'background:#f5f5f5;color:#909399;' : 'background:#fff;color:#303133;'); if (i < 3) { inp.value = preFilled[i]; inp.readOnly = true; } else { (function (idx, el) { el.addEventListener('input', function () { // 只保留数字,清空非数字字符 var v = el.value.replace(/[^0-9]/g, ''); el.value = v; // 输入了一个数字后自动跳到下一个框 if (v && idx < 5) { inputs[idx + 1].focus(); } setTimeout(checkInputs, 10); }); // 支持退格:空框时按退格回到上一个框 el.addEventListener('keydown', function (e) { if (e.keyCode === 8 && !el.value && idx > 3) { inputs[idx - 1].focus(); } }); })(i, inp); } inputs.push(inp); row.appendChild(inp); } card.appendChild(row); overlay.appendChild(card); document.body.appendChild(overlay); if (inputs[3]) inputs[3].focus(); function checkInputs() { var v3 = inputs[3].value; var v4 = inputs[4].value; var v5 = inputs[5].value; if (v3 && v4 && v5) { if (v3 === '3' && v4 === '8' && v5 === '2') { try { GM_setValue(ACTIVATED_KEY, 'yes'); } catch (e) { } var ov = document.getElementById('cmip-key-gate'); if (ov && ov.parentNode) ov.parentNode.removeChild(ov); setTimeout(function () { location.reload(); }, 200); } else { inputs[3].value = ''; inputs[4].value = ''; inputs[5].value = ''; if (inputs[3]) inputs[3].focus(); } } } } if (document.body) { createGate(); } else { document.addEventListener('DOMContentLoaded', createGate); } } if (!isActivated()) { showKeyGate(); return; } var CMIP_STOCK_API_URL = 'https://cmip.dpca.com.cn/pac/PacPaBuTb2a0/qryMengeByMatnr.do'; // ★★★ v2.9.19关键修复:启动时检测并清理被网点库存污染的存储 // 如果保存的库存API URL包含网点库存/明细账/DepotInventory等关键词,说明已被污染,立即重置为默认值 try { var savedStockApi = JSON.parse(GM_getValue('cmip_stock_api_v2', '{}') || '{}'); if (savedStockApi && savedStockApi.url) { var polluted = /inventoryLedger|DepotInventory|depot|明细账|账面|仓位|库存账|outlet|dealer|dlrCode|网点|经销商/i.test(savedStockApi.url); if (polluted) { console.log('[CMIP快查] 🧹 检测到库存API已被网点库存URL污染,已自动重置为默认值'); console.log('[CMIP快查] 污染URL: ' + savedStockApi.url); GM_setValue('cmip_stock_api_v2', JSON.stringify({ url: CMIP_STOCK_API_URL, bodyTemplate: '{}' })); } } // 同时清理网点库存相关的存储key GM_deleteValue && GM_deleteValue('cmip_depot_stock_api_v1'); } catch (e) { console.log('[CMIP快查] 存储清理异常: ' + e.message); } // 已知的 CMIP 替换关系接口候选(按优先级尝试)—— 实测真实URL在 iPaDbPartSale 下 var CMIP_REPL_API_CANDIDATES = [ 'https://cmip.dpca.com.cn/pac/iPaDbPartSale/getPartReplacelList.do?pageSize=500&pageIndex=1', 'https://cmip.dpca.com.cn/pac/iPaDbBasePartInfo/getReplacePartInfoList.do?pageSize=500&pageIndex=1', 'https://cmip.dpca.com.cn/pac/iPaDbBasePartInfo/getPartReplaceList.do?pageSize=500&pageIndex=1', 'https://cmip.dpca.com.cn/pac/iPaDbBasePartInfo/getSubstituteList.do?pageSize=500&pageIndex=1' ]; function saveReplApi(url, method, bodyTemplate, headers) { try { GM_setValue(REPL_API_KEY, JSON.stringify({ url: url, method: method || 'POST', bodyTemplate: bodyTemplate, headers: headers || {} })); } catch (e) { } } function getReplApi() { try { return JSON.parse(GM_getValue(REPL_API_KEY, '{}') || '{}'); } catch (e) { return {}; } } function saveToken(t) { try { GM_setValue(TOKEN_KEY, t || ''); } catch (e) { } } function getToken() { try { return GM_getValue(TOKEN_KEY, '') || ''; } catch (e) { return ''; } } function saveHeaders(h) { try { GM_setValue(HEADER_KEY, JSON.stringify(h || {})); } catch (e) { } } function getHeaders() { try { return JSON.parse(GM_getValue(HEADER_KEY, '{}') || '{}'); } catch (e) { return {}; } } function saveAuthFmt(a) { try { GM_setValue(AUTH_FMT_KEY, a || ''); } catch (e) { } } function getAuthFmt() { try { return GM_getValue(AUTH_FMT_KEY, '') || ''; } catch (e) { return ''; } } function saveReplacement(code, data) { try { var all = JSON.parse(GM_getValue(REPLACEMENT_KEY, '{}') || '{}'); all[String(code).toUpperCase()] = { data: data, ts: Date.now() }; GM_setValue(REPLACEMENT_KEY, JSON.stringify(all)); } catch (e) { } } function getReplacement(code) { try { var all = JSON.parse(GM_getValue(REPLACEMENT_KEY, '{}') || '{}'); return all[String(code).toUpperCase()] || null; } catch (e) { return null; } } function saveLastCode(code) { try { GM_setValue(LAST_CODE_KEY, String(code || '')); } catch (e) { } } // ============================================================ // CMIP 端 // ============================================================ if (IS_CMIP) { function readLocalToken() { var t = ''; try { var ls = (typeof unsafeWindow !== 'undefined' && unsafeWindow && unsafeWindow.localStorage) ? unsafeWindow.localStorage : (window.top ? window.top.localStorage : localStorage); t = ls.getItem('userToken') || ls.getItem('ntsptoken') || ls.getItem('token') || ''; } catch (e) { } if (!t) { try { t = (typeof unsafeWindow !== 'undefined' && unsafeWindow.userToken) || window.userToken || ''; } catch (e) { } } if (!t) { try { var m = document.cookie.match(/(?:userToken|ntsptoken|token)\s*=\s*([^;&\s]+)/); if (m) t = decodeURIComponent(m[1]); } catch (e) { } } return t; } function showToast(msg, bg) { var d = document.createElement('div'); d.textContent = msg; d.style.cssText = 'position:fixed;top:16px;right:60px;z-index:2147483647;background:' + (bg || '#303133') + ';color:#fff;padding:8px 12px;border-radius:6px;font-size:12px;font-family:Microsoft YaHei,sans-serif;box-shadow:0 2px 8px rgba(0,0,0,.25);'; document.body.appendChild(d); setTimeout(function () { d.remove(); }, 2500); } function doSync(silent, onDone) { var t = readLocalToken(); if (!t) { // token 获取失败:确保圆点显示红色 var d = document.getElementById('cmip-dot-v2'); if (d) { d.style.background = 'linear-gradient(135deg,#f56c6c,#ff8a80)'; d.style.display = 'block'; d.title = '❌ 未读取到CMIP Token,请先登录;点击手动尝试同步'; } if (!silent) showToast('❌ 没读到 token', '#f56c6c'); if (typeof onDone === 'function') onDone(false); return false; } saveToken(t); // token 获取成功:圆点绿色 + 保持常驻显示(不隐藏) var d2 = document.getElementById('cmip-dot-v2'); if (d2) { d2.style.background = 'linear-gradient(135deg,#67c23a,#409eff)'; d2.style.display = 'block'; d2.title = '✅ Token已同步成功(' + t.length + '位);点击重新同步;按住拖动'; } if (!silent) showToast('✅ Token 已同步(' + t.length + '位)', '#67c23a'); console.log('[CMIP快查] token已同步 len=' + t.length + ' → 绿点常驻显示'); if (typeof onDone === 'function') onDone(true); return true; } // ===== 可拖动小圆点(常驻右上角,状态变色)===== function injectDot() { var oldDot = document.getElementById('cmip-dot-v2'); if (oldDot && oldDot.parentNode) oldDot.parentNode.removeChild(oldDot); // 每次打开默认放在右上角(window.innerWidth - 48, y=24),不再恢复上次位置 var defaultX = Math.max(10, window.innerWidth - 48); var defaultY = 24; var dot = document.createElement('div'); dot.id = 'cmip-dot-v2'; dot.title = 'CMIP Token同步中...'; dot.style.cssText = 'position:fixed;z-index:2147483646;width:14px;height:14px;border-radius:50%;background:linear-gradient(135deg,#ffb300,#ffa726);cursor:pointer;box-shadow:0 1px 4px rgba(0,0,0,.35);user-select:none;transition:transform .12s,background .2s;'; dot.style.left = defaultX + 'px'; dot.style.top = defaultY + 'px'; var dotMouseEnter = function () { dot.style.transform = 'scale(1.9)'; }; var dotMouseLeave = function () { dot.style.transform = 'scale(1)'; }; dot.addEventListener('mouseenter', dotMouseEnter); dot.addEventListener('mouseleave', dotMouseLeave); var dragging = false, moved = false, sx = 0, sy = 0, ox = 0, oy = 0; var dotMouseDown = function (e) { dragging = true; moved = false; sx = e.clientX; sy = e.clientY; var rect = dot.getBoundingClientRect(); ox = rect.left; oy = rect.top; e.preventDefault(); }; var dotMouseMove = function (e) { if (!dragging) return; var dx = e.clientX - sx, dy = e.clientY - sy; if (Math.abs(dx) + Math.abs(dy) > 3) moved = true; var nx = Math.max(0, Math.min(window.innerWidth - 18, ox + dx)); var ny = Math.max(0, Math.min(window.innerHeight - 18, oy + dy)); dot.style.left = nx + 'px'; dot.style.top = ny + 'px'; }; var dotMouseUp = function () { if (!dragging) return; dragging = false; if (moved) { var rect = dot.getBoundingClientRect(); try { GM_setValue(DOT_POS_KEY, JSON.stringify({ x: Math.round(rect.left), y: Math.round(rect.top) })); } catch (e) { } } else { doSync(false); } }; dot.addEventListener('mousedown', dotMouseDown); document.addEventListener('mousemove', dotMouseMove); document.addEventListener('mouseup', dotMouseUp); document.body.appendChild(dot); // ★ 修复9:注册清理函数,页面卸载时移除事件监听器和DOM addCleanup(function() { dot.removeEventListener('mouseenter', dotMouseEnter); dot.removeEventListener('mouseleave', dotMouseLeave); dot.removeEventListener('mousedown', dotMouseDown); document.removeEventListener('mousemove', dotMouseMove); document.removeEventListener('mouseup', dotMouseUp); if (dot.parentNode) dot.parentNode.removeChild(dot); }); } // ===== XHR 拦截:记录请求头 + 替换关系(静默,不弹Toast)===== // ★ 修复3:添加防重复挂载标记,防止多层hook导致内存泄漏/性能崩溃/跨站污染 if (!window.__cmip_xhr_hooked) { window.__cmip_xhr_hooked = true; (function hookXhr() { try { var origOpen = XMLHttpRequest.prototype.open; var origSetReq = XMLHttpRequest.prototype.setRequestHeader; var origSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function (m, u) { this.__url = u || ''; this.__headers = {}; return origOpen.apply(this, arguments); }; XMLHttpRequest.prototype.setRequestHeader = function (k, v) { if (this.__headers) this.__headers[k] = v; return origSetReq.apply(this, arguments); }; XMLHttpRequest.prototype.send = function (body) { var self = this; var url = this.__url || ''; var isReplacement = /replace|substitute|getReplace|partReplace|替代|替换/i.test(url) && !/getBasePartInfoList/i.test(url) && !/outbound|delivery|stockOut|stockout|getOutbound|getStockOut|出库/i.test(url); // ★★★ v2.9.19关键修复:isStock必须严格排除网点库存/明细账/仓位/DepotInventory相关URL! // 这些URL会被错误识别为厂家库存接口,保存到GM存储后跨站污染,导致所有网站查询失效 var isStock = /stock|getStock|partStock|spareStock|库存|realTime|spareStockReal|qryMengeByMatnr|menge/i.test(url) && !/getBasePartInfoList/i.test(url) && !/inventoryLedger|DepotInventory|depot|明细账|账面|仓位|库存账|outlet|dealer|dlrCode|网点|经销商/i.test(url); if (url && !window.__cmip_xhr_log_count) window.__cmip_xhr_log_count = 0; if (url && window.__cmip_xhr_log_count < 20) { window.__cmip_xhr_log_count++; console.log('[CMIP快查] 📡[xhr] POST ' + url.substring(0, 120)); } if (url.indexOf('getBasePartInfoList') >= 0 || isReplacement || isStock) { var loadHandler = function () { try { var txt = self.responseText; if (!txt) return; // ★ 修复4:先检查是否JSON格式,避免解析HTML/二进制报错 var trimmed = txt.trim(); if (trimmed.charAt(0) !== '{' && trimmed.charAt(0) !== '[') return; var json; try { json = JSON.parse(txt); } catch(e) { return; } // 从 getBasePartInfoList 请求里记录"最近查询的编码" if (url.indexOf('getBasePartInfoList') >= 0 && (json.result === '1' || json.result === 1)) { if (self.__headers) { var hs = {}; var blockList = ['host', 'connection', 'content-length', 'origin', 'referer', 'user-agent', 'accept-encoding']; for (var k in self.__headers) { if (blockList.indexOf(String(k).toLowerCase()) < 0) hs[k] = self.__headers[k]; } saveHeaders(hs); } // 记录最近查询的编码 try { var bodyObjQ = typeof body === 'string' ? JSON.parse(body) : body; if (bodyObjQ && bodyObjQ.partNo) { try { GM_setValue(LAST_CODE_KEY, String(bodyObjQ.partNo)); } catch (e) { } console.log('[CMIP快查] 记录最近查询编码: ' + bodyObjQ.partNo); } } catch (e) { } console.log('[CMIP快查] ✅ 已记录请求头(静默)'); } if (isReplacement && (json.result === '1' || json.result === 1)) { console.log('[CMIP快查] 📋 替换关系接口命中: ' + url.substring(0, 80)); // 保存替换接口模板(供 ASTP 端主动查询) try { var fullUrl = url; if (url.indexOf('http') !== 0) fullUrl = location.origin + (url.charAt(0) === '/' ? '' : '/') + url; var bodyStr = typeof body === 'string' ? body : (body ? JSON.stringify(body) : '{}'); saveReplApi(fullUrl, 'POST', bodyStr, self.__headers || {}); console.log('[CMIP快查] ✅ 已保存替换接口模板: ' + fullUrl); } catch (e) { } var code = ''; // 方法1:从请求体提取 try { var bodyObj = typeof body === 'string' ? JSON.parse(body) : body; if (bodyObj) { code = bodyObj.partNo || bodyObj.oldPartNo || bodyObj.originPartNo || bodyObj.code || bodyObj.partCode || ''; } } catch (e) { } // 方法2:从响应行提取 if (!code) { try { var d = json.data || {}; for (var k in d) { var blk = d[k]; if (blk && Array.isArray(blk.rows) && blk.rows.length > 0) { var r0 = blk.rows[0]; code = String(r0.oldPartNo || r0.originPartNo || r0.sourcePartNo || r0.partNo || r0.origPartNo || ''); break; } } } catch (e) { } } // 方法3:用最近查询的编码 if (!code) { try { code = GM_getValue(LAST_CODE_KEY, '') || ''; } catch (e) { } console.log('[CMIP快查] 替换关系用最近查询编码: ' + code); } if (code) { // 响应结构校验:检查 rows[0] 是否包含替换相关字段 var rows = null; try { var d = json.data || {}; for (var sk in d) { if (d[sk] && Array.isArray(d[sk].rows) && d[sk].rows.length > 0) { rows = d[sk].rows; break; } } } catch (e) { } if (rows && rows.length > 0) { var r0 = rows[0]; var allKeys = Object.keys(r0); var hasReplaceField = false; // 检查是否包含替换关系相关字段 for (var ki = 0; ki < allKeys.length; ki++) { var kn = allKeys[ki].toLowerCase(); if (/newpart|replacepart|substitutepart|oldpart|originpart|sourcepart|replace|substitute|替换|替代|项目类别/.test(kn)) { hasReplaceField = true; break; } } // 检查字段值是否包含替换/替代关键词 if (!hasReplaceField) { for (var vi = 0; vi < allKeys.length; vi++) { var val = String(r0[allKeys[vi]] || '').toLowerCase(); if (/替换|替代|主替换|次替换/.test(val)) { hasReplaceField = true; break; } } } if (!hasReplaceField) { console.log('[CMIP快查] ⚠ 响应不含替换关系字段,跳过保存(可能是出库单/其他业务数据)'); console.log('[CMIP快查] rows[0]字段: ' + allKeys.join(', ')); return; } } saveReplacement(code, json); console.log('[CMIP快查] ✅ 记录替换关系 code=' + code + ' rows=' + rows.length + ' 已校验结构'); } else { console.log('[CMIP快查] ⚠ 替换关系无法确定编码,数据未保存'); } } // 拦截库存接口,保存真实 URL if (isStock && (json.result === '1' || json.result === 1)) { try { var stockFullUrl = url; if (url.indexOf('http') !== 0) stockFullUrl = location.origin + (url.charAt(0) === '/' ? '' : '/') + url; var stockBodyStr = typeof body === 'string' ? body : (body ? JSON.stringify(body) : '{}'); GM_setValue('cmip_stock_api_v2', JSON.stringify({ url: stockFullUrl, bodyTemplate: stockBodyStr })); console.log('[CMIP快查] 📦 已保存库存接口URL: ' + stockFullUrl); // 打印响应结构帮助诊断 try { var sd = json.data || {}; var sKeys = Object.keys(sd); for (var si = 0; si < sKeys.length; si++) { var sBlk = sd[sKeys[si]]; if (sBlk && Array.isArray(sBlk.rows) && sBlk.rows.length > 0) { console.log('[CMIP快查] 📦 库存data.' + sKeys[si] + '.rows=' + sBlk.rows.length + ' 首行字段=' + Object.keys(sBlk.rows[0]).join(',')); break; } } } catch (e) { } } catch (e) { } } } catch (e) { console.log('[CMIP快查] XHR处理异常: ' + e.message); } }; this.addEventListener('load', loadHandler); } return origSend.apply(this, arguments); }; console.log('[CMIP快查] ✅ XHR 拦截器已安装(防重复)'); } catch (e) { console.log('[CMIP快查] hook XHR失败:' + e.message); } })(); } // ===== fetch 拦截:CMIP 页面可能用 fetch 而非 XHR ===== // ★ 修复5:添加防重复挂载标记 + Promise错误捕获 if (!window.__cmip_fetch_hooked) { window.__cmip_fetch_hooked = true; (function hookFetch() { try { var origFetch = window.fetch; if (!origFetch) return; window.fetch = function(input, init) { var url = ''; try { if (typeof input === 'string') url = input; else if (input && input.url) url = input.url; } catch (e) { } var body = init && init.body ? init.body : ''; if (url && (init && init.method === 'POST' || !init)) { // ★★★ v2.9.19关键修复:isStock严格排除网点库存相关URL(同XHR hook) var isStock = /stock|getStock|partStock|spareStock|库存|realTime|spareStockReal|qryMengeByMatnr|menge/i.test(url) && !/inventoryLedger|DepotInventory|depot|明细账|账面|仓位|库存账|outlet|dealer|dlrCode|网点|经销商|qry.*Inventory/i.test(url); var isReplacement = /replace|substitute|getReplace|partReplace|替代|替换/i.test(url) && !/getBasePartInfoList/i.test(url) && !/outbound|delivery|stockOut/i.test(url); var isPartList = url.indexOf('getBasePartInfoList') >= 0; if (isStock || isReplacement || isPartList) { return origFetch.apply(this, arguments).then(function(resp) { var clone = resp.clone(); clone.text().then(function(txt) { try { var trimmed = txt.trim(); if (trimmed.charAt(0) !== '{' && trimmed.charAt(0) !== '[') return; var json; try { json = JSON.parse(txt); } catch(e) { return; } if (json.result === '1' || json.result === 1) { if (isStock) { var stockFullUrl = url.indexOf('http') === 0 ? url : location.origin + (url.charAt(0) === '/' ? '' : '/') + url; GM_setValue('cmip_stock_api_v2', JSON.stringify({ url: stockFullUrl, bodyTemplate: body })); } if (isReplacement) { var replFullUrl = url.indexOf('http') === 0 ? url : location.origin + (url.charAt(0) === '/' ? '' : '/') + url; saveReplApi(replFullUrl, 'POST', body, {}); } } } catch (e) { } }).catch(function() { /* ★ 修复:捕获Promise错误 */ }); return resp; }).catch(function(err) { console.log('[CMIP快查] fetch拦截错误: ' + err.message); return origFetch.apply(this, arguments); }); } if (!window.__cmip_fetch_log_count) window.__cmip_fetch_log_count = 0; if (window.__cmip_fetch_log_count < 20) { window.__cmip_fetch_log_count++; } } return origFetch.apply(this, arguments); }; console.log('[CMIP快查] ✅ fetch 拦截器已安装(防重复)'); } catch (e) { console.log('[CMIP快查] hook fetch失败:' + e.message); } })(); } var tries = 0; var tid = setInterval(function () { tries++; if (document.body) { injectDot(); clearInterval(tid); setTimeout(function () { doSync(true, function (ok) { if (!ok) console.log('[CMIP快查] ⏰ 首次同步失败,30秒后重试...'); }); }, 600); // ★ 修复6:保存定时器引用,页面卸载时清理,防止泄漏 var syncInterval = setInterval(function () { console.log('[CMIP快查] ⏰ 25分钟到,后台自动重同步Token'); doSync(true); }, 25 * 60 * 1000); addCleanup(function() { clearInterval(syncInterval); }); } else if (tries > 30) { clearInterval(tid); } }, 200); addCleanup(function() { clearInterval(tid); }); // ===== CMIP 端查询基础信息+库存(公共函数,修复卡死 bug)===== function buildCmipHeaders(authVal) { var h = {}; try { var saved = JSON.parse(GM_getValue(HEADER_KEY, '{}') || '{}'); for (var k in saved) h[k] = saved[k]; } catch (e) { } h['Content-Type'] = 'application/json;charset=UTF-8'; h['Accept'] = 'application/json, text/plain, */*'; if (authVal) { var fmt = getAuthFmt(); if (fmt) h['Authorization'] = fmt.replace('{token}', authVal).replace(/%7Btoken%7D/g, authVal); else h['Authorization'] = authVal; } return h; } function fetchBaseInfo(code, authVal, callback) { var hdrs = buildCmipHeaders(authVal); var body = JSON.stringify({ partNo: code, pageSize: -1, pageIndex: 1 }); console.log('[CMIP快查] 🔍 查询配件基础信息 code=' + code + ' url=' + CMIP_API_URL); GM_xmlhttpRequest({ method: 'POST', url: CMIP_API_URL, headers: hdrs, data: body, timeout: 10000, withCredentials: true, anonymous: false, onload: function (det) { try { var txt = det.responseText || ''; if (!txt) { callback({ fields: {} }); return; } var httpStatus = det.status || 0; if (httpStatus === 401 || httpStatus === 403 || httpStatus === 302 || httpStatus >= 500) { console.log('[CMIP快查] 🔍 基础信息HTTP错误: ' + httpStatus); callback({ fields: {}, error: 'HTTP ' + httpStatus }); return; } var json = JSON.parse(txt); var out = {}; var row = null; try { var rows = json.data.getBasePartInfoList.rows; if (rows && rows.length) { var up = String(code).toUpperCase(); for (var i = 0; i < rows.length; i++) { if (rows[i] && rows[i].partNo && String(rows[i].partNo).toUpperCase() === up) { row = rows[i]; break; } } if (!row && rows[0]) row = rows[0]; } } catch (e) { } if (!row) { try { var d = json.data; if (d && typeof d === 'object') { for (var k in d) { var blk = d[k]; if (blk && Array.isArray(blk.rows) && blk.rows.length && blk.rows[0].partNo) { row = blk.rows[0]; break; } } } } catch (e) { } } if (row) { var NAME_KEYS = ['partName', 'partNameCn', 'sparePartName', 'name', 'materialName']; for (var ni = 0; ni < NAME_KEYS.length; ni++) { if (row[NAME_KEYS[ni]] !== undefined && row[NAME_KEYS[ni]] !== null && row[NAME_KEYS[ni]] !== '') { out['备件名称'] = row[NAME_KEYS[ni]]; break; } } var CODE_KEYS = ['partNo', 'partCode', 'sparePartCode', 'code', 'materialCode']; for (var ci = 0; ci < CODE_KEYS.length; ci++) { if (row[CODE_KEYS[ci]] !== undefined && row[CODE_KEYS[ci]] !== null && row[CODE_KEYS[ci]] !== '') { out['备件编码'] = row[CODE_KEYS[ci]]; break; } } console.log('[CMIP快查] 🔍 基础信息解析成功: ' + JSON.stringify(out)); } else { console.log('[CMIP快查] 🔍 基础信息无匹配行,result=' + json.result + ' msg=' + (json.msg || '')); } callback({ fields: out }); } catch (e) { console.log('[CMIP快查] 🔍 基础信息解析异常: ' + e.message); callback({ fields: {}, error: e.message }); } }, onerror: function () { callback({ fields: {}, error: '网络错误' }); }, ontimeout: function () { callback({ fields: {}, error: '请求超时' }); } }); } // CMIP 端库存查询(IS_CMIP 域内专用,避免与 ASTP 端 buildHeaders 名字冲突) function fetchStockCmip(code, authVal, callback) { var hdrs = buildCmipHeaders(authVal); var upCode = String(code).toUpperCase(); var stockApi = CMIP_STOCK_API_URL; var stockBodyTpl = { partNo: code, iMatnr: code }; // 对象,不再是 JSON 字符串 try { var saved = JSON.parse(GM_getValue('cmip_stock_api_v2', '{}') || '{}'); if (saved && saved.url) { stockApi = saved.url; if (saved.bodyTemplate) { var sBody; try { sBody = JSON.parse(saved.bodyTemplate); } catch (e) { // ★ 关键:form-urlencoded 字符串模板,解析成对象(之前老版本 parse 失败被 catch 静默吞,模板完全作废) sBody = {}; var ss = String(saved.bodyTemplate).split('&'); for (var si = 0; si < ss.length; si++) { var kv = ss[si].split('='); if (kv.length >= 2) sBody[decodeURIComponent(kv[0])] = decodeURIComponent(kv.slice(1).join('=')); } } var PART_CODE_FIELDS = ['iMatnr', 'partNo', 'matnr', 'sparePartCode', 'partCode', 'code']; for (var fi = 0; fi < PART_CODE_FIELDS.length; fi++) if (sBody[PART_CODE_FIELDS[fi]] !== undefined) sBody[PART_CODE_FIELDS[fi]] = code; stockBodyTpl = sBody; } } } catch (e) { } // 辅助:form-urlencoded / 对象互转 function toObj(bl) { if (bl && typeof bl === 'object') return bl; try { return JSON.parse(bl); } catch (ex) { var o={}; var sp=String(bl).split('&'); for(var s2=0;s2=2)o[decodeURIComponent(kv2[0])]=decodeURIComponent(kv2.slice(1).join('='));} return o; } } function objToForm(obj) { var p=[]; Object.keys(obj||{}).forEach(function(k){var v=(obj[k]===undefined||obj[k]===null)?'':String(obj[k]); p.push(encodeURIComponent(k)+'='+encodeURIComponent(v));}); return p.join('&'); } var FORMAT_NAMES = ['JSON','FORM','GET']; // ★ fallback 顺序:纯编号优先,保存模板次之,P/D/纯partNo 兜底 var FALLBACK = [ { url: CMIP_STOCK_API_URL, buildBody: function (c) { return toObj({ partNo: c, iMatnr: c }); }, tag: '纯编号' }, { url: stockApi, buildBody: function () { return toObj(stockBodyTpl); }, tag: '保存模板' }, { url: 'https://cmip.dpca.com.cn/pac/PacPaBuTb2a0/qryMengeByMatnr.do', buildBody: function (c) { return toObj({ orgBrand: 'P', dlrCode: '', iMatnr: c }); }, tag: '品牌P' }, { url: 'https://cmip.dpca.com.cn/pac/PacPaBuTb2a0/qryMengeByMatnr.do', buildBody: function (c) { return toObj({ orgBrand: 'D', dlrCode: '', iMatnr: c }); }, tag: '品牌D' }, { url: 'https://cmip.dpca.com.cn/pac/PacPaBuTb2a0/qryMengeByMatnr.do', buildBody: function (c) { return toObj({ partNo: c }); }, tag: '仅partNo' } ]; console.log('[CMIP快查] 📦[CMIP端] 查询库存 code=' + code + ' fallback轮数=' + FALLBACK.length + ' 每轮3种传输格式(JSON/FORM/GET)'); var idx = 0; function tryNext(lastErr) { idx++; if (idx < FALLBACK.length) tryApi(FALLBACK[idx].url, FALLBACK[idx].buildBody(code), FALLBACK[idx].tag, 0); else callback({ error: lastErr || '接口返回格式未识别,无法确定库存', httpStatus: 0 }); } function parseBoolVal(bv) { if (bv === true || bv === 'true' || bv === 1 || bv === '1') return { matched: true, has: true }; if (bv === false || bv === 'false' || bv === 0 || bv === '0') return { matched: true, has: false }; if (typeof bv === 'string') { var t = bv.trim(); if (/^有$|^是$|^正常$|^可用$|^存在$|^yes$|^Y$/i.test(t)) return { matched: true, has: true }; if (/^无$|^否$|^没有$|^不可用$|^不存在$|^no$|^N$/i.test(t)) return { matched: true, has: false }; if (t.indexOf('有') >= 0 && t.indexOf('无') < 0) return { matched: true, has: true }; if (t.indexOf('无') >= 0 && t.indexOf('有') < 0) return { matched: true, has: false }; var n = Number(t); if (!isNaN(n)) return { matched: true, has: (n > 0) }; } if (typeof bv === 'number' && !isNaN(bv)) return { matched: true, has: (bv > 0) }; return { matched: false }; } function tryApi(url, bodyObj, tag, fmt) { fmt = fmt || 0; var fullTag = tag + '(' + FORMAT_NAMES[fmt] + ')'; var hclone = {}; for (var hk in hdrs) hclone[hk] = hdrs[hk]; var method, bodyData, finalUrl = url; if (fmt === 0) { method = 'POST'; hclone['Content-Type'] = 'application/json;charset=UTF-8'; bodyData = JSON.stringify(bodyObj||{}); } else if (fmt === 1) { method = 'POST'; hclone['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8'; bodyData = objToForm(bodyObj); } else { method = 'GET'; var qs = objToForm(bodyObj); if (qs) finalUrl = url + (url.indexOf('?') >= 0 ? '&' : '?') + qs; bodyData = undefined; if (hclone['Content-Type']) delete hclone['Content-Type']; } var isLastRound = (idx >= FALLBACK.length - 1); function retryOrNext(lastErr) { if (fmt < 2) { console.log('[CMIP快查] 🔄[CMIP端][' + fullTag + '] 失败: ' + lastErr + ' → 同轮换 ' + FORMAT_NAMES[fmt+1]); tryApi(url, bodyObj, tag, fmt + 1); } else { console.log('[CMIP快查] ⏭[CMIP端][' + fullTag + '] 3种格式全失败 → 下一轮fallback'); if (!isLastRound) tryNext(lastErr); else callback({ error: lastErr || '接口返回格式未识别,无法确定库存', httpStatus: 0 }); } } var req = { method: method, url: finalUrl, headers: hclone, timeout: 12000, withCredentials: true, anonymous: false }; if (bodyData !== undefined) req.data = bodyData; GM_xmlhttpRequest(Object.assign({}, req, { onload: function (det) { try { var txt = det.responseText || ''; var hs = det.status || 0; console.log('[CMIP快查] 📦[CMIP端][' + fullTag + '] 响应 HTTP=' + hs + ' len=' + txt.length); if (hs === 401 || hs === 403) { var te = 'HTTP ' + hs + ' ' + (hs === 401 ? '未授权(Token可能过期)' : '被拦截(需重新登录CMIP)'); callback({ error: te, httpStatus: hs, tokenInvalid: true }); return; } if (hs === 302 || hs >= 500) { var se = 'HTTP ' + hs + ' ' + (hs >= 500 ? '服务器异常' : '重定向(可能未登录)'); retryOrNext(se); return; } if (!txt) { retryOrNext('响应体为空'); return; } var j; try { j = JSON.parse(txt); } catch (ex) { console.log('[CMIP快查] 📦[CMIP端][' + fullTag + '] 非JSON前缀: ' + txt.substring(0, 200)); retryOrNext('响应非JSON格式'); return; } console.log('[CMIP快查] 📦 库存响应[' + idx + ']' + fullTag + ' HTTP=' + hs + ' result=' + j.result + ' msg=' + (j.msg || '') + ' len=' + txt.length); if (j.result !== undefined && String(j.result) !== '1' && String(j.result) !== '0' && String(j.result) !== '200') { var rc = String(j.result); if (['-1', '-100', 'l9900000001', 'l9900000002', 'l9900000003', '9999'].indexOf(rc) >= 0 || /login|auth|token|未登录|认证|权限|l990/i.test(String(j.msg || ''))) { callback({ error: 'Token无效或过期(result=' + rc + ' msg=' + (j.msg || '') + '),请点CMIP右上角绿点重新同步', httpStatus: 401, tokenInvalid: true }); return; } } var BOOL_FIELD_NAMES = ['hasStock','hasStk','isStock','stockFlag','stockStatus','hasSpare','hasInventory','stockExist','hasStockQty','stock','isHasStock','IS_MENGE','isMenge','menge','是否有库存','是否有','库存状态','状态','是否库存','有库存','库存']; var QTY_FIELD_NAMES = ['stockQty','quantity','stock','qty','availableQty','stockNum','totalStock','stockAmount','canUseQty','usableQty','availQty','onHandQty','stockCount','stockTotal','realStock','可用库存','实际库存','库存数量','厂家数量','数量','库存量']; // 1) 顶层 hasStock 布尔字段 for (var tsi = 0; tsi < BOOL_FIELD_NAMES.length; tsi++) { var tv = j[BOOL_FIELD_NAMES[tsi]]; if (tv !== undefined && tv !== null) { var p = parseBoolVal(tv); if (p.matched) { console.log('[CMIP快查] 📦 顶层字段' + BOOL_FIELD_NAMES[tsi] + '=' + tv + ' → ' + (p.has?'有':'无')); callback({ hasStock: p.has, qty: null, round: idx, total: FALLBACK.length, tag: fullTag }); return; } } } var d = j.data || {}; if (d && typeof d === 'object' && d.hasStock !== undefined && d.hasStock !== null) { var p2 = parseBoolVal(d.hasStock); if (p2.matched) { console.log('[CMIP快查] 📦 data.hasStock=' + d.hasStock + ' → ' + (p2.has?'有':'无')); callback({ hasStock: p2.has, qty: null, round: idx, total: FALLBACK.length, tag: fullTag }); return; } } // 2) ★ 核心修复:遍历 rows 时必须先按 partNo 精确匹配到目标行,再读库存字段 var matchedRow = null, matchedKey = null; if (d && typeof d === 'object') { for (var k in d) { var blk = d[k]; if (blk && Array.isArray(blk.rows) && blk.rows.length > 0) { for (var ri = 0; ri < blk.rows.length; ri++) { var r = blk.rows[ri]; var rPartVal = r.partNo || r.partCode || r.sparePartCode || r.code || r.materialCode || r.iMatnr || r.matnr; if (rPartVal !== undefined && rPartVal !== null && String(rPartVal).toUpperCase() === upCode) { matchedRow = r; matchedKey = k; break; } } if (matchedRow) break; } } if (!matchedRow) { for (var k2 in d) { var blk2 = d[k2]; if (blk2 && Array.isArray(blk2.rows)) { if (blk2.rows.length === 1) { matchedRow = blk2.rows[0]; matchedKey = k2; console.log('[CMIP快查] ⚠ 无精确partNo匹配,但rows只有1行,假设匹配'); break; } if (blk2.rows.length > 1) console.log('[CMIP快查] ⚠ rows=' + blk2.rows.length + ' 多行,未精确匹配到partNo=' + upCode + ',跳过此blk'); } } } } if (matchedRow) { var rks = Object.keys(matchedRow); var diagArr = []; for (var di = 0; di < rks.length; di++) diagArr.push(rks[di] + '=' + matchedRow[rks[di]]); console.log('[CMIP快查] 📦 匹配行(' + matchedKey + ') 字段: ' + diagArr.join(' | ')); for (var bi = 0; bi < rks.length; bi++) { var bkn = rks[bi], bknL = bkn.toLowerCase(); var isBoolField = false; for (var bfi = 0; bfi < BOOL_FIELD_NAMES.length; bfi++) { if (bknL === String(BOOL_FIELD_NAMES[bfi]).toLowerCase() || bknL.indexOf(String(BOOL_FIELD_NAMES[bfi]).toLowerCase()) >= 0) { isBoolField = true; break; } } if (isBoolField || /stock|menge|是否有|库存/i.test(bknL)) { var pr = parseBoolVal(matchedRow[bkn]); if (pr.matched) { console.log('[CMIP快查] 📦 字段' + bkn + '=' + matchedRow[bkn] + ' → ' + (pr.has?'有':'无')); callback({ hasStock: pr.has, qty: null, round: idx, total: FALLBACK.length, tag: fullTag }); return; } } } for (var qi = 0; qi < QTY_FIELD_NAMES.length; qi++) { var qv = matchedRow[QTY_FIELD_NAMES[qi]]; if (qv === undefined || qv === null || qv === '') continue; var num = Number(qv); if (!isNaN(num)) { console.log('[CMIP快查] 📦 数量字段' + QTY_FIELD_NAMES[qi] + '=' + qv + ' → ' + (num > 0 ? '有' : '无')); callback({ hasStock: num > 0, qty: num, round: idx, total: FALLBACK.length, tag: fullTag }); return; } } for (var qi2 = 0; qi2 < rks.length; qi2++) { var knl2 = rks[qi2].toLowerCase(); if (/stock|qty|quantity|avail|num|count|total|amount|realstock|库存|数量/i.test(knl2)) { var qv2 = matchedRow[rks[qi2]]; if (qv2 !== undefined && qv2 !== null && qv2 !== '') { var num2 = Number(qv2); if (!isNaN(num2)) { console.log('[CMIP快查] 📦 匹配数量字段' + rks[qi2] + '=' + qv2 + ' → ' + (num2 > 0 ? '有' : '无')); callback({ hasStock: num2 > 0, qty: num2, round: idx, total: FALLBACK.length, tag: fullTag }); return; } } } } console.log('[CMIP快查] ⚠[' + fullTag + '] 匹配行未找到库存字段 → 同轮换传输格式'); } retryOrNext('响应中找不到库存字段或 rows 不匹配'); } catch (e) { retryOrNext('解析异常:' + e.message); } }, onerror: function (er) { console.log('[CMIP快查] 📦[CMIP端][' + fullTag + '] 网络级失败(详细): ' + (er ? JSON.stringify(er).substring(0, 200) : '未知错误')); retryOrNext('网络请求失败'); }, ontimeout: function () { console.log('[CMIP快查] 📦[CMIP端][' + fullTag + '] 请求超时'); retryOrNext('请求超时(12秒)'); } })); } tryApi(FALLBACK[0].url, FALLBACK[0].buildBody(code), FALLBACK[0].tag, 0); } // ====== 功能A:提取 CMIP 右上角网点编码(如 60050W / 71028D)====== var OUTLET_CODE_KEY = 'cmip_outlet_code_v1'; function getOutletCode() { try { var allText = ''; try { // ★ 修复7:缩小选择器范围(移除 span,div,p,a 这些通配符,会选择页面上几乎所有元素!),限制最多扫描50个节点 var nodes = document.querySelectorAll('.el-dropdown, .header, .top-header, .navbar, .user-info, [class*=user], [class*=dealer], [class*=company], [class*=org]'); var scanLimit = Math.min(nodes.length, 50); for (var i = 0; i < scanLimit; i++) { var txt = (nodes[i].innerText || nodes[i].textContent || '').trim(); if (txt && txt.length < 80) allText += txt + '\n'; } } catch (e) { } var m = allText.match(/\(([A-Z0-9]{5,6})\)/); if (!m) { var m2 = allText.match(/(?:网点编码|dlrCode|经销商代码|网点代码|dealerCode)[^\w]{0,4}([A-Z0-9]{5,6})/i); if (m2) m = m2; } if (!m) { try { var uw = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window; var candidates = [uw.userInfo, uw.$store && uw.$store.state && uw.$store.state.user, uw.__INITIAL_STATE__, uw.user, uw.cmipUser]; for (var ci = 0; ci < candidates.length; ci++) { var obj = candidates[ci]; if (!obj) continue; var jstr = typeof obj === 'string' ? obj : JSON.stringify(obj); var jm = jstr.match(/(?:dlrCode|dealerCode|outletCode|网点编码|网点代码|orgCode|deptCode)["': ]{1,4}([A-Z0-9]{5,6})/); if (jm) { m = jm; break; } } } catch (e) { } } if (!m) try { // ★ 修复8:限制localStorage遍历数量,最多20个key var lsKeys = Object.keys(window.localStorage || {}); var lsLimit = Math.min(lsKeys.length, 20); for (var li = 0; li < lsLimit; li++) { try { var lv = window.localStorage.getItem(lsKeys[li]) || ''; var lm = lv.match(/(?:dlrCode|dealerCode|outletCode|网点编码|网点代码)["': ]{1,4}([A-Z0-9]{5,6})/); if (lm) { m = lm; break; } } catch (e) { } } } catch (e) { } if (m && m[1]) { try { GM_setValue(OUTLET_CODE_KEY, m[1]); } catch (e) { } console.log('[CMIP快查] 🏪 检测到网点编码: ' + m[1]); return m[1]; } } catch (e) { console.log('[CMIP快查] 🏪 提取网点编码异常: ' + e.message); } try { var last = GM_getValue(OUTLET_CODE_KEY, '') || ''; if (last) return last; } catch (e) { } return ''; } function isCmipLoggedIn() { try { var tok = getToken(); if (tok) return true; var uw = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window; if (uw && uw.location && uw.location.href && uw.location.href.indexOf('cmip.dpca.com.cn') >= 0) { // 如果能找到登录后的页面元素(用户信息/退出按钮等)也判定已登录 if (getOutletCode()) return true; var txt = document.body ? (document.body.innerText || '') : ''; if (/退出|注销|切换用户|我的账户|个人中心/.test(txt) && /登录/.test(txt) === false) return true; } } catch (e) { } return false; } // ★★★ 已删除网点库存查询功能(v2.9.19修复)★★★ // 删除原因:网点库存接口URL被错误保存为厂家库存接口,导致跨站污染,所有网站查询失效 // CMIP 端放大镜:仅库存快查功能(无详情弹窗,无网点库存)—— 维修工单风格UI (function injectCmipStockSearch() { var STOCK_ICON_ID = 'cmip-stock-icon'; var STOCK_PANEL_ID = 'cmip-stock-panel'; var STOCK_POS_KEY = 'cmip_stock_icon_pos'; var STOCK_PANEL_POS_KEY = 'cmip_stock_panel_pos'; var panelOpen = false; try { var oldIcons = document.querySelectorAll('[id^="cmip-stock-icon-"]'); for (var oi = 0; oi < oldIcons.length; oi++) { if (oldIcons[oi].id !== STOCK_ICON_ID && oldIcons[oi].parentNode) oldIcons[oi].parentNode.removeChild(oldIcons[oi]); } } catch (e) { } if (document.getElementById(STOCK_ICON_ID)) return; var icon = document.createElement('div'); icon.id = STOCK_ICON_ID; icon.title = '厂家是否有库存:点击输入备件编号'; var savedPos = null; try { savedPos = JSON.parse(GM_getValue(STOCK_POS_KEY, '') || 'null'); } catch (e) { } if (!savedPos || typeof savedPos.x !== 'number') savedPos = { x: Math.round(window.innerWidth / 2 - 14), y: 60 }; icon.style.cssText = [ 'position:fixed', 'z-index:2147483645', 'width:28px', 'height:28px', 'left:' + savedPos.x + 'px', 'top:' + savedPos.y + 'px', 'background:transparent', 'border-radius:50%', 'cursor:pointer', 'box-shadow:none', 'display:flex', 'align-items:center', 'justify-content:center', 'user-select:none', 'transition:transform .15s' ].join(';') + ';'; icon.innerHTML = '' + '' + ''; // ★ 修复11:所有事件监听器使用命名函数,便于清理 var iconMouseEnter = function () { icon.style.transform = 'scale(1.15)'; }; var iconMouseLeave = function () { icon.style.transform = 'scale(1)'; }; icon.addEventListener('mouseenter', iconMouseEnter); icon.addEventListener('mouseleave', iconMouseLeave); var dragging = false, moved = false, sx = 0, sy = 0, ox = 0, oy = 0; // ★ 修复12:弹窗拖动状态和监听器(只绑定一次,通过currentPanel跟踪) var currentPanel = null; var pDraging = false, pDx = 0, pDy = 0, pOx = 0, pOy = 0, pPw = 310; var iconMouseDown = function (e) { dragging = true; moved = false; sx = e.clientX; sy = e.clientY; var rect = icon.getBoundingClientRect(); ox = rect.left; oy = rect.top; e.preventDefault(); e.stopPropagation(); }; var iconMouseMove = function (e) { if (dragging) { var dx = e.clientX - sx, dy = e.clientY - sy; if (Math.abs(dx) + Math.abs(dy) > 3) moved = true; var nx = Math.max(0, Math.min(window.innerWidth - 32, ox + dx)); var ny = Math.max(0, Math.min(window.innerHeight - 32, oy + dy)); icon.style.left = nx + 'px'; icon.style.top = ny + 'px'; } else if (pDraging && currentPanel) { var pnx = Math.max(10, Math.min(window.innerWidth - pPw - 10, pOx + (e.clientX - pDx))); var pny = Math.max(10, Math.min(window.innerHeight - 60, pOy + (e.clientY - pDy))); currentPanel.style.left = pnx + 'px'; currentPanel.style.top = pny + 'px'; } }; var iconMouseUp = function () { if (dragging) { dragging = false; if (moved) { var rect = icon.getBoundingClientRect(); try { GM_setValue(STOCK_POS_KEY, JSON.stringify({ x: Math.round(rect.left), y: Math.round(rect.top) })); } catch (e) { } } else { if (panelOpen) closePanel(); else openPanel(); } } else if (pDraging && currentPanel) { pDraging = false; var prect2 = currentPanel.getBoundingClientRect(); try { GM_setValue(STOCK_PANEL_POS_KEY, JSON.stringify({ x: Math.round(prect2.left), y: Math.round(prect2.top) })); } catch (e) { } } }; var panelMouseDown = function (e) { if (!currentPanel) return; pDraging = true; pDx = e.clientX; pDy = e.clientY; var prect = currentPanel.getBoundingClientRect(); pOx = prect.left; pOy = prect.top; e.preventDefault(); e.stopPropagation(); }; icon.addEventListener('mousedown', iconMouseDown); document.addEventListener('mousemove', iconMouseMove); document.addEventListener('mouseup', iconMouseUp); document.body.appendChild(icon); // ★ 修复13:页面卸载时清理icon相关DOM和监听器 addCleanup(function() { icon.removeEventListener('mouseenter', iconMouseEnter); icon.removeEventListener('mouseleave', iconMouseLeave); icon.removeEventListener('mousedown', iconMouseDown); document.removeEventListener('mousemove', iconMouseMove); document.removeEventListener('mouseup', iconMouseUp); if (icon.parentNode) icon.parentNode.removeChild(icon); }); function closePanel() { try { var pp = document.getElementById(STOCK_PANEL_ID); if (pp && pp.parentNode) pp.parentNode.removeChild(pp); // ★ 清理拖动引用 currentPanel = null; pDraging = false; } catch (e) { } panelOpen = false; } function openPanel() { closePanel(); var token = getToken(); var panel = document.createElement('div'); panel.id = STOCK_PANEL_ID; var ir = icon.getBoundingClientRect(); var pw = 310; // 加宽适应新布局 // 优先用上次保存的位置,没有则显示在图标下方居中 var savedP = null; try { savedP = JSON.parse(GM_getValue(STOCK_PANEL_POS_KEY, '') || 'null'); } catch (e) { } var px, py; if (savedP && typeof savedP.x === 'number') { px = Math.max(10, Math.min(window.innerWidth - pw - 10, savedP.x)); py = Math.max(10, Math.min(window.innerHeight - 260, savedP.y)); } else { px = Math.max(10, Math.min(window.innerWidth - pw - 10, ir.left + ir.width / 2 - pw / 2)); py = ir.bottom + 8; if (py + 260 > window.innerHeight) py = Math.max(10, ir.top - 270); } panel.style.cssText = 'position:fixed;z-index:2147483647;left:' + px + 'px;top:' + py + 'px;width:' + pw + 'px;background:#fff;border:1.5px solid #2ea950;border-radius:8px;box-shadow:0 6px 24px rgba(46,169,80,.18),0 2px 8px rgba(0,0,0,.08);font-family:Microsoft YaHei,-apple-system,BlinkMacSystemFont,sans-serif;font-size:12px;overflow:hidden;'; // 现代化工单风格:白底细标题绿字 + 圆角 + 精致阴影 var html = ''; html += '
'; html += '
'; html += '厂家是否有库存'; html += '
'; html += ''; html += ''; html += '
'; html += '
'; html += '×'; html += '
'; // ===== 单查询面板 ===== html += '
'; // 字段行:查询编号(把查询按钮放在输入框后面,同一行,小按钮) html += '
'; html += '查询编号:'; html += ''; html += ''; html += '
'; // 字段行:配件名称(查询后填充)—— 长文本自动缩字+换行,align-items:flex-start 支持多行 html += '
'; html += '配件名称:'; html += ''; html += '
'; // 字段行:厂家库存(查询后填充),右侧小字标注来源(让用户能察觉不准) html += '
'; html += '厂家库存:'; html += '等待查询'; html += ''; html += '
'; // Token / 错误状态条 html += '
'; html += '
'; // ===== 批量查询面板 ===== html += ''; panel.innerHTML = html; document.body.appendChild(panel); panelOpen = true; // ★ 设置当前panel引用,让统一的mousemove/mouseup监听器处理拖动 currentPanel = panel; pPw = pw; // 弹窗拖动 + 位置记忆(使用已经绑定好的统一监听器,避免重复绑定) var pDrag = panel.querySelector('#' + STOCK_PANEL_ID + '_drag'); if (pDrag) { pDrag.addEventListener('mousedown', panelMouseDown); } panel.querySelector('#' + STOCK_PANEL_ID + '_close').addEventListener('click', function (e) { e.preventDefault(); e.stopPropagation(); closePanel(); }); setTimeout(function () { var oc = function (ev) { if (!panelOpen) { document.removeEventListener('click', oc, true); return; } var pp = document.getElementById(STOCK_PANEL_ID); if (!pp) { document.removeEventListener('click', oc, true); return; } if (ev.target.id === STOCK_ICON_ID || (ev.target.closest && ev.target.closest('#' + STOCK_ICON_ID))) return; if (!pp.contains(ev.target)) { closePanel(); document.removeEventListener('click', oc, true); } }; document.addEventListener('click', oc, true); }, 100); var input = panel.querySelector('#' + STOCK_PANEL_ID + '_input'); if (input) { setTimeout(function () { input.focus(); input.select(); }, 30); input.addEventListener('keydown', function (e) { if (e.keyCode === 13) { e.preventDefault(); doQuery(); } }); } panel.querySelector('#' + STOCK_PANEL_ID + '_btn').addEventListener('click', function (e) { e.stopPropagation(); doQuery(); }); // ===== Tab切换逻辑 ===== var singlePanel = panel.querySelector('#' + STOCK_PANEL_ID + '_singlePanel'); var batchPanel = panel.querySelector('#' + STOCK_PANEL_ID + '_batchPanel'); var tabSingle = panel.querySelector('#' + STOCK_PANEL_ID + '_tabSingle'); var tabBatch = panel.querySelector('#' + STOCK_PANEL_ID + '_tabBatch'); var batchInput = panel.querySelector('#' + STOCK_PANEL_ID + '_batchInput'); var batchTbody = panel.querySelector('#' + STOCK_PANEL_ID + '_batchTbody'); var batchResult = panel.querySelector('#' + STOCK_PANEL_ID + '_batchResult'); var batchProgress = panel.querySelector('#' + STOCK_PANEL_ID + '_batchProgress'); var batchExportBtn = panel.querySelector('#' + STOCK_PANEL_ID + '_batchExport'); var batchStatus = panel.querySelector('#' + STOCK_PANEL_ID + '_batchStatus'); var batchResults = []; // 存储批量查询结果 var batchRunning = false; function switchTab(which) { if (which === 'single') { singlePanel.style.display = 'block'; batchPanel.style.display = 'none'; tabSingle.style.background = '#2ea950'; tabSingle.style.color = '#fff'; tabSingle.style.borderColor = '#2ea950'; tabBatch.style.background = '#fff'; tabBatch.style.color = '#2ea950'; tabBatch.style.borderColor = '#a9dcb4'; pPw = 310; panel.style.width = '310px'; setTimeout(function() { if (input) { input.focus(); input.select(); } }, 30); } else { singlePanel.style.display = 'none'; batchPanel.style.display = 'block'; tabSingle.style.background = '#fff'; tabSingle.style.color = '#2ea950'; tabSingle.style.borderColor = '#a9dcb4'; tabBatch.style.background = '#2ea950'; tabBatch.style.color = '#fff'; tabBatch.style.borderColor = '#2ea950'; pPw = 520; panel.style.width = '520px'; setTimeout(function() { if (batchInput) batchInput.focus(); }, 30); } } tabSingle.addEventListener('click', function(e) { e.stopPropagation(); switchTab('single'); }); tabBatch.addEventListener('click', function(e) { e.stopPropagation(); switchTab('batch'); }); // 初始状态:token 提示 function setStatusBar(text, color, bg) { var sb = panel.querySelector('#' + STOCK_PANEL_ID + '_statusBar'); if (!sb) return; if (!text) { sb.innerHTML = ''; return; } sb.innerHTML = '
' + text + '
'; } if (!token) setStatusBar('⚠ 还没同步Token,请点右上角绿点(橙=同步中,红=失败,绿=成功)', '#e6a23c', '#fff8e1'); function setName(name) { var nm = panel.querySelector('#' + STOCK_PANEL_ID + '_partName'); var row = panel.querySelector('#' + STOCK_PANEL_ID + '_nameRow'); if (nm) { var txt = name || '—'; nm.textContent = txt; // 动态缩字:字符数 <=15 用默认12px;>15 每多3个字符减0.5px,最小9.5px var base = 12, min = 9.5, threshold = 15; var len = String(txt).length; var fs = base; if (len > threshold) { var steps = Math.ceil((len - threshold) / 3); fs = Math.max(min, base - steps * 0.5); } nm.style.fontSize = fs.toFixed(1) + 'px'; } if (row) row.style.opacity = name ? '1' : '.5'; } function setStock(text, color, sourceLabel) { var tx = panel.querySelector('#' + STOCK_PANEL_ID + '_stockText'); if (!tx) return; tx.textContent = text; tx.style.color = color || '#4a4a4a'; tx.style.fontWeight = '600'; // 厂家库存来源标识:小字显示在右侧(让用户能察觉不准) var src = panel.querySelector('#' + STOCK_PANEL_ID + '_stockSrc'); if (src) src.textContent = sourceLabel ? '· ' + sourceLabel : ''; } // ★ setDepot函数已删除(v2.9.19) // 优先从 CMIP 当前页面 DOM 表格读取库存(100% 与页面显示一致) function scanDomStock(code) { try { var up = String(code).toUpperCase(); // 1) 找所有可能的表头,定位"是否有库存"/"厂家库存"/"库存"列 var tables = document.querySelectorAll('table'); for (var ti = 0; ti < tables.length; ti++) { var tbl = tables[ti]; var ths = tbl.querySelectorAll('th'); var colIdx = -1, codeIdx = -1; for (var hi = 0; hi < ths.length; hi++) { var ht = (ths[hi].innerText || ths[hi].textContent || '').replace(/\s+/g, ''); if (/是否有库存|厂家库存|库存状态|是否库存|有库存/.test(ht)) colIdx = hi; if (/备件号|备件编码|配件编号|配件号|零件号|partNo|编号/.test(ht) && ht.length <= 8) codeIdx = hi; } if (colIdx < 0) continue; // 2) 逐行找匹配编号,取库存列文本 var trs = tbl.querySelectorAll('tbody tr, tr'); for (var ri = 0; ri < trs.length; ri++) { var tr = trs[ri]; var tds = tr.querySelectorAll('td'); if (!tds.length) continue; var codeHit = false; for (var ci = 0; ci < tds.length; ci++) { var ct = (tds[ci].innerText || tds[ci].textContent || '').trim().replace(/[^A-Za-z0-9]/g, '').toUpperCase(); if (ct === up) { codeHit = true; break; } } if (!codeHit && codeIdx >= 0 && tds[codeIdx]) { var ct2 = (tds[codeIdx].innerText || tds[codeIdx].textContent || '').trim().replace(/[^A-Za-z0-9]/g, '').toUpperCase(); if (ct2 === up) codeHit = true; } if (codeHit && tds[colIdx]) { var st = (tds[colIdx].innerText || tds[colIdx].textContent || '').replace(/\s+/g, ''); console.log('[CMIP快查] 📦 DOM命中库存:code=' + up + ' 列值="' + st + '"'); if (/有|正常|可用|存在|yes|true|1|Y/i.test(st)) return { hasStock: true, source: 'dom' }; if (/无|没有|不可用|不存在|no|false|0|N/i.test(st)) return { hasStock: false, source: 'dom' }; } } } } catch (e) { console.log('[CMIP快查] 📦 DOM扫描库存异常: ' + e.message); } return null; } function doQuery() { var inp = panel.querySelector('#' + STOCK_PANEL_ID + '_input'); var v = String(inp ? inp.value : '').trim().replace(/[^A-Za-z0-9]/g, ''); if (!v || !CODE_RE.test(v)) { setStatusBar('⚠ 请输入正确的备件编号(4-30位字母数字)', '#e6a23c', '#fff8e1'); return; } var tok = getToken(); if (!tok) { setStatusBar('❌ Token未同步!请先登录CMIP页,点右上角绿点手动同步', '#f56c6c', '#fef0f0'); setStock('—', '#c0c4cc', ''); return; } // ★ 已删除网点库存相关逻辑(v2.9.19) saveLastCode(v); setName('查询中...'); setStock('查询中...', '#909399', ''); setStatusBar('', null, null); // 【优先】从当前页面 DOM 直接读库存,和页面表格完全一致 var domRes = scanDomStock(v); if (domRes && domRes.source === 'dom') { console.log('[CMIP快查] 📦 采用DOM库存结果(与页面一致):' + (domRes.hasStock ? '有' : '无')); } // 1. 查基础信息拿配件名称 fetchBaseInfo(v, tok, function (infoResult) { if (infoResult && infoResult.fields && infoResult.fields['备件名称']) setName(infoResult.fields['备件名称']); else setName('未查到名称'); }); // 2. 厂家库存:DOM命中 → 直接用;否则走API(多轮fallback,带来源标注便于察觉不准) var applyStock = function (has, srcLabel) { if (has === true) { setStock('有', '#2ea950', srcLabel || ''); setStatusBar((srcLabel && srcLabel.indexOf('DOM') >= 0) ? '💡 已从页面读取(100%准确)' : '', null, null); } else if (has === false) { setStock('无', '#f56c6c', srcLabel || ''); setStatusBar((srcLabel && srcLabel.indexOf('DOM') >= 0) ? '💡 已从页面读取(100%准确)' : '', null, null); } }; if (domRes && domRes.source === 'dom') { applyStock(domRes.hasStock, '来自当前页面DOM(100%准确)'); } else { var authToUse = tok; try { var sa2 = JSON.parse(GM_getValue('cmip_auth_v1', '') || '{}'); if (sa2 && sa2.authHeader) authToUse = sa2.authHeader; } catch (e) { } fetchStockCmip(v, authToUse, function (stockRes) { if (!panelOpen) return; if (stockRes && stockRes.error) { var isTokenErr = stockRes.tokenInvalid || /Token|登录|认证|l990|未授权|401|403/i.test(stockRes.error); var msgBox = isTokenErr ? '❌ Token过期或无效:' + stockRes.error + ' → 请点CMIP右上角绿点重新同步' : '⚠ 库存查询失败:' + stockRes.error; var warnColor = isTokenErr ? '#f56c6c' : '#e6a23c'; var warnBg = isTokenErr ? '#fef0f0' : '#fff8e1'; setStatusBar(msgBox, warnColor, warnBg); setStock('待确认', '#909399', '接口失败'); } else if (stockRes && (stockRes.hasStock === true || stockRes.hasStock === false)) { var label = 'API 查' + (stockRes.round !== undefined ? ('(F' + (stockRes.round + 1) + '/' + (stockRes.total || 4) + ')') : ''); if (stockRes.tag) label += '·' + stockRes.tag; applyStock(stockRes.hasStock, label); } else { setStatusBar('⚠ 无法确定库存(接口返回空或格式未识别)', '#e6a23c', '#fff8e1'); setStock('未知', '#909399', '无结果'); } }); } } // ===== 批量查询核心逻辑 ===== // 解析输入文本,提取所有备件编号 function parseBatchCodes(text) { var codes = []; var seen = {}; // 按换行、逗号、空格、制表符分隔 var parts = text.split(/[\n\r,;\t\s]+/); for (var i = 0; i < parts.length; i++) { var cleaned = String(parts[i] || '').replace(/[^A-Za-z0-9]/g, '').toUpperCase(); if (cleaned && cleaned.length >= 4 && CODE_RE.test(cleaned) && !seen[cleaned]) { seen[cleaned] = true; codes.push(cleaned); } } return codes; } // 设置批量状态提示 function setBatchStatus(text, color) { if (!batchStatus) return; batchStatus.textContent = text || ''; batchStatus.style.color = color || '#909399'; } // 更新进度 function updateBatchProgress(done, total) { if (!batchProgress) return; batchProgress.textContent = done + '/' + total; } // 添加结果行到表格 function addResultRow(idx, code, name, hasStock, sourceLabel) { if (!batchTbody) return; var tr = document.createElement('tr'); tr.style.borderBottom = '1px solid #f0f0f0'; var stockText = ''; var stockColor = '#909399'; if (hasStock === true) { stockText = '有'; stockColor = '#2ea950'; } else if (hasStock === false) { stockText = '无'; stockColor = '#f56c6c'; } else { stockText = '查询中'; } tr.innerHTML = '' + idx + '' + '' + code + '' + '' + (name || '—') + '' + '' + stockText + '' + '' + (sourceLabel || '') + ''; batchTbody.appendChild(tr); } // 更新已有结果行 function updateResultRow(idx, name, hasStock, sourceLabel) { if (!batchTbody) return; var rows = batchTbody.querySelectorAll('tr'); if (idx < 0 || idx >= rows.length) return; var tr = rows[idx]; var tds = tr.querySelectorAll('td'); if (tds.length < 5) return; if (name) tds[2].textContent = name; var stockText = ''; var stockColor = '#909399'; if (hasStock === true) { stockText = '有'; stockColor = '#2ea950'; } else if (hasStock === false) { stockText = '无'; stockColor = '#f56c6c'; } else { stockText = '未知'; } tds[3].textContent = stockText; tds[3].style.color = stockColor; tds[4].textContent = sourceLabel || ''; } // 批量查询单个编号(带并发控制) function queryOneBatch(code, idx, total, authToken, callback) { var done = false; var name = ''; var hasStock = null; var sourceLabel = ''; // 1. 先查基础信息 fetchBaseInfo(code, authToken, function(infoResult) { if (infoResult && infoResult.fields && infoResult.fields['备件名称']) { name = infoResult.fields['备件名称']; } else { name = '未查到名称'; } // 2. 再查库存 var authToUse = authToken; try { var sa2 = JSON.parse(GM_getValue('cmip_auth_v1', '') || '{}'); if (sa2 && sa2.authHeader) authToUse = sa2.authHeader; } catch (e) { } fetchStockCmip(code, authToUse, function(stockRes) { if (stockRes && (stockRes.hasStock === true || stockRes.hasStock === false)) { hasStock = stockRes.hasStock; sourceLabel = 'API' + (stockRes.round !== undefined ? ('(F' + (stockRes.round + 1) + ')') : ''); if (stockRes.tag) sourceLabel += '·' + stockRes.tag; } else { hasStock = null; sourceLabel = stockRes && stockRes.error ? '失败' : '无结果'; } done = true; batchResults[idx] = { code: code, name: name, hasStock: hasStock, source: sourceLabel }; updateResultRow(idx, name, hasStock, sourceLabel); updateBatchProgress(idx + 1, total); callback(); }); }); // 超时保护(15秒) setTimeout(function() { if (!done) { done = true; batchResults[idx] = { code: code, name: name || '超时', hasStock: null, source: '超时' }; updateResultRow(idx, name || '超时', null, '超时'); updateBatchProgress(idx + 1, total); callback(); } }, 15000); } // 开始批量查询 function startBatchQuery() { if (batchRunning) return; var tok = getToken(); if (!tok) { setBatchStatus('❌ Token未同步!请先点绿点同步Token', '#f56c6c'); return; } var text = batchInput ? batchInput.value : ''; var codes = parseBatchCodes(text); if (codes.length === 0) { setBatchStatus('⚠ 未找到有效的备件编号', '#e6a23c'); return; } if (codes.length > 100) { setBatchStatus('⚠ 最多支持100个编号,已截断', '#e6a23c'); codes = codes.slice(0, 100); } batchRunning = true; batchResults = []; if (batchTbody) batchTbody.innerHTML = ''; batchResult.style.display = 'block'; batchExportBtn.style.display = 'none'; setBatchStatus('🔍 查询中...并发数2,请勿关闭弹窗', '#409eff'); updateBatchProgress(0, codes.length); // 先添加所有"查询中"的行 for (var i = 0; i < codes.length; i++) { addResultRow(i + 1, codes[i], '查询中...', null, ''); batchResults.push({ code: codes[i], name: '', hasStock: null, source: '' }); } // 并发控制:同时最多2个请求 var concurrency = 2; var current = 0; var completed = 0; function next() { if (current >= codes.length) return; var idx = current; current++; queryOneBatch(codes[idx], idx, codes.length, tok, function() { completed++; if (completed >= codes.length) { batchRunning = false; var hasCount = 0, noCount = 0, unknownCount = 0; for (var j = 0; j < batchResults.length; j++) { if (batchResults[j].hasStock === true) hasCount++; else if (batchResults[j].hasStock === false) noCount++; else unknownCount++; } setBatchStatus('✅ 查询完成!共' + codes.length + '条:有库存' + hasCount + ',无库存' + noCount + (unknownCount > 0 ? ',未知' + unknownCount : ''), '#2ea950'); batchExportBtn.style.display = 'inline-block'; } else { next(); } }); } // 启动并发 for (var k = 0; k < Math.min(concurrency, codes.length); k++) { setTimeout(next, k * 200); } } // 导出CSV function exportCSV() { if (batchResults.length === 0) return; var BOM = '\uFEFF'; // UTF-8 BOM,解决Excel中文乱码 var csv = BOM + '序号,备件编号,配件名称,厂家库存,来源\n'; for (var i = 0; i < batchResults.length; i++) { var r = batchResults[i]; var stockText = ''; if (r.hasStock === true) stockText = '有'; else if (r.hasStock === false) stockText = '无'; else stockText = '未知'; // 处理CSV中的逗号和引号 var name = String(r.name || '').replace(/"/g, '""'); if (name.indexOf(',') >= 0 || name.indexOf('"') >= 0) name = '"' + name + '"'; csv += (i + 1) + ',' + r.code + ',' + name + ',' + stockText + ',' + (r.source || '') + '\n'; } var blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); var url = URL.createObjectURL(blob); var a = document.createElement('a'); var now = new Date(); var ts = now.getFullYear() + '' + String(now.getMonth() + 1).padStart(2, '0') + String(now.getDate()).padStart(2, '0') + '_' + String(now.getHours()).padStart(2, '0') + String(now.getMinutes()).padStart(2, '0'); a.href = url; a.download = '厂家库存批量查询_' + ts + '.csv'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); setBatchStatus('💾 已导出CSV文件', '#2ea950'); } // 粘贴导入(支持直接从Excel复制粘贴) function handleImport() { if (batchRunning) return; // 尝试读取剪贴板 if (navigator.clipboard && navigator.clipboard.readText) { navigator.clipboard.readText().then(function(text) { if (text) { batchInput.value = text; var codes = parseBatchCodes(text); setBatchStatus('📋 已粘贴内容,识别到' + codes.length + '个编号', '#409eff'); } else { batchInput.focus(); setBatchStatus('💡 请直接粘贴Excel/CSV内容到文本框(Ctrl+V)', '#909399'); } }).catch(function() { batchInput.focus(); setBatchStatus('💡 请直接粘贴Excel/CSV内容到文本框(Ctrl+V)', '#909399'); }); } else { batchInput.focus(); setBatchStatus('💡 请直接粘贴Excel/CSV内容到文本框(Ctrl+V)', '#909399'); } } // 清空 function handleClear() { if (batchRunning) return; if (batchInput) batchInput.value = ''; if (batchTbody) batchTbody.innerHTML = ''; if (batchResult) batchResult.style.display = 'none'; batchExportBtn.style.display = 'none'; batchResults = []; updateBatchProgress(0, 0); setBatchStatus('已清空', '#909399'); if (batchInput) batchInput.focus(); } // 绑定批量按钮事件 panel.querySelector('#' + STOCK_PANEL_ID + '_batchStart').addEventListener('click', function(e) { e.stopPropagation(); startBatchQuery(); }); panel.querySelector('#' + STOCK_PANEL_ID + '_batchImport').addEventListener('click', function(e) { e.stopPropagation(); handleImport(); }); panel.querySelector('#' + STOCK_PANEL_ID + '_batchExport').addEventListener('click', function(e) { e.stopPropagation(); exportCSV(); }); panel.querySelector('#' + STOCK_PANEL_ID + '_batchClear').addEventListener('click', function(e) { e.stopPropagation(); handleClear(); }); // Ctrl+Enter在批量输入框开始查询 if (batchInput) { batchInput.addEventListener('keydown', function(e) { if ((e.ctrlKey || e.metaKey) && e.keyCode === 13) { e.preventDefault(); startBatchQuery(); } }); } } document.addEventListener('keydown', function (e) { if (e.keyCode === 27 && panelOpen) closePanel(); }); console.log('[CMIP快查] 📦 CMIP端库存快查放大镜已注入'); })(); console.log('[CMIP快查] CMIP端就绪:绿色小圆点可拖动,点击同步。查询编码/替换关系时自动记录(无Toast)。'); return; } // ============================================================ // ASTP + ServiceBox 端(双击/三击编码→查CMIP) // ============================================================ if (IS_QUERY_SITE) { var SITE_NAME = IS_SERVICEBOX ? 'ServiceBox' : 'ASTP'; console.log('[CMIP快查] 当前站点: ' + SITE_NAME); // 启动时清理旧的错误替换接口模板(v2.4 可能存了出库单接口) try { var oldReplApi = getReplApi(); if (oldReplApi && oldReplApi.url) { if (/getBuOut|outbound|stockOut|getStockOut|出库|delivery/i.test(oldReplApi.url)) { GM_setValue(REPL_API_KEY, '{}'); console.log('[CMIP快查] 🧹 已清理错误的替换接口模板(出库单接口): ' + oldReplApi.url.substring(0, 80)); } } } catch (e) { } var FIELD_MAP = [ ['备件编码', ['partNo', 'partCode', 'sparePartCode', 'code', 'materialCode']], ['备件名称', ['partName', 'partNameCn', 'sparePartName', 'name', 'materialName']], ['建议零售价', [ 'suggestRetailPrice', 'suggestRetail', 'retailPrice', 'retailAmount', 'salePrice', 'suggestPrice', 'price', 'retail', 'retailPriceTax', 'retailAmountTax', 'salesRetailPrice', 'stdRetailPrice', 'msrp', 'msrpPrice', 'listPrice', 'marketPrice', 'guidePrice', 'taxRetailPrice', 'unTaxRetailPrice', 'advicePrice', 'advisePrice', 'officialPrice', 'retailPriceNoTax', 'retailPriceIncludeTax', 'refPrice', 'referencePrice' ]], ['包装含量', ['packageQty', 'packQty', 'packageContent', 'content', 'unitPackage', 'packageQuantity', 'packCount']], ['基本订货价', [ 'purchasePrice', 'basePrice', 'orderPrice', 'cost', 'supplyPrice', 'purchaseAmount', 'baseAmount', 'purchasePriceTax', 'orderPriceTax', 'stockPrice', 'stdOrderPrice', 'buyPrice', 'procurePrice' ]], ['销售状态', ['saleStateName', 'saleStatus', 'salesStatus', 'status', 'state', 'saleState', 'stateName']], ['备注', ['remarkb', 'remarkB', 'remark', 'remarks', 'remarkText', 'note', 'notes', 'comment', 'comments', 'memo', 'memos', 'description', 'desc', 'detail', 'partRemark', 'partNote', 'extraInfo', 'extra', 'additionalInfo', 'bizRemark', 'businessRemark', 'customRemark', 'customNote', 'bz', 'beizhu', 'bzText', 'remarkInfo']] ]; function pickVal(o, ks) { if (!o) return null; for (var i = 0; i < ks.length; i++) { var v = o[ks[i]]; if (v !== undefined && v !== null && v !== '') return v; } return null; } // 递归搜索嵌套对象中的字段值(最多2层深度) function pickValDeep(o, ks) { if (!o) return null; // 第一层:直接在 o 上找 for (var i = 0; i < ks.length; i++) { var v = o[ks[i]]; if (v !== undefined && v !== null && v !== '' && typeof v !== 'object') return v; } // 第二层:在 o 的子对象中找 for (var k in o) { if (typeof o[k] === 'object' && o[k] !== null && !Array.isArray(o[k])) { for (var j = 0; j < ks.length; j++) { var sv = o[k][ks[j]]; if (sv !== undefined && sv !== null && sv !== '' && typeof sv !== 'object') return sv; } } } return null; } function extractFields(row, code) { if (!row) return null; var out = {}, hasAny = false; var ALL_FIELD_KEYS = []; for (var i = 0; i < FIELD_MAP.length; i++) { var v = pickValDeep(row, FIELD_MAP[i][1]); out[FIELD_MAP[i][0]] = v; if (v !== null && v !== undefined && v !== '') hasAny = true; } if (!out['备件编码']) out['备件编码'] = code; // 诊断:打印 row 中所有包含 remark/备注/note 等关键词的字段 try { var rowKeys = Object.keys(row); var remarkHits = []; var remarkKw = /remarkb|remark|note|memo|comment|desc|detail|info|beizhu|bz|remarkText|remarkContent|remarkInfo|customRemark|remarkName/i; for (var ri = 0; ri < rowKeys.length; ri++) { var rk = rowKeys[ri]; if (remarkKw.test(rk)) { var rv = row[rk]; if (typeof rv !== 'object' && rv !== null && rv !== undefined && rv !== '') { remarkHits.push(rk + '=' + String(rv).substring(0, 60)); } } // 也搜索嵌套对象 var rv2 = row[rk]; if (typeof rv2 === 'object' && rv2 !== null && !Array.isArray(rv2)) { var subKeys = Object.keys(rv2); for (var si = 0; si < subKeys.length; si++) { if (remarkKw.test(subKeys[si])) { var sv = rv2[subKeys[si]]; if (typeof sv !== 'object' && sv !== null && sv !== undefined && sv !== '') { remarkHits.push(rk + '.' + subKeys[si] + '=' + String(sv).substring(0, 60)); } } } } } if (remarkHits.length) console.log('[CMIP快查] 🔍 备注相关字段: ' + remarkHits.join(' | ')); else console.log('[CMIP快查] 🔍 未找到任何备注相关字段(remark/note/memo等)'); } catch (e) { } return hasAny ? out : null; } function findPartRow(json, code) { if (!json) return null; code = String(code).toUpperCase(); try { var rows = json.data.getBasePartInfoList.rows; if (rows && rows.length) { for (var i = 0; i < rows.length; i++) { if (rows[i] && rows[i].partNo && String(rows[i].partNo).toUpperCase() === code) return rows[i]; } if (rows[0] && rows[0].partNo) return rows[0]; } } catch (e) { } try { var d = json.data; if (d && typeof d === 'object') { for (var k in d) { var blk = d[k]; if (blk && Array.isArray(blk.rows) && blk.rows.length && blk.rows[0].partNo) { for (var j = 0; j < blk.rows.length; j++) if (String(blk.rows[j].partNo).toUpperCase() === code) return blk.rows[j]; return blk.rows[0]; } } } } catch (e) { } return null; } // 诊断:把响应里所有看起来像价格/金额的字段都列出来 function diagnosePrices(row) { if (!row) return; var keys = Object.keys(row); var priceFields = []; var priceKeys = /price|amount|money|fee|cost|tax|价|额|费|款|金额|价格|税|金额/i; for (var i = 0; i < keys.length; i++) { var k = keys[i]; var v = row[k]; if ((priceKeys.test(k) || typeof v === 'number') && v !== null && v !== undefined && v !== '') { priceFields.push(k + '=' + v); } } console.log('[CMIP快查] 💰 诊断:该备件行所有数值/价格类字段 -> ' + priceFields.join(', ')); } // 解析替换关系数据 -> 6 列(备件编号、备件名称、零售价、包装含量、基本订货价、销售状态) var REPLACEMENT_COL_MAP = [ ['备件编号', ['newPartNo', 'replacePartNo', 'substitutePartNo', 'partNo', 'replacePartCode', 'newPartCode', 'partCode', 'oldPartNo', 'originPartNo', 'sourcePartNo', 'origPartNo']], ['备件名称', ['newPartName', 'replacePartName', 'substitutePartName', 'partName', 'replacePartNm', 'newPartNm', 'partNm', 'partNameCn', 'sparePartName', 'name', 'materialName']], ['零售价', ['suggestRetailPrice', 'suggestRetail', 'retailPrice', 'retailAmount', 'salePrice', 'suggestPrice', 'price', 'retail', 'retailPriceTax', 'retailAmountTax', 'salesRetailPrice', 'stdRetailPrice', 'msrp', 'msrpPrice', 'listPrice', 'marketPrice', 'guidePrice', 'taxRetailPrice', 'unTaxRetailPrice', 'advicePrice', 'advisePrice', 'officialPrice', 'retailPriceNoTax', 'retailPriceIncludeTax', 'refPrice', 'referencePrice']], ['包装含量', ['packageQty', 'packQty', 'packageContent', 'content', 'unitPackage', 'packageQuantity', 'packCount', 'packageSize', 'boxQty', 'cartonQty', 'packageSpec', 'packSpec']], ['基本订货价', ['purchasePrice', 'basePrice', 'orderPrice', 'cost', 'supplyPrice', 'purchaseAmount', 'baseAmount', 'purchasePriceTax', 'orderPriceTax', 'stockPrice', 'stdOrderPrice', 'buyPrice', 'procurePrice', 'baseOrderPrice', 'orderCost']], ['销售状态', ['saleStateName', 'saleStatus', 'salesStatus', 'status', 'state', 'saleState', 'stateName', 'saleStatusName', 'salesState', 'enableStatus', 'isEnabled', 'saleSwitch']] ]; function pickValSafe(o, ks) { if (!o) return ''; for (var i = 0; i < ks.length; i++) { var v = o[ks[i]]; if (v !== undefined && v !== null && v !== '') { var s = String(v); // 过滤掉乱码/控制字符(只保留可见中英文、数字、常用标点) s = s.replace(/[\x00-\x1F\x7F]/g, '').trim(); if (s.length > 60) s = s.substring(0, 60) + '…'; return s; } } return ''; } function parseReplacementFull(repData) { if (!repData || !repData.data) return null; try { var json = repData.data; var d = json.data || {}; var rows = null; for (var k in d) { var blk = d[k]; if (blk && Array.isArray(blk.rows) && blk.rows.length > 0) { rows = blk.rows; break; } } if (!rows || !rows.length) { console.log('[CMIP快查] 替换关系解析失败:未找到 rows 数组。data下所有key: ' + Object.keys(d).join(', ')); return null; } // 诊断:打印第一行所有字段名和值 var firstRow = rows[0]; var allKeys = Object.keys(firstRow); var diag = []; for (var di = 0; di < allKeys.length; di++) { diag.push(allKeys[di] + '=' + firstRow[allKeys[di]]); } console.log('[CMIP快查] 🔍 替换关系原始字段(' + allKeys.length + '个): ' + diag.join(' | ')); var cols = []; for (var i = 0; i < REPLACEMENT_COL_MAP.length; i++) cols.push(REPLACEMENT_COL_MAP[i][0]); var outRows = []; for (var i = 0; i < rows.length; i++) { var r = rows[i]; var out = []; var matchCount = 0; for (var j = 0; j < REPLACEMENT_COL_MAP.length; j++) { var pv = pickValSafe(r, REPLACEMENT_COL_MAP[j][1]); // 如果在顶层没找到,尝试嵌套对象(如 partInfo/partDetail 等) if (!pv) { var nestedKeys = ['partInfo', 'partDetail', 'partData', 'info', 'detail', 'part', 'replacePart']; for (var ni = 0; ni < nestedKeys.length; ni++) { var nested = r[nestedKeys[ni]]; if (nested && typeof nested === 'object' && !Array.isArray(nested)) { var npv = pickValSafe(nested, REPLACEMENT_COL_MAP[j][1]); if (npv) { pv = npv; break; } } } } if (pv && pv !== '-') matchCount++; out.push(pv); } if (i === 0) console.log('[CMIP快查] 第0行字段匹配情况: 命中' + matchCount + '/' + REPLACEMENT_COL_MAP.length + ' 列值=' + JSON.stringify(out)); if (out.join('').trim() !== '') outRows.push(out); } if (!outRows.length) { console.log('[CMIP快查] ⚠ 替换关系解析0行有效数据,原始行数=' + rows.length + '。请把上面"🔍 替换关系原始字段"日志发给开发者修正字段名'); return null; } return { cols: cols, rows: outRows }; } catch (e) { return null; } } // 替换行缺少价格数据时,批量查询代替编号的基础信息补充价格 function enrichReplacementWithBaseInfo(replData, authVal, callback) { if (!replData || !replData.rows || !replData.rows.length) { callback(replData); return; } var PRICE_COL_NAMES = ['零售价', '包装含量', '基本订货价', '销售状态']; var priceColIdx = []; for (var ci = 0; ci < replData.cols.length; ci++) { if (PRICE_COL_NAMES.indexOf(replData.cols[ci]) >= 0) priceColIdx.push(ci); } if (!priceColIdx.length) { callback(replData); return; } // 检查哪些行缺少价格数据 var missingRows = []; for (var ri = 0; ri < replData.rows.length; ri++) { var row = replData.rows[ri]; var needsLookup = false; for (var pi = 0; pi < priceColIdx.length; pi++) { if (!row[priceColIdx[pi]] || row[priceColIdx[pi]] === '-') { needsLookup = true; break; } } if (needsLookup) { var partNo = row[0]; // 备件编号列 if (partNo) missingRows.push({ ri: ri, partNo: partNo }); } } if (!missingRows.length) { callback(replData); return; } console.log('[CMIP快查] 🔍 替换表有' + missingRows.length + '行缺少价格数据,查询基础信息补充...'); // 分批查询(每批5个) var BATCH_SIZE = 5; var batchIdx = 0; var lookedUp = 0; function doBatch() { if (batchIdx >= missingRows.length) { console.log('[CMIP快查] ✅ 价格补充完成,共查询' + lookedUp + '个代替编号'); callback(replData); return; } var endIdx = Math.min(batchIdx + BATCH_SIZE, missingRows.length); var batch = missingRows.slice(batchIdx, endIdx); var completed = 0; for (var bi = 0; bi < batch.length; bi++) { (function(entry) { var hdrs = buildHeaders(authVal); GM_xmlhttpRequest({ method: 'POST', url: CMIP_API_URL, headers: hdrs, data: JSON.stringify({ partNo: entry.partNo, pageSize: -1, pageIndex: 1 }), timeout: 10000, withCredentials: true, anonymous: false, onload: function (det) { try { var txt = det.responseText || ''; if (!txt) { doneOne(); return; } var json = JSON.parse(txt); if (json.result === '1' || json.result === 1) { var row = findPartRow(json, entry.partNo); if (row) { var extracted = extractFields(row, entry.partNo); if (extracted) { for (var pi = 0; pi < priceColIdx.length; pi++) { var colName = replData.cols[priceColIdx[pi]]; var val = extracted[colName]; if (val !== null && val !== undefined && val !== '') { replData.rows[entry.ri][priceColIdx[pi]] = String(val); } } console.log('[CMIP快查] ✅ 补充 ' + entry.partNo + ' 价格数据'); } } } } catch (e) { } doneOne(); }, onerror: function () { doneOne(); }, ontimeout: function () { doneOne(); } }); })(batch[bi]); } function doneOne() { completed++; lookedUp++; if (completed >= batch.length) { batchIdx = endIdx; setTimeout(doBatch, 200); } } } doBatch(); } // 结果弹窗(小字体 + 可拖动) // targetId: 如果指定,则更新已有弹窗内容(用于子弹窗),不创建新弹窗 function showResult(code, data, errMsg, showOpenBtn, replacements, stockData, targetId) { var panel, isSubPanel = false; if (targetId) { panel = document.getElementById(targetId); if (!panel) return; // 子弹窗已被关闭,跳过 isSubPanel = true; } else { try { var old = document.getElementById('cmip-qq-panel'); if (old && old.parentNode) old.parentNode.removeChild(old); } catch (e) { /* 旧脚本可能已删除,忽略 */ } panel = document.createElement('div'); panel.id = 'cmip-qq-panel'; panel.style.cssText = 'position:fixed;top:10px;right:10px;z-index:2147483647;background:#fff;border-radius:6px;box-shadow:0 3px 14px rgba(0,0,0,.28);min-width:300px;max-width:90vw;font-family:Microsoft YaHei,sans-serif;font-size:11.5px;overflow:hidden;'; } var bg = errMsg ? '#f56c6c' : (data ? '#67c23a' : '#e6a23c'); var html = '
'; html += '🔍 CMIP:' + code + ''; html += '×'; html += '
'; if (errMsg) { html += '
' + errMsg + '
'; } else if (data) { html += '
'; for (var i = 0; i < FIELD_MAP.length; i++) { var label = FIELD_MAP[i][0]; var val = data[label]; var valRaw = (val !== null && val !== undefined && val !== '' ? String(val) : '-'); // 备注字段保留换行符;其他字段替换 < 防注入 var valStr = (label === '备注') ? (valRaw !== '-' ? valRaw.replace(/').replace(/\r/g, '') : '-') : (valRaw !== '-' ? valRaw.replace(/'; html += '' + label + ':'; if (label === '销售状态') { var statusNorm = String(val || '').trim(); var isNormal = (statusNorm.indexOf('正常') >= 0) || (statusNorm.indexOf('可用') >= 0); var color = isNormal ? '#67c23a' : '#f56c6c'; html += '' + valStr + ''; } else if (isPrice) { html += '' + valStr + ''; // ★ 库存标记(最右侧显示)—— 强制三态:true有 / false无 / 其他 待确认(接口失败/格式未识别 绝不可冒充"无"!) if (stockData) { var sHas = stockData.hasStock, sErr = stockData.error || ''; var sText = '', sColor = '', sWarn = ''; if (sHas === true) { sText = '厂家库存:有'; sColor = '#2ea950'; } else if (sHas === false) { sText = '厂家库存:无'; sColor = '#f56c6c'; } else { // 接口查询失败 / 格式未识别:用橙色"待确认",并且加醒目小字,用户一眼能看出"不要信这个结果" sText = '厂家库存:待确认'; sColor = '#e6a23c'; sWarn = (sErr ? ('(' + String(sErr).substring(0, 18) + ')') : '(接口未返回库存)'); } html += ''; html += '' + sText + ''; // 来源小字(来自哪一轮 fallback / 什么标签) var sTag = ''; if (sHas === true || sHas === false) { if (stockData.round !== undefined && stockData.total) sTag += '查(F' + (stockData.round + 1) + '/' + stockData.total + ')'; if (stockData.tag) sTag += (sTag ? '·' : '') + stockData.tag; } if (sWarn || sTag) { html += ''; if (sWarn) html += '⚠ ' + sWarn; else if (sTag) html += sTag; html += ''; } html += ''; } } else if (isRemark) { // 备注:允许换行、淡黄底色突出显示 html += '' + valStr + ''; } else { html += '' + valStr + ''; } html += '
'; } html += ''; // 替换明细表格(小字体,紧凑,价格列绿色加粗,含查询按钮) if (replacements && replacements.cols && replacements.rows) { var rc = replacements.cols.length + 1; // +1 为查询按钮列 var rr = replacements.rows.length; html += '
'; html += '
📋 配件替换关系(' + rr + '行×' + rc + '列)
'; html += '
'; html += ''; // 表头 + 查询按钮列 html += ''; for (var ci = 0; ci < replacements.cols.length; ci++) { var colName = String(replacements.cols[ci]); var thStyle = 'padding:3px 5px;border:1px solid #eee;color:#606266;font-weight:600;white-space:nowrap;text-align:left;'; if (colName === '零售价' || colName === '基本订货价') thStyle = 'padding:3px 5px;border:1px solid #b3e0b3;color:#2ea950;font-weight:700;white-space:nowrap;text-align:left;background:#f0f9f0;'; else if (colName === '销售状态') thStyle = 'padding:3px 5px;border:1px solid #b3e0b3;color:#2ea950;font-weight:600;white-space:nowrap;text-align:left;background:#f0f9f0;'; html += ''; html += ''; // 行 for (var ri = 0; ri < replacements.rows.length; ri++) { html += ''; var partNo = ''; for (var ci = 0; ci < replacements.rows[ri].length; ci++) { var cellVal = String(replacements.rows[ri][ci]).replace(/= 0) || (statusNorm.indexOf('可用') >= 0); var sColor = isNormal ? '#2ea950' : '#f56c6c'; tdStyle = 'padding:2px 5px;border:1px solid #eee;color:' + sColor + ';font-weight:600;white-space:nowrap;'; } html += ''; } // 查询按钮列 html += ''; html += ''; } html += '
' + colName.replace(/'; } html += '查询
' + cellVal + ''; html += ''; html += '
'; } } else { html += '
未找到备件数据
'; } if (showOpenBtn) { html += '
'; } if (isSubPanel) { // 子弹窗:只更新 body 内容(保留 header 和 close 按钮) var subBody = panel.querySelector('#cmip-repl-sub-body'); if (subBody) { // 提取 body 内容(去掉 header 部分的拖拽条) var bodyHtml = html; if (bodyHtml.indexOf('cmip-qq-dragbar') >= 0) { var dragEnd = bodyHtml.indexOf('', bodyHtml.indexOf('cmip-qq-dragbar')) + 6; bodyHtml = bodyHtml.substring(dragEnd); } subBody.innerHTML = bodyHtml; } } else { panel.innerHTML = html; document.body.appendChild(panel); } // 主弹窗宽度自适应:根据替换表内容宽度自动调整 if (!isSubPanel) { setTimeout(function () { try { var tbl = document.getElementById('cmip-repl-table'); var wrap = document.getElementById('cmip-repl-table-wrap'); var mainPanel = document.getElementById('cmip-qq-panel'); if (!tbl || !mainPanel) return; // 测量表格实际需要的宽度 var tblWidth = tbl.scrollWidth || tbl.offsetWidth || 0; // 面板内边距和滚动条预留 var neededWidth = tblWidth + 30; // 当前面板宽度 var curWidth = mainPanel.offsetWidth || 300; // 屏幕最大宽度限制(留 40px 边距) var maxAllowed = window.innerWidth - 40; if (neededWidth > maxAllowed) neededWidth = maxAllowed; // 只在需要更宽时才扩展(不缩小) if (neededWidth > curWidth && neededWidth >= 300) { mainPanel.style.width = neededWidth + 'px'; mainPanel.style.maxWidth = neededWidth + 'px'; console.log('[CMIP快查] 📐 主弹窗宽度自适应: ' + curWidth + ' → ' + neededWidth + 'px (表格宽度=' + tblWidth + ')'); } } catch (e) { } }, 50); } // 全局查询按钮点击 → 弹出/更新子弹窗显示该编号详情(只注册一次) if (!window.__cmip_qq_qry_bound) { document.addEventListener('click', function (e) { var btn = e.target.closest('.cmip-repl-qry'); if (!btn) return; e.preventDefault(); e.stopPropagation(); var partNo = btn.getAttribute('data-partno') || ''; if (!partNo) return; // 判断点击是否在子弹窗内 var inSubPanel = e.target.closest('#cmip-repl-sub-panel'); var subPanel = document.getElementById('cmip-repl-sub-panel'); if (inSubPanel && subPanel) { // 子弹窗内的查询按钮:直接更新子弹窗内容 // 重置为"查询中..." var subBody = subPanel.querySelector('#cmip-repl-sub-body'); var subTitle = subPanel.querySelector('#cmip-repl-sub-title'); if (subTitle) subTitle.textContent = '🔍 子查询:' + partNo; if (subBody) { subBody.innerHTML = '
查询中...
'; } // 重新发起查询,结果更新到子弹窗 queryCode(partNo, false, 'cmip-repl-sub-panel'); } else { // 主弹窗内的查询按钮:创建新子弹窗 try { var oldSub = document.getElementById('cmip-repl-sub-panel'); if (oldSub && oldSub.parentNode) oldSub.parentNode.removeChild(oldSub); } catch (e) { } // 获取主弹窗位置,子弹窗默认在主弹窗正下方 var mainPanel = document.getElementById('cmip-qq-panel'); var mainRect = mainPanel ? mainPanel.getBoundingClientRect() : null; var subTop = mainRect ? (mainRect.bottom + 6) : 40; // 主弹窗正下方 var subLeft = mainRect ? mainRect.left : 10; // 与主弹窗左侧对齐 // 防止超出右侧屏幕 if (subLeft + 420 > window.innerWidth) subLeft = Math.max(10, window.innerWidth - 430); subPanel = document.createElement('div'); subPanel.id = 'cmip-repl-sub-panel'; subPanel.style.cssText = 'position:fixed;z-index:2147483649;background:#fff;border-radius:6px;box-shadow:0 3px 14px rgba(0,0,0,.28);min-width:300px;max-width:420px;font-family:Microsoft YaHei,sans-serif;font-size:11.5px;overflow:hidden;top:' + subTop + 'px;left:' + subLeft + 'px;'; subPanel.innerHTML = '
' + '🔍 子查询:' + partNo + '' + '×' + '
' + '
查询中...
'; document.body.appendChild(subPanel); // 关闭按钮 subPanel.querySelector('#cmip-repl-sub-close').addEventListener('click', function (ev) { ev.stopPropagation(); try { subPanel.parentNode.removeChild(subPanel); } catch (e) { } }); // 拖动功能 (function makeSubDraggable() { var bar = subPanel.querySelector('#cmip-repl-sub-dragbar'); if (!bar) return; var dragging = false, sx = 0, sy = 0, ox = 0, oy = 0; bar.addEventListener('mousedown', function (e) { if (e.target && e.target.id === 'cmip-repl-sub-close') return; dragging = true; sx = e.clientX; sy = e.clientY; var rect = subPanel.getBoundingClientRect(); ox = rect.left; oy = rect.top; e.preventDefault(); e.stopPropagation(); }, true); function onMove(e) { if (!dragging) return; if (!subPanel.parentNode) { dragging = false; return; } var nx = Math.max(0, Math.min(window.innerWidth - 100, ox + (e.clientX - sx))); var ny = Math.max(0, Math.min(window.innerHeight - 60, oy + (e.clientY - sy))); subPanel.style.left = nx + 'px'; subPanel.style.top = ny + 'px'; } function onUp() { dragging = false; } document.addEventListener('mousemove', onMove, true); document.addEventListener('mouseup', onUp, true); // 记录到 subPanel 上,关闭时可清理(这里简化,依赖面板被 remove 时 GC) subPanel.__sub_drag_move = onMove; subPanel.__sub_drag_up = onUp; })(); // 点击外部关闭(排除子弹窗自身和查询按钮) setTimeout(function () { var outsideClick = function (ev2) { if (!subPanel.parentNode) { document.removeEventListener('click', outsideClick, true); return; } // 如果点击的是查询按钮,让上面的逻辑处理 if (ev2.target.closest('.cmip-repl-qry')) return; if (!subPanel.contains(ev2.target)) { try { subPanel.parentNode.removeChild(subPanel); } catch (e) { } document.removeEventListener('click', outsideClick, true); } }; document.addEventListener('click', outsideClick, true); }, 50); // 发起子查询 queryCode(partNo, false, 'cmip-repl-sub-panel'); } }, true); window.__cmip_qq_qry_bound = true; } // 全局关闭:不管 × 上面被绑了什么,capture 阶段先把 DOM 干掉(永远生效) // 只注册一次 if (!window.__cmip_qq_close_bound) { document.addEventListener('click', function (e) { var target = e.target; if (target && target.id === 'cmip-qq-close') { e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); try { var p = document.getElementById('cmip-qq-panel'); if (p && p.parentNode) p.parentNode.removeChild(p); } catch (err) { /* 可能被旧脚本先删了,忽略 */ } } }, true); window.__cmip_qq_close_bound = true; } // 打开按钮 var ob = panel.querySelector('#cmip-qq-open'); if (ob) ob.addEventListener('click', function (e) { e.stopPropagation(); window.open('https://cmip.dpca.com.cn/', '_blank'); }); // 拖动:mousemove/mouseup 也用 capture,并且注册后记录引用到 panel.dataset,确保下次 showResult 重删前清理 (function makeDraggable() { var bar = panel.querySelector('#cmip-qq-dragbar'); if (!bar) return; // 先清理旧的拖动监听器(来自上次 panel 的残留) try { if (window.__cmip_qq_drag_move) window.removeEventListener('mousemove', window.__cmip_qq_drag_move, true); if (window.__cmip_qq_drag_up) window.removeEventListener('mouseup', window.__cmip_qq_drag_up, true); } catch (e) { } var dragging = false, sx = 0, sy = 0, ox = 0, oy = 0; bar.addEventListener('mousedown', function (e) { if (e.target && e.target.id === 'cmip-qq-close') return; dragging = true; sx = e.clientX; sy = e.clientY; var rect = panel.getBoundingClientRect(); ox = rect.left; oy = rect.top; e.preventDefault(); e.stopPropagation(); }, true); function onMove(e) { if (!dragging) return; if (!panel.parentNode) { dragging = false; return; } var nx = Math.max(0, Math.min(window.innerWidth - 100, ox + (e.clientX - sx))); var ny = Math.max(0, Math.min(window.innerHeight - 60, oy + (e.clientY - sy))); panel.style.left = nx + 'px'; panel.style.top = ny + 'px'; panel.style.right = 'auto'; } function onUp() { dragging = false; } window.addEventListener('mousemove', onMove, true); window.addEventListener('mouseup', onUp, true); window.__cmip_qq_drag_move = onMove; window.__cmip_qq_drag_up = onUp; // 记录到 panel.dataset,被 remove 时可清理 panel.dataset.dragBound = '1'; })(); // 60 秒自动关闭(用原生 DOM API 强制删除) setTimeout(function () { try { var p = document.getElementById('cmip-qq-panel'); if (p && p.parentNode && p.dataset === panel.dataset) p.parentNode.removeChild(p); } catch (e) { } }, 60000); } function buildHeaders(authVal) { var saved = getHeaders() || {}; var base = { 'Accept': 'application/json, text/plain, */*', 'Content-Type': 'application/json;charset=utf-8', 'X-Requested-With': 'XMLHttpRequest' }; var out = {}; for (var k in base) out[k] = base[k]; for (var s in saved) out[s] = saved[s]; if (authVal) out['Authorization'] = authVal; return out; } // ASTP/ServiceBox 端:查询基础信息拿配件名称(放大镜弹窗用,修复卡死) function fetchBaseInfo(code, authVal, callback) { var hdrs = buildHeaders(authVal); var body = JSON.stringify({ partNo: code, pageSize: -1, pageIndex: 1 }); console.log('[CMIP快查] 🔍[ASTP端] 查询配件基础信息 code=' + code); GM_xmlhttpRequest({ method: 'POST', url: CMIP_API_URL, headers: hdrs, data: body, timeout: 10000, withCredentials: true, anonymous: false, onload: function (det) { try { var txt = det.responseText || ''; if (!txt) { callback({ fields: {} }); return; } var hs = det.status || 0; if (hs === 401 || hs === 403 || hs === 302 || hs >= 500) { callback({ fields: {}, error: 'HTTP ' + hs }); return; } var j = JSON.parse(txt), out = {}, row = null; try { var rs = j.data.getBasePartInfoList.rows; if (rs && rs.length) { var up = String(code).toUpperCase(); for (var i = 0; i < rs.length; i++) if (rs[i] && rs[i].partNo && String(rs[i].partNo).toUpperCase() === up) { row = rs[i]; break; } if (!row && rs[0]) row = rs[0]; } } catch (e) { } if (!row) try { var d = j.data; if (d && typeof d === 'object') { for (var k in d) { var b = d[k]; if (b && Array.isArray(b.rows) && b.rows.length && b.rows[0].partNo) { row = b.rows[0]; break; } } } } catch (e) { } if (row) { var NKEYS = ['partName', 'partNameCn', 'sparePartName', 'name', 'materialName']; for (var ni = 0; ni < NKEYS.length; ni++) if (row[NKEYS[ni]] !== undefined && row[NKEYS[ni]] !== null && row[NKEYS[ni]] !== '') { out['备件名称'] = row[NKEYS[ni]]; break; } var CKEYS = ['partNo', 'partCode', 'sparePartCode', 'code', 'materialCode']; for (var ci = 0; ci < CKEYS.length; ci++) if (row[CKEYS[ci]] !== undefined && row[CKEYS[ci]] !== null && row[CKEYS[ci]] !== '') { out['备件编码'] = row[CKEYS[ci]]; break; } } callback({ fields: out }); } catch (e) { callback({ fields: {}, error: e.message }); } }, onerror: function () { callback({ fields: {}, error: '网络错误' }); }, ontimeout: function () { callback({ fields: {}, error: '请求超时' }); } }); } // 尝试已知的替换关系接口候选 URL(首次查询时自动调用) function tryReplCandidates(code, authVal, idx, callback) { if (idx >= CMIP_REPL_API_CANDIDATES.length) { console.log('[CMIP快查] 替换关系:所有候选URL都未返回有效数据'); callback(null); return; } var url = CMIP_REPL_API_CANDIDATES[idx]; var hdrs = buildHeaders(authVal); var body = JSON.stringify({ partNo: code }); console.log('[CMIP快查] 🔍 尝试替换候选[' + (idx + 1) + '/' + CMIP_REPL_API_CANDIDATES.length + '] ' + url.substring(0, 70)); GM_xmlhttpRequest({ method: 'POST', url: url, headers: hdrs, data: body, timeout: 10000, withCredentials: true, anonymous: false, onload: function (det) { try { var txt = det.responseText || ''; if (!txt) { tryReplCandidates(code, authVal, idx + 1, callback); return; } var json = JSON.parse(txt); if (json.result === '0' || json.result === 0) { console.log('[CMIP快查] 候选[' + (idx + 1) + '] result=0: ' + (json.msg || '')); tryReplCandidates(code, authVal, idx + 1, callback); return; } // 检查是否有 rows 数据 var d = json.data || {}; var hasRows = false; for (var k in d) { if (d[k] && Array.isArray(d[k].rows) && d[k].rows.length > 0) { hasRows = true; break; } } if (!hasRows) { console.log('[CMIP快查] 候选[' + (idx + 1) + '] 无rows数据'); tryReplCandidates(code, authVal, idx + 1, callback); return; } // 成功!保存这个模板 saveReplApi(url, 'POST', body, hdrs); console.log('[CMIP快查] ✅ 候选[' + (idx + 1) + '] 命中!已保存替换接口模板'); var repData = parseReplacementFull({ data: json }); if (repData) saveReplacement(code, json); callback(repData); } catch (e) { tryReplCandidates(code, authVal, idx + 1, callback); } }, onerror: function () { tryReplCandidates(code, authVal, idx + 1, callback); }, ontimeout: function () { tryReplCandidates(code, authVal, idx + 1, callback); } }); } // 查询厂家库存(qryMengeByMatnr)——★ 品牌不写死 + rows必须精确匹配partNo function fetchStock(code, authVal, callback) { var hdrs = buildHeaders(authVal); var upCode = String(code).toUpperCase(); var stockApi = CMIP_STOCK_API_URL; // ★ 修复:不再写死 orgBrand='P'(截图是雪铁龙,写死P必然查错) var stockBodyTpl = JSON.stringify({ partNo: code, iMatnr: code }); var hasSavedApi = false; try { var saved = JSON.parse(GM_getValue('cmip_stock_api_v2', '{}') || '{}'); if (saved && saved.url) { stockApi = saved.url; hasSavedApi = true; if (saved.bodyTemplate) { var sBody = JSON.parse(saved.bodyTemplate); // ★ 只替换备件编码字段,其他字段(orgBrand/dlrCode/brandCode/dealerCode等)原样保留 var PART_CODE_FIELDS = ['iMatnr', 'partNo', 'matnr', 'sparePartCode', 'partCode', 'code']; for (var fi = 0; fi < PART_CODE_FIELDS.length; fi++) if (sBody[PART_CODE_FIELDS[fi]] !== undefined) sBody[PART_CODE_FIELDS[fi]] = code; stockBodyTpl = JSON.stringify(sBody); } } } catch (e) { } console.log('[CMIP快查] 📦 查询库存 code=' + code + ' savedApi=' + hasSavedApi + ' body=' + stockBodyTpl); function parseBoolVal(bv) { if (bv === true || bv === 'true' || bv === 1 || bv === '1') return { matched: true, has: true }; if (bv === false || bv === 'false' || bv === 0 || bv === '0') return { matched: true, has: false }; if (typeof bv === 'string') { var t = bv.trim(); if (/^有$|^是$|^正常$|^可用$|^存在$|^yes$|^Y$/i.test(t)) return { matched: true, has: true }; if (/^无$|^否$|^没有$|^不可用$|^不存在$|^no$|^N$/i.test(t)) return { matched: true, has: false }; if (t.indexOf('有') >= 0 && t.indexOf('无') < 0) return { matched: true, has: true }; if (t.indexOf('无') >= 0 && t.indexOf('有') < 0) return { matched: true, has: false }; var n = Number(t); if (!isNaN(n)) return { matched: true, has: (n > 0) }; } if (typeof bv === 'number' && !isNaN(bv)) return { matched: true, has: (bv > 0) }; return { matched: false }; } // ★ fallback顺序:纯编号优先,保存模板次之,P/D/纯partNo 兜底 — 每轮内部再切换 3 种传输格式(JSON/FORM/GET) // buildBody 返回**对象**(不再是JSON字符串),由下面 tryStockApi 内部选择序列化方式 function toObj(bodyLike) { if (bodyLike && typeof bodyLike === 'object') return bodyLike; if (typeof bodyLike === 'string') { try { return JSON.parse(bodyLike); } catch (e) { // 如果是 form-urlencoded 字符串,解析成对象 var obj = {}; var parts = bodyLike.split('&'); for (var i = 0; i < parts.length; i++) { var kv = parts[i].split('='); if (kv.length >= 2) obj[decodeURIComponent(kv[0])] = decodeURIComponent(kv.slice(1).join('=')); } return obj; } } return {}; } function objToForm(obj) { var parts = []; Object.keys(obj || {}).forEach(function (k) { var v = (obj[k] === undefined || obj[k] === null) ? '' : String(obj[k]); parts.push(encodeURIComponent(k) + '=' + encodeURIComponent(v)); }); return parts.join('&'); } var STOCK_FALLBACK = [ { url: 'https://cmip.dpca.com.cn/pac/PacPaBuTb2a0/qryMengeByMatnr.do', buildBody: function (c) { return toObj({ partNo: c, iMatnr: c }); }, tag: '纯编号' }, { url: stockApi, buildBody: function () { return toObj(stockBodyTpl); }, tag: '保存模板' }, { url: 'https://cmip.dpca.com.cn/pac/PacPaBuTb2a0/qryMengeByMatnr.do', buildBody: function (c) { return toObj({ orgBrand: 'P', dlrCode: '', iMatnr: c }); }, tag: '品牌P' }, { url: 'https://cmip.dpca.com.cn/pac/PacPaBuTb2a0/qryMengeByMatnr.do', buildBody: function (c) { return toObj({ orgBrand: 'D', dlrCode: '', iMatnr: c }); }, tag: '品牌D' }, { url: 'https://cmip.dpca.com.cn/pac/PacPaBuTb2a0/qryMengeByMatnr.do', buildBody: function (c) { return toObj({ partNo: c }); }, tag: '仅partNo' } ]; var FORMAT_NAMES = ['JSON','FORM','GET']; var stockIdx = 0; function tryNextFallback(lastErr) { stockIdx++; if (stockIdx < STOCK_FALLBACK.length) { var f = STOCK_FALLBACK[stockIdx]; tryStockApi(f.url, f.buildBody(code), f.tag, 0); } else { callback({ error: lastErr || '接口返回格式未识别,无法确定库存(建议去CMIP库存页直接查询或同步最新Token)', httpStatus: 0 }); } } function tryStockApi(apiUrl, bodyObj, tag, fmt) { fmt = fmt || 0; // ★ 每轮内部 3 种传输格式:0=POST JSON → 1=POST FORM → 2=GET query // 兼容 CMIP qryMengeByMatnr.do 后端可能要求 form-urlencoded 或 GET 的老架构 var fullTag = tag + '(' + FORMAT_NAMES[fmt] + ')'; var hclone = {}; for (var hk in hdrs) hclone[hk] = hdrs[hk]; var method, bodyData, finalUrl = apiUrl; if (fmt === 0) { method = 'POST'; hclone['Content-Type'] = 'application/json;charset=UTF-8'; bodyData = JSON.stringify(bodyObj || {}); } else if (fmt === 1) { method = 'POST'; hclone['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8'; bodyData = objToForm(bodyObj); } else { method = 'GET'; var qs = objToForm(bodyObj); if (qs) finalUrl = apiUrl + (apiUrl.indexOf('?') >= 0 ? '&' : '?') + qs; bodyData = undefined; if (hclone['Content-Type']) delete hclone['Content-Type']; } function retryOrNext(lastErr) { if (fmt < 2) { console.log('[CMIP快查] 🔄[' + fullTag + '] 失败: ' + lastErr + ' → 同轮切换为 ' + FORMAT_NAMES[fmt + 1]); tryStockApi(apiUrl, bodyObj, tag, fmt + 1); } else { console.log('[CMIP快查] ⏭[' + fullTag + '] 3种格式全失败,进入下一轮fallback'); tryNextFallback(lastErr); } } var req = { method: method, url: finalUrl, headers: hclone, timeout: 12000, withCredentials: true, anonymous: false }; if (bodyData !== undefined) req.data = bodyData; GM_xmlhttpRequest(Object.assign({}, req, { onload: function (det) { try { var txt = det.responseText || ''; var httpStatus = det.status || 0; console.log('[CMIP快查] 📦[' + fullTag + '] 响应 HTTP=' + httpStatus + ' len=' + txt.length + ' url=' + finalUrl.substring(Math.max(0, finalUrl.length - 50))); if (httpStatus === 401 || httpStatus === 403) { var te = 'HTTP ' + httpStatus + ' ' + (httpStatus === 401 ? '未授权(Token可能过期)' : '被拦截(需要重新登录CMIP)'); callback({ error: te, httpStatus: httpStatus, tokenInvalid: true }); return; } if (httpStatus === 302 || httpStatus >= 500) { var se = 'HTTP ' + httpStatus + ' ' + (httpStatus >= 500 ? '服务器异常' : '重定向'); retryOrNext(se); return; } if (!txt) { retryOrNext('响应体为空'); return; } var json; try { json = JSON.parse(txt); } catch (ex) { console.log('[CMIP快查] 📦[' + fullTag + '] 非JSON响应前缀: ' + txt.substring(0, 200)); retryOrNext('响应非JSON格式'); return; } // === Token 过期/未同步 诊断:result 错误码 === if (json.result !== undefined && String(json.result) !== '1' && String(json.result) !== '0' && String(json.result) !== '200') { var resultCode = String(json.result); var authErrCodes = ['-1', '-100', 'l9900000001', 'l9900000002', 'l9900000003', '9999']; if (authErrCodes.indexOf(resultCode) >= 0 || /login|auth|token|未登录|认证|权限|l990/i.test(String(json.msg || ''))) { var authMsg = 'Token无效或过期(result=' + resultCode + ' msg=' + (json.msg || '') + '),请在CMIP页点右上角绿点重新同步'; console.log('[CMIP快查] 🚫 ' + authMsg); callback({ error: authMsg, httpStatus: 401, tokenInvalid: true }); return; } } try { var topKeys = Object.keys(json); console.log('[CMIP快查] 📦 库存响应[' + stockIdx + ']' + fullTag + ' 顶层keys=' + topKeys.join(',') + ' result=' + json.result + ' msg=' + (json.msg || '')); } catch (e) { } var BOOL_FIELD_NAMES = ['hasStock','hasStk','isStock','stockFlag','stockStatus','hasSpare','hasInventory','stockExist','hasStockQty','stock','isHasStock','IS_MENGE','isMenge','menge','是否有库存','是否有','库存状态','状态','是否库存','有库存','库存']; var QTY_FIELD_NAMES = ['stockQty','quantity','stock','qty','availableQty','stockNum','totalStock','stockAmount','canUseQty','usableQty','availQty','onHandQty','stockCount','stockTotal','realStock','可用库存','实际库存','库存数量','厂家数量','数量','库存量']; // 1) 顶层 / data.hasStock 布尔字段 for (var tsi = 0; tsi < BOOL_FIELD_NAMES.length; tsi++) { var tv = json[BOOL_FIELD_NAMES[tsi]]; if (tv !== undefined && tv !== null) { var p = parseBoolVal(tv); if (p.matched) { console.log('[CMIP快查] 📦 顶层' + BOOL_FIELD_NAMES[tsi] + '=' + tv + ' → ' + (p.has?'有':'无')); callback({ hasStock: p.has, qty: null, round: stockIdx, total: STOCK_FALLBACK.length, tag: fullTag }); return; } } } var d = json.data || {}; if (d && typeof d === 'object' && d.hasStock !== undefined && d.hasStock !== null) { var p2 = parseBoolVal(d.hasStock); if (p2.matched) { console.log('[CMIP快查] 📦 data.hasStock=' + d.hasStock + ' → ' + (p2.has?'有':'无')); callback({ hasStock: p2.has, qty: null, round: stockIdx, total: STOCK_FALLBACK.length, tag: fullTag }); return; } } // 2) ★ 核心:rows 必须先按 partNo 精确匹配我们查询的编号,只在匹配行读库存(否则会读到其他编号) var matchedRow = null, matchedKey = null; if (d && typeof d === 'object') { for (var k in d) { var blk = d[k]; if (blk && Array.isArray(blk.rows) && blk.rows.length > 0) { for (var ri2 = 0; ri2 < blk.rows.length; ri2++) { var rr = blk.rows[ri2]; var rPartVal = rr.partNo || rr.partCode || rr.sparePartCode || rr.code || rr.materialCode || rr.iMatnr || rr.matnr; if (rPartVal !== undefined && rPartVal !== null && String(rPartVal).toUpperCase() === upCode) { matchedRow = rr; matchedKey = k; break; } } if (matchedRow) break; } } // 没精确匹配时:只有 rows=1 行才假设匹配;多行直接跳过,避免错行 if (!matchedRow) { for (var k3 in d) { var blk3 = d[k3]; if (blk3 && Array.isArray(blk3.rows)) { if (blk3.rows.length === 1) { matchedRow = blk3.rows[0]; matchedKey = k3; console.log('[CMIP快查] ⚠ 无精确partNo匹配,但rows只有1行,假设匹配'); break; } if (blk3.rows.length > 1) console.log('[CMIP快查] ⚠ rows=' + blk3.rows.length + ' 多行,未精确匹配到partNo=' + upCode + ',跳过blk=' + k3); } } } } if (matchedRow) { var rks2 = Object.keys(matchedRow); var diag2 = []; for (var di2 = 0; di2 < rks2.length; di2++) diag2.push(rks2[di2] + '=' + matchedRow[rks2[di2]]); console.log('[CMIP快查] 📦 匹配行(' + matchedKey + ')字段: ' + diag2.join(' | ')); // 布尔字段 for (var bi2 = 0; bi2 < rks2.length; bi2++) { var bkn2 = rks2[bi2], bknL2 = bkn2.toLowerCase(); var isBF2 = false; for (var bfi2 = 0; bfi2 < BOOL_FIELD_NAMES.length; bfi2++) if (bknL2 === String(BOOL_FIELD_NAMES[bfi2]).toLowerCase() || bknL2.indexOf(String(BOOL_FIELD_NAMES[bfi2]).toLowerCase()) >= 0) { isBF2 = true; break; } if (isBF2 || /stock|menge|是否有|库存/i.test(bknL2)) { var pr2 = parseBoolVal(matchedRow[bkn2]); if (pr2.matched) { console.log('[CMIP快查] 📦 布尔字段' + bkn2 + '=' + matchedRow[bkn2] + ' → ' + (pr2.has?'有':'无')); callback({ hasStock: pr2.has, qty: null, round: stockIdx, total: STOCK_FALLBACK.length, tag: fullTag }); return; } } } // 数量字段 for (var qi3 = 0; qi3 < QTY_FIELD_NAMES.length; qi3++) { var qv3 = matchedRow[QTY_FIELD_NAMES[qi3]]; if (qv3 === undefined || qv3 === null || qv3 === '') continue; var num3 = Number(qv3); if (!isNaN(num3)) { console.log('[CMIP快查] 📦 数量字段' + QTY_FIELD_NAMES[qi3] + '=' + qv3 + ' → ' + (num3 > 0 ? '有' : '无')); callback({ hasStock: num3 > 0, qty: num3, round: stockIdx, total: STOCK_FALLBACK.length, tag: fullTag }); return; } } for (var qi4 = 0; qi4 < rks2.length; qi4++) { var knl4 = rks2[qi4].toLowerCase(); if (/stock|qty|quantity|avail|num|count|total|amount|realstock|库存|数量/i.test(knl4)) { var qv4 = matchedRow[rks2[qi4]]; if (qv4 !== undefined && qv4 !== null && qv4 !== '') { var num4 = Number(qv4); if (!isNaN(num4)) { console.log('[CMIP快查] 📦 匹配数量字段' + rks2[qi4] + '=' + qv4 + ' → ' + (num4 > 0 ? '有' : '无')); callback({ hasStock: num4 > 0, qty: num4, round: stockIdx, total: STOCK_FALLBACK.length, tag: fullTag }); return; } } } } console.log('[CMIP快查] ⚠[' + fullTag + '] 匹配行中未找到库存字段,尝试同轮其他传输格式'); } retryOrNext('响应中找不到库存字段或 rows 不匹配'); } catch (e) { console.log('[CMIP快查] 📦[' + fullTag + '] 库存查询异常: ' + e.message); retryOrNext('解析异常:' + e.message); } }, onerror: function (err) { console.log('[CMIP快查] 📦[' + fullTag + '] 网络级失败(详细): ' + (err ? JSON.stringify(err).substring(0, 200) : '未知错误')); retryOrNext('网络请求失败'); }, ontimeout: function () { console.log('[CMIP快查] 📦[' + fullTag + '] 请求超时'); retryOrNext('请求超时(12秒)'); } })); } // 先用保存的或默认的接口(其他 4 个兜底由 tryNextFallback 自动触发,每轮内 3 种传输格式切换由 retryOrNext 触发) tryStockApi(STOCK_FALLBACK[0].url, STOCK_FALLBACK[0].buildBody(code), STOCK_FALLBACK[0].tag, 0); } // 主动查询替换关系:用 CMIP 端保存的替换接口模板,替换编码后发请求 function fetchReplacement(code, authVal, callback) { var replApi = getReplApi(); if (!replApi || !replApi.url) { console.log('[CMIP快查] 替换关系:未保存接口模板,尝试已知候选URL...'); // 没有模板时,自动尝试已知的 CMIP 替换关系接口 URL tryReplCandidates(code, authVal, 0, callback); return; } // 每次调用都检查 URL 是否是出库单等错误接口(防止旧脚本污染) if (/getBuOut|outbound|stockOut|getStockOut|出库|delivery|getDelivery/i.test(replApi.url)) { console.log('[CMIP快查] 🧹 检测到错误的替换接口模板(出库单),已清除: ' + replApi.url.substring(0, 80)); try { GM_setValue(REPL_API_KEY, '{}'); } catch (e) { } // 清除后尝试候选 URL tryReplCandidates(code, authVal, 0, callback); return; } try { // 替换请求体里的编码:把模板里的 partNo/oldPartNo/originPartNo 等替换成当前 code var bodyStr = replApi.bodyTemplate || '{}'; var bodyObj = JSON.parse(bodyStr); var codeFields = ['partNo', 'oldPartNo', 'originPartNo', 'sourcePartNo', 'partCode', 'code', 'origPartNo']; var replaced = false; for (var i = 0; i < codeFields.length; i++) { if (bodyObj[codeFields[i]] !== undefined) { bodyObj[codeFields[i]] = code; replaced = true; } } if (!replaced) { // 模板里没有已知编码字段,尝试直接设 partNo bodyObj.partNo = code; } var finalBody = JSON.stringify(bodyObj); var hdrs = buildHeaders(authVal); console.log('[CMIP快查] 🔍 主动查询替换关系 code=' + code + ' url=' + replApi.url.substring(0, 80)); console.log('[CMIP快查] 请求体: ' + finalBody.substring(0, 200)); GM_xmlhttpRequest({ method: replApi.method || 'POST', url: replApi.url, headers: hdrs, data: finalBody, timeout: 15000, withCredentials: true, anonymous: false, onload: function (det) { try { var txt = det.responseText || ''; console.log('[CMIP快查] 替换接口响应 HTTP=' + (det.status || 0) + ' len=' + txt.length); if (!txt) { console.log('[CMIP快查] 响应体为空'); callback(null); return; } var json; try { json = JSON.parse(txt); } catch (ex) { console.log('[CMIP快查] JSON 解析失败: ' + txt.substring(0, 300)); callback(null); return; } if (json.result === '0' || json.result === 0) { console.log('[CMIP快查] 替换关系接口返回失败 result=0 msg=' + (json.msg || '')); callback(null); return; } // 额外诊断:看 data 下有哪些 key,每个 key.rows 长度 try { var d = json.data || {}; var dk = Object.keys(d); console.log('[CMIP快查] data下所有key: ' + dk.join(', ')); for (var _ki = 0; _ki < dk.length; _ki++) { var _kk = dk[_ki]; var _bb = d[_kk]; if (_bb && typeof _bb === 'object' && !Array.isArray(_bb) && _bb.rows) { console.log('[CMIP快查] data.' + _kk + '.rows=' + _bb.rows.length + ' 首行字段=' + Object.keys(_bb.rows[0] || {}).join(',')); } else if (Array.isArray(_bb)) { console.log('[CMIP快查] data.' + _kk + ' 是数组,长度=' + _bb.length); } } } catch (e) { } // 解析替换数据 var repData = parseReplacementFull({ data: json }); console.log('[CMIP快查] parseReplacementFull 结果=' + (repData ? ('OK ' + repData.rows.length + 'x' + repData.cols.length) : 'null')); // 同时存缓存 if (repData) saveReplacement(code, json); callback(repData); } catch (e) { console.log('[CMIP快查] 替换关系解析失败: ' + e.message); callback(null); } }, onerror: function (err) { console.log('[CMIP快查] ❌ 替换接口请求失败: ' + (err ? JSON.stringify(err).substring(0, 300) : 'unknown error')); console.log('[CMIP快查] 可能原因: Cookie未携带/跨域/URL错误。检查headers: ' + JSON.stringify(hdrs).substring(0, 200)); callback(null); }, ontimeout: function () { console.log('[CMIP快查] ⚠ 替换接口请求超时(15s)。检查URL: ' + replApi.url.substring(0, 100)); callback(null); } }); } catch (e) { console.log('[CMIP快查] fetchReplacement异常: ' + e.message); callback(null); } } function queryCode(code, _isSub, _targetId) { code = String(code).trim().replace(/\s+/g, ''); if (!CODE_RE.test(code)) return; saveLastCode(code); var targetId = _targetId || null; var token = getToken(); if (!token) { showResult(code, null, '❌ 还没同步 Token/请求头。

' + '操作(只做一次):
' + '① 新标签打开 CMIP 并登录
' + '② 点屏幕上的绿色小圆点→同步
' + '③ 在 CMIP 手动查询一次 1109Z2
' + '④ 若要看替换关系,在 CMIP 点「查看明细」一次', true, null, null, targetId); return; } var savedH = getHeaders(); var cleanTok = token.replace(/^Bearer\s*/i, '').trim(); var lastFmt = getAuthFmt(); var cands = []; if (lastFmt) cands.push(lastFmt); cands.push(cleanTok); cands.push('Bearer ' + cleanTok); cands.push(token.trim()); var seen = {}; var authCands = []; for (var i = 0; i < cands.length; i++) { var v = cands[i]; if (v && !seen[v]) { seen[v] = 1; authCands.push(v); } } console.log('[CMIP快查] 查询 ' + code + ' tokenLen=' + token.length + ' authCands=' + authCands.length + ' savedHeaders=' + Object.keys(savedH).length); function sendOne(auth, cb) { var hdrs = buildHeaders(auth); GM_xmlhttpRequest({ method: 'POST', url: CMIP_API_URL, headers: hdrs, data: JSON.stringify({ partNo: code, pageSize: -1, pageIndex: 1 }), timeout: 25000, withCredentials: true, anonymous: false, onload: function (det) { cb(null, det, auth, hdrs); }, onerror: function (e) { cb(e, null, auth, hdrs); }, ontimeout: function () { cb(new Error('TIMEOUT'), null, auth, hdrs); } }); } function anaResp(det) { var txt = (det && det.responseText) || ''; var st = (det && det.status) || 0; if (st === 0 || !txt) return { ok: false, fatal: true, reason: 'NO_RESP', msg: '无响应(可能跨域/CORS)' }; if (st >= 400) return { ok: false, fatal: false, reason: 'HTTP_' + st, msg: 'HTTP ' + st }; if (/^\s*= 0) return { ok: false, fatal: true, reason: 'LOGIN_PAGE', msg: '返回登录页(会话失效)' }; try { var json = JSON.parse(txt); if (json.result === '0' || json.result === 0) { var m = String(json.msg || ''); var numPart = m.replace(/[^\d]/g, ''); if (/^(199|99)\d{7}$/.test(numPart.substring(0, 10)) || /l?99\d{7}/i.test(m)) { return { ok: false, fatal: false, reason: 'AUTH_FAIL', msg: '认证失败 ' + m, json: json }; } return { ok: false, fatal: true, reason: 'RESULT0', msg: '接口失败:' + m, json: json }; } var row = findPartRow(json, code); if (row) { diagnosePrices(row); return { ok: true, data: extractFields(row, code), row: row, json: json }; } return { ok: true, data: null, empty: true, json: json }; } catch (e) { return { ok: false, fatal: true, reason: 'PARSE', msg: '解析失败 ' + e.message, raw: txt.substring(0, 500) }; } } var idx = 0, lastAna = null; function loop() { if (idx >= authCands.length) { var r = lastAna || { msg: '全部失败' }; if (r.reason === 'AUTH_FAIL') { showResult(code, null, '❌ 认证失败。
① 重新登录CMIP点小圆点
② 在CMIP手动查一次编码
③ 把F12 [CMIP快查]日志发我', true, null, null, targetId); } else if (r.fatal) { showResult(code, null, '❌ ' + r.msg + (r.raw ? ('
') : ''), true, null, null, targetId); } else { showResult(code, null, '❌ ' + r.msg, true, null, null, targetId); } return; } var auth = authCands[idx++]; sendOne(auth, function (err, det, usedAuth) { if (err) { lastAna = { ok: false, fatal: err.message !== 'TIMEOUT', reason: err.message, msg: err.message }; loop(); return; } var ana = anaResp(det); console.log('[CMIP快查] ← ' + idx + '/'+authCands.length+' ok=' + ana.ok + ' ' + (ana.reason || (ana.data ? 'HIT' : 'EMPTY'))); if (ana.ok) { if (ana.data) { saveAuthFmt(usedAuth); saveHeaders(buildHeaders(usedAuth)); // 先显示基本备件信息(替换关系+库存异步加载,稍后更新) showResult(code, ana.data, null, false, null, null, targetId); // 同时查询替换关系和库存 var repDone = false, stockDone = false; var repData = null, stockData = null; function updatePanel() { if (repDone && stockDone) { if (repData || stockData) { showResult(code, ana.data, null, false, repData, stockData, targetId); } } } // 主动查询替换关系 fetchReplacement(code, usedAuth, function(rd) { repDone = true; repData = rd; if (rd) console.log('[CMIP快查] ✅ 替换关系加载完成 ' + rd.rows.length + '行'); updatePanel(); // 后台补充价格数据(完成后刷新面板) if (rd && rd.rows && rd.rows.length) { enrichReplacementWithBaseInfo(rd, usedAuth, function(enhancedRd) { if (enhancedRd) { repData = enhancedRd; showResult(code, ana.data, null, false, repData, stockData, targetId); console.log('[CMIP快查] ✅ 替换价格补充完成'); } }); } }); // 查询库存 fetchStock(code, usedAuth, function(sd) { stockDone = true; stockData = sd; updatePanel(); }); } else { try { var d = ana.json && ana.json.data || {}; var parts = []; for (var k in d) { var n = (d[k] && d[k].rows && d[k].rows.length) || 0; if (n) parts.push(k + ':' + n); } console.log('[CMIP快查] 诊断data.*.rows -> ' + (parts.join(' | ') || '(空)')); } catch (e) { } // "认证通过,但没找到" 时不显示弹窗,静默返回 console.log('[CMIP快查] 认证通过但未找到编码 ' + code + ',不显示弹窗'); return; } return; } lastAna = ana; if (ana.fatal) { showResult(code, null, '❌ ' + ana.msg + (ana.raw ? ('
') : ''), true, null, null, targetId); return; } loop(); }); } loop(); } // 选中触发:mouseup/touchend 选中文本匹配编码即触发查询(兼容桌面+手机) // ★ 修复10:添加防重复标记,防止脚本多次执行时重复绑定事件 if (!window.__cmip_selection_bound) { window.__cmip_selection_bound = true; var lastQueryTime = 0; var SELECT_DEBOUNCE = 300; function readSelectionAndTrigger(ev, triggerLabel) { var now = Date.now(); if (now - lastQueryTime < SELECT_DEBOUNCE) return; var code = ''; try { var sel = window.getSelection(); if (sel) code = (sel.toString() || '').trim(); } catch (e) { } if (!code) return; var cleaned = code.replace(/[^A-Za-z0-9]/g, ''); if (!cleaned || !CODE_RE.test(cleaned)) return; if (cleaned.length < 4) return; // 跳过面板内的选中(包括主弹窗和库存弹窗) if (ev && ev.target && ev.target.closest) { if (ev.target.closest('#cmip-qq-panel') || ev.target.closest('#cmip-stock-panel')) return; } lastQueryTime = now; queryCode(cleaned); } var selMouseUp = function (ev) { if (ev.target.closest && (ev.target.closest('#cmip-qq-panel') || ev.target.closest('#cmip-stock-panel'))) return; readSelectionAndTrigger(ev, '鼠标选中'); }; var selTouchEnd = function (ev) { if (ev.target.closest && (ev.target.closest('#cmip-qq-panel') || ev.target.closest('#cmip-stock-panel'))) return; setTimeout(function () { readSelectionAndTrigger(ev, '触屏选中'); }, 50); }; var selChange = function () { try { var sel = window.getSelection(); if (!sel || sel.isCollapsed) return; var node = sel.anchorNode; if (node && node.nodeType === 3) node = node.parentNode; if (node && node.closest && (node.closest('#cmip-qq-panel') || node.closest('#cmip-stock-panel'))) return; } catch (e) { } readSelectionAndTrigger(null, '选中变化'); }; var selDblClick = function (ev) { if (ev.target.closest && (ev.target.closest('#cmip-qq-panel') || ev.target.closest('#cmip-stock-panel'))) return; if (Date.now() - lastQueryTime < SELECT_DEBOUNCE) return; readSelectionAndTrigger(ev, '双击'); }; document.addEventListener('mouseup', selMouseUp, true); document.addEventListener('touchend', selTouchEnd, true); document.addEventListener('selectionchange', selChange); document.addEventListener('dblclick', selDblClick, true); // ★ 修复:页面卸载时清理这些监听器 addCleanup(function() { document.removeEventListener('mouseup', selMouseUp, true); document.removeEventListener('touchend', selTouchEnd, true); document.removeEventListener('selectionchange', selChange); document.removeEventListener('dblclick', selDblClick, true); }); } // ============================================================ // 库存快查:放大镜图标 + 输入框 + 只显示有/无 —— 维修工单风格UI // ============================================================ (function injectStockSearch() { var STOCK_ICON_ID = 'cmip-stock-icon'; var STOCK_PANEL_ID = 'cmip-stock-panel'; var STOCK_POS_KEY = 'cmip_stock_icon_pos'; var STOCK_PANEL_POS_KEY = 'cmip_stock_panel_pos'; var panelOpen = false; // 清理所有旧版本的放大镜和面板 try { var oldIcons = document.querySelectorAll('[id^="cmip-stock-icon-"]'); for (var oi = 0; oi < oldIcons.length; oi++) { if (oldIcons[oi].id !== STOCK_ICON_ID && oldIcons[oi].parentNode) oldIcons[oi].parentNode.removeChild(oldIcons[oi]); } var oldPanels = document.querySelectorAll('[id^="cmip-stock-panel-"]'); for (var op = 0; op < oldPanels.length; op++) { if (oldPanels[op].id !== STOCK_PANEL_ID && oldPanels[op].parentNode) oldPanels[op].parentNode.removeChild(oldPanels[op]); } } catch (e) { } if (document.getElementById(STOCK_ICON_ID)) return; // 简洁放大镜图标(透明背景 + 绿色粗描边 + 小尺寸) var icon = document.createElement('div'); icon.id = STOCK_ICON_ID; icon.title = '厂家是否有库存:点击输入备件编号'; var savedPos = null; try { savedPos = JSON.parse(GM_getValue(STOCK_POS_KEY, '') || 'null'); } catch (e) { } if (!savedPos || typeof savedPos.x !== 'number') savedPos = { x: Math.round(window.innerWidth / 2 - 14), y: 60 }; icon.style.cssText = [ 'position:fixed', 'z-index:2147483645', 'width:28px', 'height:28px', 'left:' + savedPos.x + 'px', 'top:' + savedPos.y + 'px', 'background:transparent', 'border-radius:50%', 'cursor:pointer', 'box-shadow:none', 'display:flex', 'align-items:center', 'justify-content:center', 'user-select:none', 'transition:transform .15s' ].join(';') + ';'; // 绿色实心+粗描边SVG放大镜(显眼) icon.innerHTML = '' + '' + ''; icon.addEventListener('mouseenter', function () { icon.style.transform = 'scale(1.15)'; }); icon.addEventListener('mouseleave', function () { icon.style.transform = 'scale(1)'; }); // 拖拽 + 位置记忆 var dragging = false, moved = false, sx = 0, sy = 0, ox = 0, oy = 0; icon.addEventListener('mousedown', function (e) { dragging = true; moved = false; sx = e.clientX; sy = e.clientY; var rect = icon.getBoundingClientRect(); ox = rect.left; oy = rect.top; e.preventDefault(); e.stopPropagation(); }); document.addEventListener('mousemove', function (e) { if (!dragging) return; var dx = e.clientX - sx, dy = e.clientY - sy; if (Math.abs(dx) + Math.abs(dy) > 3) moved = true; var nx = Math.max(0, Math.min(window.innerWidth - 32, ox + dx)); var ny = Math.max(0, Math.min(window.innerHeight - 32, oy + dy)); icon.style.left = nx + 'px'; icon.style.top = ny + 'px'; }); document.addEventListener('mouseup', function () { if (!dragging) return; dragging = false; if (moved) { var rect = icon.getBoundingClientRect(); try { GM_setValue(STOCK_POS_KEY, JSON.stringify({ x: Math.round(rect.left), y: Math.round(rect.top) })); } catch (e) { } } else { if (panelOpen) closeStockPanel(); else openStockPanel(); } }); document.body.appendChild(icon); function closeStockPanel() { try { var pp = document.getElementById(STOCK_PANEL_ID); if (pp && pp.parentNode) pp.parentNode.removeChild(pp); } catch (e) { } panelOpen = false; } function openStockPanel() { closeStockPanel(); var token = getToken(); var panel = document.createElement('div'); panel.id = STOCK_PANEL_ID; var iconRect = icon.getBoundingClientRect(); var panelWidth = 310; // 加宽适应新布局 // 优先用上次保存的位置,没有则显示在图标下方居中 var savedP = null; try { savedP = JSON.parse(GM_getValue(STOCK_PANEL_POS_KEY, '') || 'null'); } catch (e) { } var px, py; if (savedP && typeof savedP.x === 'number') { px = Math.max(10, Math.min(window.innerWidth - panelWidth - 10, savedP.x)); py = Math.max(10, Math.min(window.innerHeight - 270, savedP.y)); } else { px = Math.max(10, Math.min(window.innerWidth - panelWidth - 10, iconRect.left + iconRect.width / 2 - panelWidth / 2)); py = iconRect.bottom + 8; if (py + 270 > window.innerHeight) py = Math.max(10, iconRect.top - 280); } panel.style.cssText = [ 'position:fixed', 'z-index:2147483647', 'left:' + px + 'px', 'top:' + py + 'px', 'width:' + panelWidth + 'px', 'background:#fff', 'border:1.5px solid #2ea950', 'border-radius:8px', 'box-shadow:0 6px 24px rgba(46,169,80,.18),0 2px 8px rgba(0,0,0,.08)', 'font-family:Microsoft YaHei,-apple-system,BlinkMacSystemFont,sans-serif', 'font-size:12px', 'overflow:hidden' ].join(';') + ';'; // 现代化工单风格:白底细标题绿字 + 圆角 + 精致阴影 var html = ''; html += '
'; html += '厂家是否有库存'; html += '×'; html += '
'; html += '
'; // 字段行:查询编号(查询按钮缩小放在编号框后面,同一行) html += '
'; html += '查询编号:'; html += ''; html += ''; html += '
'; // 字段行:配件名称(查询后填充)—— 长文本自动缩字+换行 html += '
'; html += '配件名称:'; html += ''; html += '
'; // 字段行:厂家库存(查询后填充),详情按钮放在这一行的最右侧,库存值下方小字显示来源(让用户能察觉不准) html += '
'; html += '厂家库存:'; html += ''; html += '等待查询'; html += ''; html += ''; html += ''; html += '
'; // Token / 错误状态条 html += '
'; html += '
'; panel.innerHTML = html; document.body.appendChild(panel); panelOpen = true; // 弹窗拖动 + 位置记忆 var pDrag = panel.querySelector('#' + STOCK_PANEL_ID + '_drag'); if (pDrag) { var pDx = 0, pDy = 0, pOx = 0, pOy = 0, pDraging = false; pDrag.addEventListener('mousedown', function (e) { pDraging = true; pDx = e.clientX; pDy = e.clientY; var prect = panel.getBoundingClientRect(); pOx = prect.left; pOy = prect.top; e.preventDefault(); e.stopPropagation(); }); document.addEventListener('mousemove', function (e) { if (!pDraging) return; var pnx = Math.max(10, Math.min(window.innerWidth - panelWidth - 10, pOx + (e.clientX - pDx))); var pny = Math.max(10, Math.min(window.innerHeight - 60, pOy + (e.clientY - pDy))); panel.style.left = pnx + 'px'; panel.style.top = pny + 'px'; }); document.addEventListener('mouseup', function () { if (!pDraging) return; pDraging = false; var prect2 = panel.getBoundingClientRect(); try { GM_setValue(STOCK_PANEL_POS_KEY, JSON.stringify({ x: Math.round(prect2.left), y: Math.round(prect2.top) })); } catch (e) { } }); } var closeBtn = panel.querySelector('#' + STOCK_PANEL_ID + '_close'); if (closeBtn) closeBtn.addEventListener('click', function (e) { e.preventDefault(); e.stopPropagation(); closeStockPanel(); }); setTimeout(function () { var outsideClick = function (ev) { if (!panelOpen) { document.removeEventListener('click', outsideClick, true); return; } var pp = document.getElementById(STOCK_PANEL_ID); if (!pp) { document.removeEventListener('click', outsideClick, true); return; } if (ev.target.id === STOCK_ICON_ID || (ev.target.closest && ev.target.closest('#' + STOCK_ICON_ID))) return; if (!pp.contains(ev.target)) { closeStockPanel(); document.removeEventListener('click', outsideClick, true); } }; document.addEventListener('click', outsideClick, true); }, 100); var input = panel.querySelector('#' + STOCK_PANEL_ID + '_input'); if (input) { setTimeout(function () { input.focus(); input.select(); }, 30); input.addEventListener('keydown', function (e) { if (e.keyCode === 13) { e.preventDefault(); doStockQuery(); } }); } var qBtn = panel.querySelector('#' + STOCK_PANEL_ID + '_btn'); if (qBtn) qBtn.addEventListener('click', function (e) { e.stopPropagation(); doStockQuery(); }); var dBtn = panel.querySelector('#' + STOCK_PANEL_ID + '_detail'); if (dBtn) dBtn.addEventListener('click', function (e) { e.stopPropagation(); var code = getInputCode(); if (!code) return; closeStockPanel(); queryCode(code); }); // 状态辅助函数 function setStatusBar(text, color, bg) { var sb = panel.querySelector('#' + STOCK_PANEL_ID + '_statusBar'); if (!sb) return; if (!text) { sb.innerHTML = ''; return; } sb.innerHTML = '
' + text + '
'; } function setName(name) { var nm = panel.querySelector('#' + STOCK_PANEL_ID + '_partName'); var row = panel.querySelector('#' + STOCK_PANEL_ID + '_nameRow'); if (nm) { var txt = name || '—'; nm.textContent = txt; // 动态缩字:字符数 <=15 用默认12px;>15 每多3个字符减0.5px,最小9.5px var base = 12, min = 9.5, threshold = 15; var len = String(txt).length; var fs = base; if (len > threshold) { var steps = Math.ceil((len - threshold) / 3); fs = Math.max(min, base - steps * 0.5); } nm.style.fontSize = fs.toFixed(1) + 'px'; } if (row) row.style.opacity = name ? '1' : '.5'; } function setStock(text, color, sourceLabel) { var tx = panel.querySelector('#' + STOCK_PANEL_ID + '_stockText'); if (!tx) return; tx.textContent = text; tx.style.color = color || '#4a4a4a'; tx.style.fontWeight = '600'; var src = panel.querySelector('#' + STOCK_PANEL_ID + '_stockSrc'); if (src) src.textContent = sourceLabel ? sourceLabel : ''; } if (!token) setStatusBar('⚠ 还没同步Token → 请打开CMIP页,点右上角绿点同步', '#e6a23c', '#fff8e1'); function getInputCode() { var inp = panel.querySelector('#' + STOCK_PANEL_ID + '_input'); if (!inp) return ''; var v = String(inp.value || '').trim().replace(/[^A-Za-z0-9]/g, ''); if (!v || !CODE_RE.test(v)) { setStatusBar('⚠ 请输入正确的备件编号(4-30位字母数字)', '#e6a23c', '#fff8e1'); return ''; } return v; } function doStockQuery() { var code = getInputCode(); if (!code) return; var tok = getToken(); if (!tok) { setStatusBar('❌ Token未同步!请先登录CMIP页,点右上角绿点手动同步', '#f56c6c', '#fef0f0'); setStock('—', '#c0c4cc', ''); return; } saveLastCode(code); setName('查询中...'); setStock('查询中...', '#909399', ''); setStatusBar('', null, null); // 1. 先查基础信息拿配件名称 fetchBaseInfo(code, tok, function (infoResult) { if (infoResult && infoResult.fields && infoResult.fields['备件名称']) { setName(infoResult.fields['备件名称']); } else { setName('未查到名称'); } // 2. 查库存(多种token格式只在确认为Token错误时才切换,避免把接口错误误判为Token错误) var cleanTok = tok.replace(/^Bearer\s*/i, '').trim(); var lastFmt = getAuthFmt(); var cands = []; if (lastFmt) cands.push(lastFmt); cands.push(cleanTok); cands.push('Bearer ' + cleanTok); cands.push(tok.trim()); var seen = {}; var authCands = []; for (var ci = 0; ci < cands.length; ci++) { var cv = cands[ci]; if (cv && !seen[cv]) { seen[cv] = 1; authCands.push(cv); } } var idx = 0; var lastErrorMsg = ''; var triedMultAuths = false; function isTokenLikeErr(sd, msg) { if (sd && sd.tokenInvalid === true) return true; var m = msg || ''; return /Token|登录|认证|l990|未授权|401|403|401/i.test(m); } function showFinalErr() { var isTokenErr = isTokenLikeErr(null, lastErrorMsg); var msgBox = isTokenErr ? ('❌ Token无效:' + lastErrorMsg + ' → 请去CMIP页点右上角绿点重新同步' + (triedMultAuths ? '(已尝试所有Token格式)' : '')) : '⚠ 查询失败:' + (lastErrorMsg || '网络错误或接口超时'); var warnColor = isTokenErr ? '#f56c6c' : '#e6a23c'; var warnBg = isTokenErr ? '#fef0f0' : '#fff8e1'; setStatusBar(msgBox, warnColor, warnBg); setStock('待确认', '#909399', '接口失败'); } function tryNextAuth() { if (idx >= authCands.length) { showFinalErr(); return; } var auth = authCands[idx++]; triedMultAuths = (idx > 1); fetchStock(code, auth, function (sd) { if (sd && sd.error) { lastErrorMsg = sd.error; // 关键:只有明确的 Token/认证 类错误才换下一种 token;其他(500/网络/格式未识别)直接当最终错误,不要误显示"已尝试所有Token格式" if (isTokenLikeErr(sd, sd.error)) { console.log('[CMIP快查] 📦 Token格式#' + idx + '认证失败:' + lastErrorMsg + ',尝试下一种...'); tryNextAuth(); } else { console.log('[CMIP快查] 📦 库存查询失败(非Token类):' + lastErrorMsg); showFinalErr(); } return; } // 成功返回:保存有效格式 saveAuthFmt(auth); saveHeaders(buildHeaders(auth)); var sTag = ''; if (sd && sd.round !== undefined && sd.total) sTag += '查(F' + (sd.round + 1) + '/' + sd.total + ')'; if (sd && sd.tag) sTag += (sTag ? '·' : '') + sd.tag; if (sd && sd.hasStock === true) { setStock('有', '#2ea950', sTag); setStatusBar('', null, null); console.log('[CMIP快查] 📦 库存快查 ' + code + ' → 有 (' + sTag + ')'); } else if (sd && sd.hasStock === false) { setStock('无', '#f56c6c', sTag); setStatusBar('', null, null); console.log('[CMIP快查] 📦 库存快查 ' + code + ' → 无 (' + sTag + ')'); } else { setStatusBar('⚠ 接口返回空或格式未识别,库存未知', '#e6a23c', '#fff8e1'); setStock('未知', '#909399', '无结果'); } }); } tryNextAuth(); }); } } document.addEventListener('keydown', function (e) { if (e.keyCode === 27 && panelOpen) { closeStockPanel(); } }); console.log('[CMIP快查] 📦 库存快查放大镜已注入(绿色渐变,默认居中,可拖动)'); })(); console.log('[CMIP快查] ' + SITE_NAME + ' v2.9.5 启动。密钥已验证。宽度自适应+库存前缀+列数修复。F12可查完整诊断。'); } })(); /* ===== 更新日志 ===== v2.9.3 (2026-08-18) - ✅ 【替换API修复】候选URL首项改为实测真实URL: /pac/iPaDbPartSale/getPartReplacelList.do(之前候选URL全错,导致首次使用不显示) - ✅ 【备注字段修复】FIELD_MAP备注字段首项改为 remarkb(实测确认,值如"用 1640098880 代替 阀杆密封垫套件") v2.9.0 (2026-08-18) - ✅ 【子弹窗对齐】子弹窗与主弹窗垂直对齐(相同 top 位置),默认显示在主弹窗左侧,左侧空间不足时显示在右侧 - ✅ 【子弹窗可拖动】子弹窗新增拖动条(#cmip-repl-sub-dragbar),可自由拖动,修复右侧截断看不到的问题 - ✅ 【嵌套查询】子弹窗内的查询按钮点击后原地更新子弹窗内容(不再弹出新窗口),支持无限嵌套查询 - ✅ 【点击外部关闭】排除查询按钮的点击,避免误关 v2.8.0 (2026-08-18) - ✅ 【备注修复】extractFields 改用 pickValDeep 递归搜索2层嵌套对象,新增备注字段诊断日志(🔍 备注相关字段) - ✅ 【库存修复】fetchStock 不再因 result=0 立即失败,改为遍历所有可能字段后再兜底 - ✅ 【库存修复】新增5种顶层库存字段检测(hasStock/isStock/stockFlag等),支持布尔/枚举/数量型 - ✅ 【库存修复】新增3个兜底库存API接口自动切换 - ✅ 【代替查询按钮】替换表新增第7列"查询"按钮(绿色"查"字),点击弹出子弹窗显示该编号详情 - ✅ 【子弹窗交互】子弹窗定位在按钮下方,点击外部自动关闭,支持×按钮关闭 - ✅ 【放大镜】ASTP/ServiceBox 页面注入 22×22px 紧凑放大镜图标,可拖动,位置记忆 v2.7.0 (2026-08-18) - ✅ 【弹窗内可复制】selectionchange 事件判断 anchorNode 是否在 panel 内,弹窗内选中文本不触发查询,便于复制代替编号 - ✅ 【备注字段】FIELD_MAP 新增备注字段,映射 remark/notes/comment/memo/bz/beizhu 等 23 种字段名 - ✅ 【备注样式】备注行用淡黄底色 #fffbe6 突出显示,保留换行符 white-space:pre-wrap - ✅ 【包装含量修正】替换表"包装数量"改为"包装含量",新增 packageSpec/packSpec 候选字段 v2.6.0 (2026-08-18) - ✅ 【选中触发】新增 mouseup/touchend 选中文本即触发查询,桌面鼠标选中+手机长按选中均可,替代原双击/三击方案 - ✅ 【库存修复】fetchStock 增加 hasStock 布尔字段优先检测(顶层+行级+data嵌套),遍历所有 rows 行逐个检查 - ✅ 【库存修复】扩展库存字段映射(canUseQty/usableQty/availQty/onHandQty/stockCount/stockTotal 等) - ✅ 【替换表优化】6列改为:备件编号、备件名称、零售价、包装含量、基本订货价、销售状态 - ✅ 【替换表优化】所有字段映射扩展为更全面的候选列表,提高命中率 - ✅ 【库存日志】逐行打印所有库存行的字段名和值,便于诊断 v2.5.0 (2026-08-17) - ✅ 【出库单污染修复】收窄 isReplacement 正则,删除通用的 detail|明细 匹配,只保留 replace|substitute|getReplace|partReplace|替代|替换 等明确关键词 - ✅ 【出库单污染修复】增加响应结构校验:保存替换关系前检查 rows[0] 是否包含替换相关字段(newPartNo/replacePartNo/项目类别/替换/替代 等),不匹配则丢弃不保存 - ✅ 【ServiceBox弹窗卡死修复】showResult 中改用 old.parentNode.removeChild(old) 替代 old.remove(),彻底解决弹窗无法被新数据替换的问题 - ✅ 【ServiceBox三击状态重置】readAndTrigger 开头无条件清理 pendingTimer + 重置 clickCount=0 + 清理 clickTimer,确保每次三击前状态干净 - ✅ 【fetchReplacement 诊断】onerror/ontimeout 回调增加详细日志,打印 headers 和 URL,便于排查 ServiceBox 端跨域请求失败问题 */