// ==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 = '
| ' + String(replacements.cols[ci]).replace(/'; } html += ' |
|---|
| ' + String(replacements.rows[ri][ci]).replace(/'; } html += ' |