// ==UserScript== // @name ASTP/ServiceBox→CMIP 双击快查 v2.4(ServiceBox支持+空格清洗) // @namespace cmip-qq-v24 // @version 2.4.0 // @description 在 ASTP 和 ServiceBox 双击/三击备件编码直连CMIP查询。ServiceBox编码含空格自动清洗;建议零售价醒目;销售状态红/绿;替换关系6列中文;结果弹窗可拖动。 // @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'; 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'; 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) { var t = readLocalToken(); if (!t) { if (!silent) showToast('❌ 没读到 token', '#f56c6c'); return false; } saveToken(t); if (!silent) showToast('✅ Token 已同步(' + t.length + '位)', '#67c23a'); console.log('[CMIP快查] token已同步 len=' + t.length); return true; } // ===== 可拖动小圆点 ===== function injectDot() { if (document.getElementById('cmip-dot-v2')) return; var savedPos = null; try { savedPos = JSON.parse(GM_getValue(DOT_POS_KEY, '') || 'null'); } catch (e) { } if (!savedPos || typeof savedPos.x !== 'number') savedPos = { x: window.innerWidth - 48, y: 24 }; var dot = document.createElement('div'); dot.id = 'cmip-dot-v2'; dot.title = '点击:同步Token;拖拽:移动位置;手动查一次编码→自动记录请求头/替换数据'; dot.style.cssText = 'position:fixed;z-index:2147483646;width:14px;height:14px;border-radius:50%;background:linear-gradient(135deg,#67c23a,#409eff);cursor:pointer;box-shadow:0 1px 4px rgba(0,0,0,.35);user-select:none;transition:transform .12s;'; dot.style.left = savedPos.x + 'px'; dot.style.top = savedPos.y + 'px'; dot.addEventListener('mouseenter', function () { dot.style.transform = 'scale(1.9)'; }); dot.addEventListener('mouseleave', function () { dot.style.transform = 'scale(1)'; }); var dragging = false, moved = false, sx = 0, sy = 0, ox = 0, oy = 0; dot.addEventListener('mousedown', function (e) { dragging = true; moved = false; sx = e.clientX; sy = e.clientY; var rect = dot.getBoundingClientRect(); ox = rect.left; oy = rect.top; e.preventDefault(); }); 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 - 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'; }); document.addEventListener('mouseup', 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); } }); document.body.appendChild(dot); } // ===== XHR 拦截:记录请求头 + 替换关系(静默,不弹Toast) ===== (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|替代|替换|replacement|detail|明细|getReplace|partReplace/i.test(url) && !/getBasePartInfoList/i.test(url); if (url.indexOf('getBasePartInfoList') >= 0 || isReplacement) { this.addEventListener('load', function () { try { var txt = self.responseText; if (!txt) return; var json = JSON.parse(txt); // 从 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) { saveReplacement(code, json); console.log('[CMIP快查] ✅ 记录替换关系 code=' + code + ' rows=' + (function(){try{var d=json.data||{};for(var k in d){if(d[k]&&d[k].rows)return d[k].rows.length}return 0}catch(e){return 0}})()); } else { console.log('[CMIP快查] ⚠ 替换关系无法确定编码,数据未保存'); } } } catch (e) { } }); } return origSend.apply(this, arguments); }; } catch (e) { console.log('[CMIP快查] hook XHR失败:' + e.message); } })(); var tries = 0; var tid = setInterval(function () { tries++; if (document.body) { injectDot(); clearInterval(tid); setTimeout(function () { doSync(true); }, 600); } else if (tries > 30) { clearInterval(tid); } }, 200); 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); 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']] ]; 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; } function extractFields(row, code) { if (!row) return null; var out = {}, hasAny = false; for (var i = 0; i < FIELD_MAP.length; i++) { var v = pickVal(row, FIELD_MAP[i][1]); out[FIELD_MAP[i][0]] = v; if (v !== null && v !== undefined && v !== '') hasAny = true; } if (!out['备件编码']) out['备件编码'] = code; 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 = [ ['项目类别', ['projectType', 'projectCategory', 'projectTypeName', 'itemCategory', 'itemType', 'replaceType', 'substituteType', 'category', 'type']], ['备件编号', ['newPartNo', 'replacePartNo', 'substitutePartNo', 'partNo', 'replacePartCode', 'newPartCode', 'partCode']], ['备件名称', ['newPartName', 'replacePartName', 'substitutePartName', 'partName', 'replacePartNm', 'newPartNm', 'partNm']], ['组件数量', ['replaceQty', 'qty', 'quantity', 'replaceCount', 'componentQty', 'compQty', 'amount', 'count']], ['生效日期', ['effectiveDate', 'startDate', 'effectDate', 'fromDate', 'beginDate', 'validFrom', '生效日期', 'dateFrom']], ['文本号', ['textNo', 'textNumber', 'docNo', 'docNumber', 'noticeNo', 'textId', 'documentNo', 'txtNo', 'referenceNo']] ]; 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]); 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 showResult(code, data, errMsg, showOpenBtn, replacements) { var old = document.getElementById('cmip-qq-panel'); if (old) old.remove(); var 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:420px;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 valStr = (val !== null && val !== undefined && val !== '' ? String(val).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 + ''; } else { html += '' + valStr + ''; } html += '
'; } html += ''; // 替换明细表格(小字体,紧凑) if (replacements && replacements.cols && replacements.rows) { var rc = replacements.cols.length; var rr = replacements.rows.length; html += '
'; html += '
📋 配件替换关系(' + rr + '行×' + rc + '列)
'; html += '
'; html += ''; // 表头 html += ''; for (var ci = 0; ci < replacements.cols.length; ci++) { html += ''; // 行 for (var ri = 0; ri < replacements.rows.length; ri++) { html += ''; for (var ci = 0; ci < replacements.rows[ri].length; ci++) { html += ''; } html += '
' + String(replacements.cols[ci]).replace(/'; } html += '
' + String(replacements.rows[ri][ci]).replace(/'; } html += '
'; } } else { html += '
未找到备件数据
'; } if (showOpenBtn) { html += '
'; } panel.innerHTML = html; document.body.appendChild(panel); // 全局关闭:不管 × 上面被绑了什么,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(); var p = document.getElementById('cmip-qq-panel'); if (p && p.parentNode) p.parentNode.removeChild(p); } }, 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; } // 主动查询替换关系:用 CMIP 端保存的替换接口模板,替换编码后发请求 function fetchReplacement(code, authVal, callback) { var replApi = getReplApi(); if (!replApi || !replApi.url) { console.log('[CMIP快查] 替换关系:未保存接口模板(需先在CMIP点一次查看明细)'); callback(null); 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 () { callback(null); }, ontimeout: function () { callback(null); } }); } catch (e) { console.log('[CMIP快查] fetchReplacement异常: ' + e.message); callback(null); } } function queryCode(code) { code = String(code).trim().replace(/\s+/g, ''); if (!CODE_RE.test(code)) return; saveLastCode(code); var token = getToken(); if (!token) { showResult(code, null, '❌ 还没同步 Token/请求头。

' + '操作(只做一次):
' + '① 新标签打开 CMIP 并登录
' + '② 点屏幕上的绿色小圆点→同步
' + '③ 在 CMIP 手动查询一次 1109Z2
' + '④ 若要看替换关系,在 CMIP 点「查看明细」一次', true); 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 }), 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); } else if (r.fatal) { showResult(code, null, '❌ ' + r.msg + (r.raw ? ('
') : ''), true); } else { showResult(code, null, '❌ ' + r.msg, true); } 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); // 主动查询替换关系 fetchReplacement(code, usedAuth, function(repData) { if (repData) { console.log('[CMIP快查] ✅ 替换关系加载完成 ' + repData.rows.length + '行'); // 重新渲染弹窗(带替换表) showResult(code, ana.data, null, false, repData); } else { console.log('[CMIP快查] 替换关系:无或未获取到'); } }); } 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) { } showResult(code, null, '⚠ 认证通过,但没找到 ' + code + '
F12控制台有字段诊断,建议零售价为空请看控制台输出。', false); } return; } lastAna = ana; if (ana.fatal) { showResult(code, null, '❌ ' + ana.msg + (ana.raw ? ('
') : ''), true); return; } loop(); }); } loop(); } // ServiceBox: 双击只选中空格分隔的一段,三击才全选。 // 处理办法:在 dblclick 后再延迟 350ms 读 selection(这样三击的第三次 click 已经完成,selection 就是完整编码)。 var pendingTimer = null; function readAndTrigger(ev, extraLabel) { // ① 读当前 selection var code = ''; try { var sel = window.getSelection(); if (sel) code = (sel.toString() || '').trim(); } catch (e) { } // ② selection 空时 fallback:从点击的最近容器拿 textContent if (!code && ev) { try { var cell = ev.target.closest('td,th,tr,li,p,span,div,a,.cell,.vxe-body--column,.el-table__cell,pre,code'); if (cell) code = (cell.textContent || '').trim(); } catch (e) { } } // ③ 清洗:删除所有非字母数字字符(含空格、制表符、不间断空格、换行、中文标点…) var cleaned = code.replace(/[^A-Za-z0-9]/g, ''); if (!cleaned || !CODE_RE.test(cleaned)) return false; console.log('[CMIP快查] ' + (extraLabel || '捕获') + ' raw=' + JSON.stringify(code) + ' → cleaned=' + cleaned); queryCode(cleaned); return true; } document.addEventListener('dblclick', function (ev) { if (ev.target.closest('#cmip-qq-panel')) return; // ServiceBox 用延迟 350ms 方案(等三击),ASTP 正常即可 if (IS_SERVICEBOX) { // 先尝试立即读(双击选中的情况),不触发查询(可能只选中一半),只用于日志 try { var s = (window.getSelection().toString() || '').trim(); if (s) console.log('[CMIP快查] dblclick即时选中: ' + s); } catch (e) { } if (pendingTimer) clearTimeout(pendingTimer); pendingTimer = setTimeout(function () { readAndTrigger(ev, '三击/双击'); }, 350); } else { readAndTrigger(ev, '双击'); } }, true); // ServiceBox 额外:监听第三次 click,直接取消上面的 350ms 延迟,立即读 if (IS_SERVICEBOX) { var clickCount = 0; var clickTimer = null; document.addEventListener('click', function (ev) { if (ev.target.closest('#cmip-qq-panel')) return; clickCount++; if (clickTimer) clearTimeout(clickTimer); clickTimer = setTimeout(function () { clickCount = 0; }, 600); if (clickCount >= 3) { clickCount = 0; if (pendingTimer) { clearTimeout(pendingTimer); pendingTimer = null; } setTimeout(function () { readAndTrigger(ev, '三击'); }, 0); } }, true); } console.log('[CMIP快查] ' + SITE_NAME + ' v2.4 启动。ServiceBox特殊支持:三击含空格的编码(如 RP 16 431 885 80)。F12 可查看零售价字段诊断。'); } })();