作用域:5 个 ERP 页面(
purchaseout_Item/allocate_item/purchaseinitem/PackItems/otherOut_Item)
名字叫"图片放大",实际交付的是三件事:把货品大图贴在页面右上角、把真实可退货库存回填进表格、把退货下架/移仓这套手工流程一键跑完。
前两个功能是被动的(页面一动就自动跑),第三个是主动的(要手动点红字按钮)。
这个脚本与工作区里已分析过的另外几个脚本属于同一作者、同一套拦截机制,但版本坐标需要理清:
| 脚本 | 版本 | 关系 |
|---|---|---|
图片自动放大.js(本文档) |
5.2 | 主线。包含图片放大 + 表格回填 + 一键下架/移仓 |
物流盒子提示.js |
3.8 | 从主线早期版本派生的定制分支,加了玉石珍珠包装盒型判定,但没有表格回填和一键操作 |
仓位信息获取_v6.24.user.js |
6.24 | 另一条线(仓位占有信息栏),独立演进 |
搜索供应商_v3.2.5.user.js |
3.2.5 | 另一条线(退货单/供应商),独立演进 |
关键差异:主线 5.2 只保留两条静态提示(18k 金镶嵌 / 贵重),没有包装盒型判定;那部分逻辑被抽到 3.8 那个定制分支里单独维护了。所以两个脚本的提示条数量对不上是正常的,不是 bug。
RequestMonitor(劫持 XHR.send + window.fetch)
↓ 只放行 Method = CheckQty / LoadDataToJSON 的响应
├─→ ① 图片自动放大 + 价格变动提醒 ← 自动,无需操作
├─→ ② 表格库存回填(可退/未审列) ← 自动,跟随图片更新
└─→ ③ 一键下架 / 移仓 ← 手动点红字按钮
脚本启动时只做一件事:挂上拦截器(RequestMonitor.init(),L2691-2693)。面板不预建,等到第一次捕获到数据才创建。这是和早期版本的明显区别——不会在页面一打开就出现一个空框。
用户在 ERP 搜货品
↓ ERP 自己发 XHR / fetch(body 含 __CALLBACKPARAM,Method = CheckQty 或 LoadDataToJSON)
↓ RequestMonitor 的 hook 判为相关请求
↓ 响应回来 → parseResponseData 剥「0|」前缀 → JSON.parse → 再对 ReturnValue 二次 JSON.parse
↓ DataProcessor.findFirstImageUrl 递归扫整个响应体找第一个 http(s) 链接
↓ DataProcessor.extractProductInfo 递归提取 name / taxAfterPrice / sku_id
↓ RequestMonitor.updateDisplayedImage 渲染
拦截条件是双通道判断(isRelevantRequest,L2242):
| 判断位置 | 条件 |
|---|---|
| URL 里 | 含 __CALLBACKPARAM + Method:CheckQty 或 LoadDataToJSON |
| 请求体里 | 含 __CALLBACKPARAM(从 FormData / 字符串 / JSON 里解出) |
| 排除黑名单 | URL 含 PackItems.aspx · URL 含 sku_id · 含 [p].bin · 含 waitpay |
黑名单的 PackItems.aspx 是用来挡住脚本自己发的仓位查询请求,防止递归;waitpay 是挡住自己查订单出库的请求。
| 提示 | 触发条件 | 样式 | 代码位置 |
|---|---|---|---|
| 金镶嵌,注意包装 | 商品名小写后含 18k |
红字 | L2471 |
| 货品贵重,注意包装 | taxAfterPrice >= 600 |
灰字 | L2478 |
命中后走一条串行 6~7 个请求的链路:
checkPriceChange — 查商品资料修改日志(ItemApi/Log/GetPageListV2),用正则从 remark 抓「成本价:A->B」getOriginalPriceFromStockIn — 查入库单明细拿真实旧价(变动日前 1 个月 ~ 前 3 天)getStockInQty — 查入库单数量queryOperationQty — 查操作日志「直接采购入库」数量getTotalOperationQty — 拉退货单号列表,逐个查 qty 后求和queryOrderOutQty — 查订单出库数,并把用新价格成交的订单 qty 单独加上最终 新价可退数量 = 入库合计 − 出库合计:
价格变动, 120->135 2026-09-15, 新价格可退 18新价格已退完,请修改为旧价格这部分的防串号做得相当好:几乎每个 await 之后都重新校验 state.currentSkuId !== currentSkuId,如果不一致立刻 return。链路上 10 处这样的检查。因为用户可能连续搜多个货品,前一个货品的慢查询回来时不能覆盖当前显示——这个细节很多脚本会漏。
图片 load 后按宽高比二选一(L1882-1924):
| 图片形态 | 处理方式 |
|---|---|
| 高 > 宽(竖图) | 宽度撑满、高度自适应、容器可滚动,自动滚到垂直正中间 |
| 宽 > 高(横图) | 高度撑满、object-fit: cover + object-position: center —— 裁成正方形,留中间、切两边 |
URL 上会自动追加 OSS 参数 x-oss-process=image/resize,m_lfit,w_200,h_200(L2460)——即实际请求的是 200×200 缩略图再放大显示。容器隐藏了所有滚动条(::-webkit-scrollbar { display: none } + Firefox/IE 两套写法,L1664-1690),所以竖图是"看不见滚动条但能滑",用鼠标滚轮操作。
| 操作 | 效果 |
|---|---|
| 拖动面板 | top / right 都限制不小于 0,mousedown 时临时提 z-index 到 10000 并关掉过渡动画 |
| 点 ⊡ 按钮 | 收成 100×100 小方块;再点恢复原尺寸(记在 sizeBeforeMinimize) |
| 点 + / − | 每次 ±50px,最小 150px |
| 悬停 | 按钮透明度 0.8 → 1 |
| 位置记忆 | 按 protocol + host + pathname 存 localStorage.erpImageDisplayPositions(getBaseUrl(),L193) |
字号随面板宽度自适应:字号 = 14 × √(宽度/400),限制在 12~24px(L1693-1719)。用平方根而不是线性,缩小的时候字号变化更平缓。
每次图片更新后 setTimeout(1000ms) 触发 updateTableData()(L2681-2683)。
| 表格列 | 写入内容 | 颜色规则 |
|---|---|---|
| 可生效库存(原「实物库存」改名列) | 与「退货数量」比对 | 一致 = 绿 #4CAF50,不一致 = 红 #f44336 |
| 可退货库存 | 查询到的该 SKU 库存 | 同上 |
| 未审订单数 | GetPageListV2 的 order_lock |
≠ 0 = 红字加粗,= 0 = 黑字常规 |
任一新值为 0 时强制黑字不加粗(#333333),视觉上"归零就不刺眼"。
表头还会被改写:实物库存 → 可生效库存,并挂一个 12×12 的问号帮助图标 + tooltip(L5647-5726)。
ERP 的 jtable 可能渲染成 <table> 也可能渲染成 <div> 网格,脚本两套都处理:
findTargetTable()(L5209)先找 #_jt_body_list,找不到再退到任何含「商品编码」列的 <table>,最后退到 div 网格resolveFieldIndex(L5383):先精确匹配,再包含匹配,且包含匹配时可排除关键字(避免「商品编码」误命中「供应商商品编码」)updateTableData()
→ 写入一次
→ setTimeout(3000ms) 再写一次 ← 防 ERP 异步渲染覆盖
→ setTimeout(8000ms) 再写一次 ← 同上
另外表格上挂了 click 委托:点表格任意一行,延迟 100ms 后重新 processTableData(L5729-5749)。因为 ERP 点击行会重新渲染该行、把脚本写的值冲掉。
这是唯一会改变真实数据的功能,也是脚本最有业务价值的部分。
在 #_jt_toolbar_left 工具栏里插入两组控件:
[退货类型: ▼ 下拉框 ] [ 🔴 下架/移仓 ] ☐ 排除日期 [ 2026-09-21 ]
filterSku.aspx?type=12(限定条件下架任务)实时抓取规则列表,解析 #rule_list #list .item 里的 input,再用 XPath 找 data_{id} 元素解析出 bin 字段。抓不到就用 4 条硬编码兜底localStorage.erpExcludeDateAddAllSupplierItems、btModifyCostPrice、SyncQty、AddItemsForTask、$AppendTool(L3272)| 下拉项 | rule_id |
bin |
备注前缀 |
|---|---|---|---|
| 退供应商仓 | move_stock |
— | 退供应商仓 |
| P区纸质拣货(退样) | 2548 |
P |
P区 |
| P-1-1纸质拣货(销退退供) | 2592 |
P-1-1 |
P-1-1区 |
| P-1-4纸质拣货(品控次品汇总) | 2593 |
P-1-4 |
P-1-4区 |
| P-1-3次品下架(当日退) | 2613 |
P-1-3 |
P-1-3区 |
这是全脚本最该警惕的硬编码区:rule_id 和 bin 是从服务端动态抓的,但注释里的中文名和备注前缀是写死的。如果 ERP 那边改了规则名,下拉框会显示新名字,但写进备注的前缀还是旧的(generateRemark,L2737)。
1. 读退货类型 → 未选则提示退出
2. getReturnData():抓当前页 _jt_data,得到「SKU + 退货数量」
3. 分叉:
├─ move_stock → getAvailableStock() 查可移仓数量(PackItems.aspx, 页大小 500)
└─ 其他 4 种 → searchOffPickQty() 查可下架数量(CreateOffPick.aspx, SkuSearch)
↓ 返回 0 条时自动回退
searchBin() 旧版 like 查询(PackItems.aspx)
4. 若勾了「排除日期」→ getExclusionQuantities() 查该日期已操作数量
5. generateMoveRequestArray() 逐 SKU 比对:
调整后可移量 = max(0, 可移量 − 排除量)
移动量 = min(调整后可移量, 退货量)
同时产出 errorInfo(数量不足 / 数量超额 / 无可用数量 / 退货数量为0)
6. 有误差 → 弹确认框
├─ 用户取消 → 退出
└─ 用户确认 → 若含「数量不足」则**禁止提交**,只给操作引导
7. 提交:
├─ move_stock → sendMoveRequest() → MoveStock.aspx?am___=Move
└─ 其他 4 种 → confirmPick('returnOrder') → CreateOffPick.aspx?am___=Confirm
8. 成功后:
├─ 按「退货类型 + 款数 + 件数」生成备注 → updateRemark() 写回单据
└─ 自动点 #_jt_reload 刷新
第 6 步的"数量不足就禁止提交"是设计上的亮点——宁可让用户手工处理,也不让系统带着错误数量去下架。这类操作一旦错就是库存账实不符。
generateRemark(pickType, { styleCount, itemCount }) 生成形如 P区退货3款12件 的备注,通过 updateRemark() 写回退货单。
写入用的是 .NET WebForm 的 Save 回调:先 GET 页面拿 __VIEWSTATE / __VIEWSTATEGENERATOR,再 fetchCallbackData 拿当前行完整数据,把 remark 字段替换后整体回传。这是必须的——直接只发 remark 字段会丢掉其它字段。
showShelveModal / processShelveOperation(L4919 / L5056)实现了一条上架后自动接移仓的链:
SkuBinQuery/GetShelveFromPackItems 拿要上架的商品明细ShelveSkus,然后调 Shelve/SkuBatchShelveConfirm 确认上架confirmOffShelve(items, '8') 把货从别的仓位下架await 2000ms,自动点击页面上的「下架/移仓」按钮,接着跑 4.3 那条链这条链一口气做了"下架 → 上架 → 再下架/移仓"三个动作,全部自动衔接。
图片自动放大.js 全部内容 → 保存@match 已覆盖):purchaseout_Item.aspx(采购出库明细 —— 主战场)allocate_item.aspx(调拨)purchaseinitem.aspx(采购入库)PackItems.aspx(装箱)otherOut_Item.aspx(其他出库)@grant none,无授权弹窗,装完即用。
图片功能(无需操作) 搜货品 → 面板自动从右上角弹出 → 拖动/缩放/收小都行。位置会自动记住。
表格回填(无需操作) 面板弹出后约 1 秒,表格的「可退货库存」「可生效库存」「未审订单数」三列自动更新。如果数字几秒后跳变或被冲掉,等 3 秒和 8 秒那两次重写。
一键下架/移仓(要手动)
| 症状 | 先查什么 |
|---|---|
| 面板不弹 | 控制台看有没有请求被判为相关;确认你搜的货品页面走的是 CheckQty 或 LoadDataToJSON |
| 图片不显示 | 响应里有没有 http(s) 链接;findFirstImageUrl 是取第一个链接,可能取到了非图片链接 |
| 表格数字是 0 或没变 | 控制台看 [ERP图片放大][表格写入] 日志:未找到目标表格 / 一行都没写入 + 实际表头字段列表 |
| 表格数字对但一会儿变回去 | 正常,ERP 重渲染会覆盖,等 3 秒/8 秒的重写。仍不对就点一下表格行触发重处理 |
| 退货类型下拉框空白 | filterSku.aspx?type=12 可能被重定向到登录页,看控制台 [ERP图片放大][类型切换] |
| 下架提示"没有可下架的数据" | 先等表格库存列加载完;globalBinResults 为空时按钮就是空转 |
| 下架提交后返回失败 | 控制台看 [ERP图片放大][确认下架] 服务器返回失败: 后面的原文 |
| 价格提醒数量不对 | 那条链有 6~7 个请求,任一失败会降级成"只显示价格变动不显示数量" |
// 重置面板位置和尺寸(所有页面)
localStorage.removeItem('erpImageDisplayPositions');
// 重置记住的退货类型
localStorage.removeItem('erpOneClickPickType');
// 重置排除日期
localStorage.removeItem('erpExcludeDate');
// 清 VIEWSTATE 缓存(ApiClient 那套,30 分钟过期)
// key 形如 viewState_https___w_erp321_com_app_xxx
Object.keys(localStorage)
.filter(k => k.startsWith('viewState_'))
.forEach(k => localStorage.removeItem(k));
想改行为直接改这些位置:
| 想改什么 | 改哪里 | 当前值 |
|---|---|---|
| 面板默认尺寸 | L1439-1440 | 400px × 400px |
| 面板最小尺寸 | L2050 | 150px |
| 每次缩放步长 | L1807、L1814 | 50px |
| 收小后的小方块尺寸 | L1943-1944 | 100px × 100px |
| 提示字号基准 / 上下限 | L1702-1704 | 14 / 12 / 24 |
| 「金镶嵌」关键字 | L2471 | 18k |
| 「贵重」金额阈值 | L2478 | 600 |
| 价格变动有效期 | L2497-2498 | 1.5 个月 |
| 图片缩略图尺寸 | L2460 | w_200,h_200 |
| VIEWSTATE 缓存时长 | L320 | 30 分钟 |
| 未审订单查询条数 | L6016 | 2000 |
| 可移仓查询页大小 | L3812 | 500 |
| 未审订单最大条数分页 | L6016 | 不分页,硬上限 2000 |
| 退货单列表页大小 | L911 | 100 |
| 表格重写延迟 | L5854 | [3000, 8000] ms |
| 行点击后重处理延迟 | L5743-5746 | 100 ms |
| 图片更新后触发表格更新延迟 | L2681-2683 | 1000 ms |
| 表格数据索引偏移 | L5413、L5522 | +2 |
| 下架目标仓库 | L4287 | '10' |
| 原仓库兜底 ID | L4207、L4210、L3698 | '8' |
| 移仓 from / to | L4653-4654 | 912816726000008000 / 912816726000010000 |
| 上架默认仓位 | L5167 | 'P-1-5' |
| 上架 packType | L5122、L5178 | customize_3 |
以下问题都不报错、不抛异常,只是悄悄少做一点事或悄悄做错一点事。按危害从高到低排。
getOrderLockData 硬编码域名,违反脚本自己的注释(L6007)脚本开头第 34-36 行有一段明确警告:
// 注意:聚水潭 ERP 页面实际域名已变为 w.erp321.com(不再统一是 www.erp321.com)。
// 所有后端 API 请求必须使用 window.location.origin 动态拼接,写死 www.erp321.com
// 会被浏览器 CORS 策略拦截(fetch 直接失败)。请勿改回硬编码域名。
但 getOrderLockData 就写死了域名:
const url = `https://apiweb.erp321.com/webapi/ItemApi/ItemSku/GetPageListV2?...`;
同一文件里还有两处(L5093、L5172):
https://api.erp321.com/erp/webapi/WmsApi/SkuBinQuery/GetShelveFromPackItems
https://api.erp321.com/erp/webapi/WmsApi/Shelve/SkuBatchShelveConfirm
危害:如果当前页面不是 w.erp321.com(或后端不再为这些接口返回 CORS 头),这三处请求会直接失败。而失败全都被 catch { return [] } 吞掉 —— 结果是**「未审订单数」整列永远显示 0**,看起来像是"这个货真的没订单锁定"。
修法:三处统一改成 ${window.location.origin}/webapi/...,并加 catch 里的显式失败标记。
const u_co_id = getCookie('u_co_id') || '12816726';
'12816726' 是某个特定企业的 ID。当 cookie 取不到时不是报错,而是静默用别人的公司 ID 发请求。
危害:多公司 / 切换货主场景下,下架请求可能落到错误公司(服务端通常会拒,但一旦没拒就是跨公司数据污染)。
修法:改成取不到就抛错 throw new Error('未取到登录上下文,禁止发请求')。全文件 grep 一遍 || '12816726',两处都要改。
formData.append('move_from', '912816726000008000');
formData.append('move_to', '912816726000010000');
这两个串是编码过的仓位标识(912816726 前缀看起来正是公司 ID 12816726 的变体)。换公司、换仓库、甚至同一公司改仓位编码,这里都会静默失败或落到错误仓位。
同类的还有 targetWhId = '10'(L4287)、whId 兜底 '8'(L3698/L4207/L4210)、pack_type: 'wh_8'(L3811)、wh_id = 8(L3821)、上架 bin: "P-1-5"(L5167)、shelveFromPackType: "customize_3"(L5122)。
修法:这些值都应该从「退货类型配置」或页面下拉框解析出来,而不是写成字面量。至少要集中到一个 WAREHOUSE_CONST 常量对象里,并加注释说明来源。
extractProductInfo 的 SKU 候选键含过度泛化的 'id'(L2183)if ((obj.hasOwnProperty('sku_id') || obj.hasOwnProperty('id') ||
obj.hasOwnProperty('SkuId') || obj.hasOwnProperty('SKU_ID')) && !info.sku_id) {
info.sku_id = obj.sku_id || obj.id || obj.SkuId || obj.SKU_ID;
}
这是深度递归遍历,遇到第一个含 id 字段的对象就用它的 id 当 sku_id。而 ERP 响应里 id 出现在各种层级(订单 id、仓库 id、规则 id、row id…)。
危害:sku_id 会被喂给 checkPriceChange 和 getOrderLockData —— 可能给一个错误的货品查价格变动并弹出红字提示。用户看到的是"别的商品的价格变动",会以为数据串了。这个 'id' 在候选列表里还排在 SkuId / SKU_ID 之前,优先命中。
修法:分两层信任度 ——
sku_id= / "k":"sku_id","v":"..."),这个脚本的 isRelevantRequest 其实已经能看到请求体了'id' 移到候选列表末尾(或直接删掉)+2 偏移(L5413、L5522)// 调整索引,根据用户反馈,表头索引需要+2才是实际数据索引
const adjustedProductCodeIdx = productCodeIdx !== undefined ? productCodeIdx + 2 : undefined;
+2 是个经验值("根据用户反馈")。ERP 改了表格结构(比如前面插了一列序号、或去掉一个占位列)就会整体错位两列 —— 脚本会往「可退货库存」列写入别的列该有的值,且因为 replacedCount > 0 还会打印"写入成功"日志。
修法:改成从数据行反推偏移 —— 拿表头里已知唯一值的字段(如「商品编码」)在表头序号和数据行序号之间做差,动态算出偏移量。
headers['Cookie'] = document.cookie 是无效代码(L214、L265、L1359、L4069)四处都在手动设置 Cookie 请求头。Cookie 是 forbidden header name,浏览器静默忽略,写了永远不生效。
值得注意的是:这个脚本在 L5095-5096 有正确的做法和注释:
// 构建请求headers(cookie由credentials: 'include'自动携带,
// 浏览器禁止在fetch中手动设置Cookie头,写了也会被忽略)
说明作者已经知道这个问题了,但只修了 processShelveOperation 这一处,另外四处没改。
危害不是"功能坏了"(真正起作用的是 credentials: 'include'),而是排障时被误导:代码里明明"传了 cookie",于是把登录态问题排除掉,而真正的根因恰恰在 cookie 上。
const isRelevant = DataProcessor.isRelevantRequest(xhr.responseURL, data);
在 send() 阶段,xhr.responseURL 还是空字符串(要等 open() 之后、响应回来才有值)。所以 XHR 这条路的 URL 判断、以及黑名单里的 PackItems.aspx / sku_id / [p].bin 排除全部落空,实际只靠请求体判断。
对照:工作区里已分析过的 ERP_Unified_Board_v1.0.user.js 已经修掉了这个坑(hook 了 XMLHttpRequest.prototype.open 记录 URL)。可以照那个改法移植过来。
isRelevantRequest 的防自身递归靠巧合(L2247)黑名单只覆盖了 PackItems.aspx。但脚本自己还发这些请求:
| 自身调用 | 目标 | 是否被黑名单挡住 |
|---|---|---|
getAvailableStock / searchBin |
PackItems.aspx |
✅ 挡住 |
searchOffPickQty / confirmPick |
CreateOffPick.aspx |
❌ 靠 Method 不匹配侥幸 |
confirmOffShelve |
CreateOffPick.aspx |
❌ 同上 |
updateRemark / fetchCallbackData |
purchaseout.aspx |
❌ 同上 |
queryOrderOutQty |
order/list.aspx |
❌ 同上 |
后四条之所以没出事,是因为它们的 am___ 是 SkuSearch / Confirm / Save / Move,Method 不在放行名单里。但 Status 为 waitpay,... 那个 @= 过滤和 LoadDataToJSON 请求长得很像 —— 一旦 ERP 改了参数名,或者有人加一个新调用,就会立刻递归自触发。
修法:黑名单按「URL 路径 + am___ 参数 + Method」三元组覆盖全部自身调用形态,并在注释里列出清单。
ApiClient.request 用 await response.json() 直接解析;ApiClient.formRequest 走 parseResponseData(会剥 0| 前缀 + 二次解析 ReturnValue)。
危害:checkPriceChange 走的是 request()。如果后端返回 0|{...},response.json() 直接抛异常 → checkPriceChange 静默 return {hasChange:false} → 价格变动提醒整个功能静默消失,且控制台一声不响。
修法:两条路统一走 parseResponseData。
fetchJsonSafe 的 trim 到第一个 {(L164)while (text && !text.trim().startsWith('{')) {
text = text.slice(1);
if (text.length === 0) break;
}
如果响应是 JSON 数组([{...}])或带 0| 前缀,会被逐字符切掉直到遇到 {,第一个 { 之前的合法内容全丢。confirmPick 和 confirmOffShelve 都用这个函数。
修法:明确到 0| 这个 magic 前缀,而不是"第一个 {":
while (text && !text.startsWith('0|') && !text.startsWith('{') && !text.startsWith('[')) {
text = text.slice(1);
}
const costPriceMatch = item.remark.match(/成本价:([\d.]+)->([\d.]+)/);
「:」是全角。服务端文案一改成半角 : 或去掉空格差异,永久静默失配,价格提醒再也不弹,且没有任何告警(parsePriceChangeResponse 直接 return { hasChange: false })。
这与工作区里已分析过的 物流盒子提示.js v3.8 是完全相同的问题 —— 同一作者的另一个分支也没修。
修法:
/成本价\s*[::]\s*([\d.]+)\s*(?:->|→)\s*([\d.]+)/
// 未命中时:console.warn('[解析] remark 格式已变更,原文:', item.remark)
} catch (error) {
const errorMsg = `解析响应数据失败: ${error.message}`;
if (requestInfo.description) {
// console.warn(errorMsg); ← 全部被注释掉
}
return {};
}
全脚本的 console.warn / console.error 被注释掉的数量远超保留的(只有 19 处 console.log 幸存,且集中在新增的一键操作部分)。结果是后端一改数据结构,功能静默失效且没有任何线索。
修法:至少保留 console.warn,用统一前缀方便过滤;解析失败返回带原因的 { ok: false, reason } 而不是裸 {}。
"page": { "currentPage": 1, "pageSize": 2000, "pageAction": 1 }
按 skuPrefix* 模糊查,不分页。同一个 5 位前缀下 SKU 超过 2000 条时静默截断,超出部分的「未审订单数」列显示 0。而 orderBy: "customize_qty_5 ASC" 这个排序字段跟"未审订单"没有语义关系,截断掉哪些 SKU 是不可预测的。
修法:加 total 判断,超限时在 UI 上提示「仅显示前 2000 条」;或改为按当前页 SKU 列表精确查询。
三层叠加:
| 触发源 | 行为 |
|---|---|
| 每次图片更新 | setTimeout(1000ms) → 全量 updateTableData()(含 3~4 个网络请求) |
每次 updateTableData 结束 |
setTimeout(3000ms) + setTimeout(8000ms) 各再写一遍 |
| 表格行任意点击 | setTimeout(100ms) → 重新 processTableData |
用户快速连点几行表格时,会连续触发多轮。而 addRowClickListeners 用 table._rowClickListenersAdded 做了去重(好),但每次点击的处理本身没有去抖。
修法:给 processTableData 加防抖,或把行点击的重处理改成 requestIdleCallback。
processTableHeaders 会清掉表头里的图标(L5658、L5697)cell.textContent = '可生效库存';
textContent = ... 会清空该单元格的所有子节点。如果 ERP 原表头里有排序箭头、筛选图标、帮助图标,全被清掉。虽然随后脚本自己加了个问号图标(且有 !cell.querySelector('.help_bg') 的保护避免重复加),但原来的图标回不来了。
而且这个函数每次 updateTableData 都遍历 document.querySelectorAll('table') 全表扫描,无幂等跳过。
修法:改用「只改文本节点」的方式(遍历 childNodes 找 text node 替换),或用 innerHTML 前先保存并恢复原有元素。
getViewState 的 timeout 参数无效(L345)const response = await fetch(url, {
credentials: 'include',
mode: 'cors',
timeout: 10000 // ← fetch 不支持这个选项
});
fetch 没有 timeout 选项,正确写法是 signal: AbortSignal.timeout(10000)。这一行是纯粹的心理安慰,请求卡住时会一直挂着。
| 函数 | 位置 | 缓存 | 重试 | 取 generator |
|---|---|---|---|---|
ApiClient.getViewState |
L318 | ✅ 30 分钟 | ❌ | ✅ |
getViewStateFromUrl |
L4699 | ❌ | ❌ | ✅(还额外返回 html) |
confirmPick / getAvailableStock / searchOffPickQty 这些高频路径用的是没缓存的 getViewStateFromUrl —— 每次操作都要多发一次页面 GET。而 queryOperationQty / queryOrderOutQty 用带缓存的那个,失败时还会 clearViewStateCache 重试。
修法:合并成一个函数,把缓存和 html 返回都合进去。
.NET WebForm 三件套缺 __EVENTVALIDATION全脚本只有 __VIEWSTATE + __VIEWSTATEGENERATOR,没有任何地方处理 __EVENTVALIDATION。多数页面不需要,但某些开了事件校验的页面会返回 500 或被判 CSRF。
(这条与工作区里已分析过的 搜索供应商_v3.2.5.user.js 是同一个问题 —— 同系列脚本的通病。)
showShelveModal 插值未转义(L4968-4971)productItem.innerHTML = `
<span>${item.sku_id} ${modified1}</span>
<span>数量: ${item.userShelfQty}</span>
`;
modified1 来自 PackItems 的 modified1 字段(商品资料修改标记),sku_id 来自数据。虽然这两个字段通常可控,但同文件里其它地方(如 alert3.innerHTML)也是直接插值 —— 全脚本没有 escHtml 这样的统一转义函数。
| 位置 | 值 |
|---|---|
| 文件名 | 图片自动放大.js(无版本后缀) |
@version |
5.2 |
@description |
无 changelog |
| 启动日志 | 无版本号输出(L2691 的 init() 不打印版本) |
| 代码内注释 | 引用 image_drag_drop_uploader.user.js(另一个脚本名,可能导致读者找错文件) |
用户报"图片放大脚本有问题"时,无法从日志确认版本。建议 init() 里加一行 console.info('[ERP图片放大] v5.2 loaded', location.pathname)。
ApiClient.buildOperationQtyForm 里 creator 硬编码 5 个 ID(L478)// 使用固定的5个id,而不是传入的值
filters.push({"k":"creator","v":"20879672,21417895,21465083,22012863,22029078","c":"@="});
注释自己都说明白了"忽略传入值"。这意味着入库数量统计只算这 5 个人的操作,换人、加人、人员离职后统计都会静默偏小 —— 而偏小的入库数会直接导致"新价可退数量"算错。
await new Promise(resolve => setTimeout(resolve, 2000));
const transferButton = document.getElementById('one-click-transfer-btn');
if (!transferButton.disabled) transferButton.click();
用固定 2000ms 等待上架完成。网络慢时上架还没落库就点移仓,会把旧库存下架掉。应该轮询等待「上架成功」的确定信号,而不是固定延时。
monitorPageRefresh 已失效(L3890-3908、L3913)// 移除重复的事件监听器,避免重复执行
// window.addEventListener('load', init);
window load → init 那行被注释掉了。init() 现在只在脚本末尾裸调一次(L6068)。
于是 init() 里挂载 addPickTypeSelect / addOneClickTransferButton 时,如果 ERP 的 jtable 工具栏还没渲染出来,document.getElementById('_jt_toolbar_left') 返回 null,整个功能就静默不挂载(函数里只有空 else 分支,无重试)。
修法:用 MutationObserver 等 #_jt_toolbar_left 出现,或加带重试次数的轮询。
timeout / MAX_RETRIES 重试策略不一致| 函数 | 重试 | 重试时清 VIEWSTATE 缓存 |
|---|---|---|
queryOperationQty |
3 次,递增延迟 | ✅ |
queryOrderOutQty |
3 次,递增延迟 | ✅ |
getReturnOrderIds |
3 次,递增延迟 | ✅ |
getStockInQty |
MAX_RETRIES = 3 声明了 |
❌ 代码里没用(L1065 声明后未出现判断) |
getAvailableStock |
无 | — |
searchOffPickQty |
无 | — |
confirmPick / confirmOffShelve |
无 | — |
getStockInQty 声明了 MAX_RETRIES 却从不使用,是明显的半成品;而所有会改变数据的写操作(Confirm / Move)都没有重试,这个取舍是对的(写操作不能自动重试),但应该在注释里写明理由,避免后人"补上重试"反而造成重复下架。
state.originalWidth / originalHeight 写进去从未被读state 里有 originalWidth / originalHeight,在 createImageDisplay(L1443)、toggleMinimize(L1985)、resize(L2061)三处赋值,但全文没有任何地方读取它们。尺寸恢复实际靠的是 sizeBeforeMinimize。属于死代码,容易误导后续维护。
审这份脚本时,下面这些点是做得比同类脚本好的,修改时不要破坏:
currentSkuId 校验(L2490-2645)—— 连续搜索时避免慢查询回来覆盖当前显示。这个细节绝大多数脚本会漏。resolveFieldIndex 的两级匹配 + 排除关键字(L5383)—— 解决了「商品编码」误命中「供应商商品编码」,比硬编码列序号健壮得多。findTargetTable 的三级降级(L5240-5267)—— <table> / div 网格 / 全局扫描全覆盖,ERP 改渲染方式也不会立刻崩。_jt_page_increament_enabled 增量分页(L502-505)—— 订单出库查询用了 jtable 的增量翻页能力,避免拉全量。processShelveOperation 里对 forbidden header 的正确注释(L5095)—— 说明作者理解这个问题,只是没推广。MutationObserver)。修改这个脚本后,按下面逐项确认:
grep -n "https://[a-z]*\.erp321\.com" 图片自动放大.js → 除 @icon 外应无结果grep -n "|| '12816726'" 图片自动放大.js → 应为 0 结果grep -n "91281672600000" 图片自动放大.js → 确认 from/to 仓位是否仍有效grep -n "xhr.responseURL" 图片自动放大.js → 确认是否已改为 hook open()grep -n "Cookie': document.cookie" 图片自动放大.js → 应为 0 结果grep -n "成本价:" 图片自动放大.js → 确认正则已容错全角/半角grep -n "+ 2 :" 图片自动放大.js → 确认表头偏移是否改为动态计算grep -n "20879672,21417895" 图片自动放大.js → 确认 creator 白名单是否仍有效grep -n "MAX_RETRIES" 图片自动放大.js → 检查每个声明是否都被使用grep -n "timeout: 10000" 图片自动放大.js → fetch 不支持,应改为 AbortSignal.timeout@match 页面各试一次,确认工具栏按钮都挂上了@match 页面对应哪条链路| 页面 | 主要用到 |
|---|---|
purchaseout_Item.aspx(采购出库明细) |
全部三个模块 —— 主战场 |
allocate_item.aspx(调拨) |
模块 ①② |
purchaseinitem.aspx(采购入库) |
模块 ①② |
PackItems.aspx(装箱) |
模块 ①②(同时是脚本自己查询的目标页) |
otherOut_Item.aspx(其他出库) |
模块 ①② |
模块 ③ 的一键下架/移仓依赖 #_jt_toolbar_left 和 pick-type-select,只在采购出库明细页有完整工具栏。