// ==UserScript== // @name ERP仓位占有信息栏(统一兼容版 v6.24) // @namespace https://scriptcat.org/zh-CN/users/214207 // @version 6.24 // @description 在ERP系统中显示仓位数量和订单占有数,支持商品编码搜索、仓位多选勾选固定并记忆、单仓位(按仓位名)记忆锁定显示、单选后自动进入精简视图只显示该仓位、订单占有计算可排除指定仓库/仓位(自动记忆)、面板拉大时仓位明细同步放大、底部进度条始终显示占用比、CORS修复、兼容聚水潭;v6.17 新增「订单剩余发货时间卡片」——查到商品编码后自动统计 全部/已超时/今天必发/紧急(≤3天)/正常(>3天),点击卡片展开对应订单清单;v6.18 修复「仓位查到但显示为空」——PackItems.aspx 的 LoadDataToJSON 缺 owner_co_id/authorize_co_id 时接口静默返回 0 条,现自动从页面 URL/顶层/iframe/登录态补齐公司上下文;v6.19 修复「切换仓库后查不到数据」——令牌缓存绑定公司上下文(切仓即失效重取)、owner/authorize 强制覆盖隐藏域旧值、请求失败自动重取令牌重试一次并明确提示原因,另外被记忆的锁定/固定筛选挡住真实数据时给一键解除;v6.20 新增「折叠功能」——标题栏新增 ▾/▸ 折叠按钮,折叠后只留标题条(位置可拖、宽度可拉),折叠状态自动记忆(erpBinInfoFolded);修复「面板位置不记忆」——拖拽结束保存位置的条件写反(!hasMoved 才保存),实际拖动后反而不保存,现拖动结束即保存;折叠态下缩放只调宽度不记高度,展开后恢复原高度;v6.21 修正折叠粒度——不是折叠整个面板,而是「仓位明细里每一条仓位卡片」单独折叠(点击卡片标题行折叠/展开,折叠后只显示仓位名+数量/占有摘要)、单独拉大小(每张卡片右下角新增缩放手柄,拖动可横跨多列/加高,双击手柄恢复默认大小),每张卡片的折叠状态与大小都按仓位自动记忆(erpBinCardStates);v6.22 新增「一键折叠」——仓位明细标题栏新增 ⇕ 全部折叠/⊞ 全部展开 按钮,一键折叠或展开全部仓位卡片;商品信息徽章行、订单剩余发货时间卡片区、固定显示仓位区三个区块也各自新增 ▾/▸ 折叠箭头(折叠状态自动记忆 erpBinSecFolded);v6.23 全新「按页面维度独立记忆」——位置/大小/折叠/已关闭/自动显示/固定仓位/单选锁定/计算排除/卡片状态/区块折叠等所有设置都按页面类型(采购出/调拨/采购入/PackItems)分开存,互不干扰,并且新增「关闭自动显示」开关:标题栏右上角新增 ⏰/🚫 按钮,关掉后面板不再自动弹出;右侧浮动按钮(📦)若显示「关」红点说明当前页面已关闭自动显示,点红点即可恢复;v6.24 修复「仓位明细没有滚动条」——根因是 #erp-bin-detail-row 没有 flex 收缩约束,整个列表随内容自然撑高,溢出部分被面板 overflow:hidden 切掉,肉眼看不出可以滚。现让它和面板其他伸缩子项一样 flex:1 1 auto + min-height:0,列表自身补 max-height:100%,给 .erp-bin-list 加 webkit 滚动条样式(之前样式挂在 detail-row 上但它不滚动所以没用),同时把面板全折叠态下的高度下限从 360px 提到 200px(标题+按钮够用即可)让面板能更紧凑 // @author HuaSao // @match https://*.erp321.com/app/scm/purchaseout/purchaseout_Item.aspx* // @match https://*.erp321.com/app/wms/allocate/allocate_item.aspx* // @match https://*.erp321.com/app/scm/purchasein/purchaseinitem.aspx* // @match https://*.erp321.com/app/wms/Pack/PackItems.aspx* // @match https://*.erp321.com/app/wms/otherOut/otherOut_Item.aspx* // @match https://*.erp321.com/** // @icon https://files.erp321.com/img/platIcon/ziShen.png // @grant GM_xmlhttpRequest // @grant GM_log // @connect * // @license MIT // ==/UserScript== (function () { 'use strict'; // 防止重复加载 if (window.erpBinInfoScriptLoaded) return; window.erpBinInfoScriptLoaded = true; // 移动端检测 const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) || window.innerWidth <= 768; /*********************** * 工具函数 ***********************/ const createEl = (tag, attrs = {}, css = '') => { const el = document.createElement(tag); Object.assign(el, attrs); if (css) el.style.cssText = css; return el; }; const storage = { get(key, fallback = null) { try { const raw = localStorage.getItem(key); return raw ? JSON.parse(raw) : fallback; } catch (e) { return fallback; } }, set(key, value) { try { localStorage.setItem(key, JSON.stringify(value)); } catch (e) {} } }; // 🆕 v6.23 按页面维度独立记忆:4 种 ERP 页面各自一套配置(位置/大小/折叠/卡片/区块等) // 兼容旧版:先读「按页面」键,没有再回退读老键(一次性迁移,下次写入只写按页面键) const PAGE_KEY = (function () { const path = location.pathname || ''; if (/purchaseout_Item/i.test(path)) return 'purchaseout'; if (/allocate_item/i.test(path)) return 'allocate'; if (/purchaseinitem/i.test(path)) return 'purchasein'; if (/PackItems/i.test(path)) return 'packitems'; return 'default'; })(); const pStorage = { get(key, fallback = null) { try { const raw = localStorage.getItem(key + '__' + PAGE_KEY); if (raw !== null) return JSON.parse(raw); } catch (e) {} return storage.get(key, fallback); }, set(key, value) { try { localStorage.setItem(key + '__' + PAGE_KEY, JSON.stringify(value)); } catch (e) {} // 同步写一份到老键(保留旧客户端/调试可见),新读取一律走 pStorage storage.set(key, value); } }; // 写历史:方便用户看到当前按页面存的实际 key 集合 console.log('[仓位信息栏 v6.23] 当前页面 pageKey =', PAGE_KEY); // 安全解析响应数据 const parseResponseData = (responseText) => { try { let processedText = responseText; if (processedText.startsWith('0|')) processedText = processedText.substring(2); const responseObj = JSON.parse(processedText); if (responseObj.ReturnValue && typeof responseObj.ReturnValue === 'string') { try { responseObj.ReturnValue = JSON.parse(responseObj.ReturnValue); } catch (e) {} } return responseObj; } catch (error) { return {}; } }; // 兼容多种仓位字段名 function getBin(item) { if (!item || typeof item !== 'object') return ''; return item.bin || item.location || item.position || item.wh_name || item.bin_code || item.warehouse_pos || item.Bin || item.Location || item.Position || item['仓位'] || item['bin_name'] || item['location_name'] || item['仓位名称'] || item['货位'] || ''; } // 兼容多种「每仓位订单占有数」字段名 function getItemLock(item) { if (!item || typeof item !== 'object') return null; // 1. 优先从 item 本身查找(仓位接口返回的逐行锁字段) const raw = getItemLockRaw(item); if (raw != null) return raw; // 2. 回退:从 lockData[currentSkuId] 取 SKU 级锁数(解决“第二次更新才显示”问题) if (currentSkuId && lockData && lockData[currentSkuId]) { const info = lockData[currentSkuId]; if (info && info.order_lock != null) { const n = parseFloat(info.order_lock); if (!isNaN(n) && n > 0) return n; } } return null; } // 🆕 仅从 item 本身读取「每仓位订单占有数」(不回退到 SKU 级),用于排除计算扣减 function getItemLockRaw(item) { if (!item || typeof item !== 'object') return null; const keys = ['order_lock', 'lock_qty', 'locked_qty', 'occupy_qty', 'occupy', 'locked', 'qty_lock', 'order_occupy']; for (const k of keys) { if (item[k] != null) { const n = parseFloat(item[k]); if (!isNaN(n)) return n; } } return null; } // 🆕 单条仓位卡片的唯一 key:仓位名优先,没有仓位名时退回仓库 ID(B:/W: 前缀区分) function keyOfItem(it) { const b = getBin(it); if (b && b !== '-' && b !== '未指定仓位') return 'B:' + b; return 'W:' + (it.wh_id || '-'); } /*********************** * 全局配置 ***********************/ const CONFIG = { ITEM_API: 'https://apiweb.erp321.com/webapi/ItemApi/ItemSku/GetPageListV2?__from=web_component&owner_co_id=12816726&authorize_co_id=12816726', PAGE_SIZE: 500, TOKEN_EXPIRE: 300000, CACHE_DURATION: 5 * 60 * 1000 }; // 🆕 v6.18 公司上下文(owner_co_id / authorize_co_id) // ⚠️ 实测(2026-09-20 真实接口回放):PackItems.aspx 的 LoadDataToJSON // 不带 owner_co_id / authorize_co_id 时,接口返回 IsSuccess=true 但 DataCount=0(静默空结果), // 看起来像"该 SKU 没有仓位",其实是公司上下文丢失。 function getCompanyCtx() { const read = (w) => { try { const p = new URLSearchParams(w.location.search); return { owner: p.get('owner_co_id') || '', auth: p.get('authorize_co_id') || '' }; } catch (_) { return { owner: '', auth: '' }; } }; let ctx = read(window); // 当前 frame 的 URL 上没有时,依次向顶层、同域 iframe 兜底 if (!ctx.owner) { const t = read(window.top); if (t.owner) ctx = { owner: t.owner, auth: t.auth || ctx.auth }; } if (!ctx.owner) { try { document.querySelectorAll('iframe').forEach(f => { if (ctx.owner) return; const r = read(f.contentWindow); if (r.owner) ctx = { owner: r.owner, auth: r.auth || ctx.auth }; }); } catch (_) {} } // 最后兜底:登录态里记的公司 ID if (!ctx.owner) { try { ctx.owner = localStorage.getItem('gylUser_coId_wy') || ''; } catch (_) {} } return ctx; } // 动态获取 BIN_API(避免 CORS) function getBinApiUrl() { const { owner, auth } = getCompanyCtx(); let url = window.location.origin + '/app/wms/Pack/PackItems.aspx?_c=jst-epaas&epaas=true'; if (owner) url += '&owner_co_id=' + encodeURIComponent(owner); if (auth) url += '&authorize_co_id=' + encodeURIComponent(auth); return url; } /*********************** * 全局数据 ***********************/ let lockData = {}; let lockLoading = false; let lockLoaded = false; let currentBinItems = []; let currentSkuId = null; let currentLockQty = 0; let currentTotalQty = 0; let currentWhInfo = null; let lastBinHint = ''; // 🆕 v6.19 仓位查询的“非致命但会影响结果”的提示(如未能识别公司上下文) let selectedWhIds = new Set(); let singleMode = false; let singleWhId = null; let excludedKeys = new Set(); let formTokens = null; let tokenTime = 0; let tokenCtxSig = ''; // 🆕 v6.19 令牌对应的公司上下文签名:切换仓库/货主后签名变化,缓存立即作废 /*********************** * 认证信息获取 ***********************/ function getAuth() { let gwfp = localStorage.getItem('gwfp') || ''; let uid = localStorage.getItem('gylUser_uid_wy') || ''; let coid = localStorage.getItem('gylUser_coId_wy') || ''; // 尝试从 iframe 获取 if (!gwfp || !uid) { document.querySelectorAll('iframe').forEach(f => { try { const l = f.contentWindow?.localStorage; if (l) { gwfp = gwfp || l.getItem('gwfp') || ''; uid = uid || l.getItem('gylUser_uid_wy') || ''; coid = coid || l.getItem('gylUser_coId_wy') || ''; } } catch(_) {} }); } return { gwfp, uid, coid }; } /*********************** * 订单占有数据加载 ***********************/ async function loadLockData() { const auth = getAuth(); if (!auth.gwfp) { console.warn('[仓位信息栏] 未获取到认证信息'); return; } if (lockLoading) return; lockLoading = true; console.log('[仓位信息栏] 开始加载订单占有数据...'); try { let all = []; let page = 1; let hasMore = true; const queryFlds = ['sku_id', 'order_lock', 'supplier_name', 'i_id', 'name', 'properties_value', 'bin']; while (hasMore) { const body = { ip: '', uid: auth.uid, coid: auth.coid, page: { currentPage: page, pageSize: CONFIG.PAGE_SIZE, pageAction: 1 }, data: { sku_type: 1, queryFlds: queryFlds, orderBy: 'order_lock DESC', enabled: '1' } }; const resp = await fetch(CONFIG.ITEM_API, { method: 'POST', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json; charset=utf-8', 'Webbox-Request-Id': crypto.randomUUID?.() || 'jst-' + Date.now(), 'Webbox-Route-Path': '/erp-components/goods-selector/', 'gwfp': auth.gwfp, 'u_sso_token': '' }, credentials: 'include', body: JSON.stringify(body) }); const json = await resp.json(); if (json.code !== 0) { throw new Error('API错误: ' + (json.msg || 'code=' + json.code)); } const data = json.data || []; all = all.concat(data); if (data.length < CONFIG.PAGE_SIZE) { hasMore = false; } else { const lastLock = parseFloat(data[data.length - 1]?.order_lock) || 0; if (lastLock === 0) { hasMore = false; } else { page++; } } if (page > 20) { console.warn('[仓位信息栏] 已达到最大查询页数'); break; } } // 只保留有订单占有的SKU lockData = {}; for (const item of all) { const lock = parseFloat(item.order_lock) || 0; if (lock > 0) { lockData[item.sku_id] = { order_lock: lock, supplier_name: item.supplier_name || '', i_id: item.i_id || '', name: item.name || '', properties_value: item.properties_value || '', bin: getBin(item) }; } } lockLoaded = true; const totalLock = Object.values(lockData).reduce((sum, item) => sum + item.order_lock, 0); console.log('[仓位信息栏] ✅ 订单占有加载完成:', Object.keys(lockData).length, '个SKU,合计', totalLock, '件'); // 🆕 订单占有到达后自动重渲染整个面板(顶部占有徽章+右上百分比+底部进度条+明细卡片+供应商) if (currentSkuId && UI && typeof UI.updateInfoBar === 'function') { try { const updatedLock = lockData[currentSkuId]?.order_lock || 0; UI.updateInfoBar(currentSkuId, currentTotalQty, updatedLock, currentBinItems, currentWhInfo); } catch (e) { console.warn('[仓位信息栏] 自动刷新面板失败:', e.message); } } } catch(e) { console.error('[仓位信息栏] ❌ 订单占有加载失败:', e.message); lockLoaded = false; } lockLoading = false; } /*********************** * 仓位库存查询 - 使用 GM_xmlhttpRequest 绕过 CORS ***********************/ async function getTokensWithGM() { const ctx = getCompanyCtx(); const ctxSig = (ctx.owner || '') + '/' + (ctx.auth || ''); // 🆕 v6.19 缓存必须绑定公司上下文:切换仓库/货主后 URL 上的 owner/authorize 变了, // 旧令牌里的隐藏域还是上一套上下文,继续复用会让接口静默返回 0 条。 if (formTokens && tokenCtxSig === ctxSig && Date.now() - tokenTime < CONFIG.TOKEN_EXPIRE) { return formTokens; } if (formTokens && tokenCtxSig !== ctxSig) { console.log('[仓位信息栏] 检测到仓库/公司上下文变化(' + (tokenCtxSig || '空') + ' → ' + (ctxSig || '空') + '),重新获取表单令牌'); } const url = getBinApiUrl() + '&ts___=' + Date.now(); return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: url, headers: { 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' }, onload: function(response) { try { const html = response.responseText; const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); const tokens = {}; doc.querySelectorAll('input[type="hidden"]').forEach(el => { if (el.name) tokens[el.name] = el.value; }); doc.querySelectorAll('input:not([type="hidden"])').forEach(el => { if (el.name && !tokens[el.name]) tokens[el.name] = el.value || ''; }); formTokens = tokens; tokenTime = Date.now(); tokenCtxSig = ctxSig; resolve(tokens); } catch (e) { reject(e); } }, onerror: function(error) { console.warn('[仓位信息栏] getTokensWithGM 失败,尝试从当前页面提取'); // 备用:从当前页面DOM提取 try { const tokens = {}; document.querySelectorAll('input').forEach(el => { if (el.name) tokens[el.name] = el.value || ''; }); formTokens = tokens; tokenTime = Date.now(); tokenCtxSig = ctxSig; resolve(tokens); } catch (fallbackError) { reject(new Error('获取Token失败')); } } }); }); } // 🆕 v6.19 单次仓位请求(便于失败后清缓存重试) function postBinStock(tokens, sku_id) { const filter = JSON.stringify([{ k: '[pit].sku_id', v: sku_id, c: '=' }]); const callbackParam = JSON.stringify({ Method: 'LoadDataToJSON', Args: ['1', filter, '{}'] }); const params = new URLSearchParams(); for (const [k, v] of Object.entries(tokens)) { if (k) params.append(k, v); } params.append('__CALLBACKID', 'JTable1'); params.append('__CALLBACKPARAM', callbackParam); params.set('_jt_page_size', '50'); // 🆕 v6.18/6.19 公司上下文:必须用宿主页面上的实时值「覆盖」隐藏域里的旧值。 // 实测:owner_co_id=12816726 + authorize_co_id=12816726 → DataCount=0; // owner_co_id=12816726 + authorize_co_id=15371422 → 正常返回。 // 用 !params.get() 只补不覆盖是不够的——隐藏域里往往已经有一个过期值。 const ctx = getCompanyCtx(); if (ctx.owner) params.set('owner_co_id', ctx.owner); if (ctx.auth) params.set('authorize_co_id', ctx.auth); const url = getBinApiUrl() + '&ts___=' + Date.now() + '&am___=LoadDataToJSON'; return new Promise((resolve) => { GM_xmlhttpRequest({ method: 'POST', url: url, headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': '*/*' }, data: params.toString(), onload: function(response) { try { const text = (response && response.responseText) || ''; const pipeIdx = text.indexOf('|'); // 非回调响应(空响应 / HTML 登录页 / 过期令牌):明确标记为失败,不再伪装成“没数据” if (pipeIdx < 0 || pipeIdx > 5) { resolve({ ok: false, items: [], whInfo: null, reason: text ? '非回调响应(长度 ' + text.length + ')' : '空响应' }); return; } const result = JSON.parse(text.substring(pipeIdx + 1)); if (!result.IsSuccess) { resolve({ ok: false, items: [], whInfo: null, reason: 'IsSuccess=false: ' + (result.ExceptionMessage || '') }); return; } const data = JSON.parse(result.ReturnValue); const items = data.datas || []; let whInfo = null; if (items.length > 0) { whInfo = items[0].wh_name || items[0].warehouse_name || items[0].wh_id || null; } resolve({ ok: true, items, whInfo, ctxKnown: !!(ctx.owner || ctx.auth) }); } catch (e) { console.error('[仓位信息栏] 解析仓位数据失败:', e); resolve({ ok: false, items: [], whInfo: null, reason: '解析异常: ' + e.message }); } }, onerror: function(error) { console.error('[仓位信息栏] 查询仓位失败'); resolve({ ok: false, items: [], whInfo: null, reason: '网络错误' }); } }); }); } async function queryBinStock(sku_id) { try { let res = await postBinStock(await getTokensWithGM(), sku_id); // 🆕 v6.19 失败自动重试一次:切换仓库 / 会话过期 / 令牌失效都会让首次请求拿到空响应, // 这里强制丢弃令牌缓存后用新上下文重取一遍,而不是直接返回“无数据”。 if (!res.ok) { console.warn('[仓位信息栏] 仓位请求失败(' + res.reason + '),重取表单令牌后重试一次…'); formTokens = null; tokenTime = 0; tokenCtxSig = ''; try { res = await postBinStock(await getTokensWithGM(), sku_id); } catch (e) { console.error('[仓位信息栏] 重试失败:', e); } } if (!res.ok) { console.error('[仓位信息栏] ❌ 仓位查询失败(重试后仍失败):', res.reason, '\n · 若刚切换过仓库/货主,请按 F5 刷一次页面\n · 若刚重新登录,请刷新页面后重试'); } else if (res.items.length === 0 && !res.ctxKnown) { // 没拿到公司/仓库上下文时,“0 条”有两种可能:该商品确实没仓位,或上下文不对被静默降级。 // 单看响应无法区分(接口两种情况都返回 IsSuccess=true),所以必须提示用户而不是默认当成“无仓位”。 res.hint = '未能识别当前公司/仓库上下文(结果可能不完整),请按 F5 刷新页面后再搜一次'; console.warn('[仓位信息栏] ⚠️ ' + res.hint); } return res; } catch(e) { console.error('[仓位信息栏] queryBinStock 异常:', e); return { ok: false, items: [], whInfo: null, reason: String(e && e.message || e) }; } } /*********************** * 🆕 v6.17 订单剩余发货时间卡片 * 查询到商品编码后,同步请求该 SKU 的订单,按「计划发货时间」统计: * 全部订单 / 已超时 / 今天必发 / 紧急(≤3天) / 正常(>3天) * 在面板顶部显示一排卡片,点击卡片可展开对应订单清单。 ***********************/ const ORDER_API = 'https://api.erp321.com/erp/webapi/ItemApi/ItemInventory/GetOrderLockShowV2?owner_co_id=12816726&authorize_co_id=12816726'; const ORDER_PAGE_SIZE = 500; const ORDER_MAX_PAGES = 6; let remOrders = []; // 当前 SKU 的订单(含剩余发货时间计算结果) let remActiveBucket = 'all'; // 当前高亮的卡片 let remListOpen = false; // 订单清单是否展开 let remReqToken = 0; // 请求令牌,防止旧请求覆盖新结果 function pad2(n) { return n < 10 ? '0' + n : '' + n; } function parseOrderTime(s) { if (!s) return null; const m = String(s).match(/^(\d{4})-(\d{1,2})-(\d{1,2})(?:[T\s]+(\d{1,2}):(\d{1,2}))?/); if (m) return new Date(+m[1], +m[2] - 1, +m[3], +(m[4] || 0), +(m[5] || 0)); const d = new Date(s); return isNaN(d.getTime()) ? null : d; } // 剩余发货时间分档(口径与统一看板一致) function remBucketOf(status, planDeliveryDate) { const st = status || ''; if (st === '已发货' || st === '已完成' || st === '已取消') return 'done'; const pd = parseOrderTime(planDeliveryDate); if (!pd) return 'done'; const days = Math.ceil((pd.getTime() - Date.now()) / 86400000); if (days < 0) return 'over'; if (days === 0) return 'today'; if (days <= 3) return 'warn'; return 'ok'; } function remTextOf(ms, bucket) { if (bucket === 'done' || ms == null) return '无计划时间'; const t = Math.abs(ms); const d = Math.floor(t / 86400000); const h = Math.floor((t % 86400000) / 3600000); const mi = Math.floor((t % 3600000) / 60000); if (bucket === 'over') return '超' + d + '天' + pad2(h) + '时' + pad2(mi) + '分'; if (bucket === 'today') return (ms < 0 ? '已超' : '剩') + pad2(h) + '时' + pad2(mi) + '分'; return '剩' + d + '天' + pad2(h) + '时' + pad2(mi) + '分'; } function fetchOrderPage(skuId, page) { return new Promise((resolve) => { GM_xmlhttpRequest({ method: 'POST', url: ORDER_API, headers: { 'content-type': 'application/json', 'accept': 'application/json' }, data: JSON.stringify({ ip: '', uid: '21465083', coid: '12816726', data: { orderBy: '', skuId: skuId, oids: [], shopids: [] }, page: { currentPage: page, pageSize: ORDER_PAGE_SIZE } }), onload: (r) => { try { const json = JSON.parse(r.responseText); resolve({ orders: json.data || [], total: (json.page && json.page.count) || 0 }); } catch (e) { resolve({ orders: [], total: 0 }); } }, onerror: () => resolve({ orders: [], total: 0 }) }); }); } async function fetchSkuOrders(skuId) { const first = await fetchOrderPage(skuId, 1); const all = (first.orders || []).slice(); const totalPages = Math.min(ORDER_MAX_PAGES, Math.ceil((first.total || 0) / ORDER_PAGE_SIZE) || 1); if (totalPages > 1) { const rest = await Promise.all( Array.from({ length: totalPages - 1 }, (_, i) => fetchOrderPage(skuId, i + 2)) ); rest.forEach(r => all.push(...(r.orders || []))); } const orders = all.map(o => { const pd = parseOrderTime(o.planDeliveryDate); const ms = pd ? pd.getTime() - Date.now() : null; const bucket = remBucketOf(o.status, o.planDeliveryDate); return { oId: o.oId || o.orderId || '-', shop: o.shopName || o.shop_name || '-', status: o.status || '', qty: o.qty || 1, plan: o.planDeliveryDate || '', pay: o.payDate || '', _ms: ms, _bucket: bucket, _text: remTextOf(ms, bucket) }; }); return { orders: orders, total: first.total || orders.length }; } // 卡片定义(外观与统一看板「订单剩余发货时间」卡片一致) const REM_CARDS = [ { key: 'all', label: '全部订单', sub: '', icon: '📦', color: '#6b7280', bg: '#f3f4f6' }, { key: 'over', label: '已超时', sub: '超期', icon: '⚠️', color: '#991b1b', bg: '#fee2e2' }, { key: 'today', label: '今天必发', sub: '今日', icon: '🔥', color: '#b91c1c', bg: '#fee2e2' }, { key: 'warn', label: '紧急', sub: '≤3天', icon: '⏱️', color: '#92400e', bg: '#fef3c7' }, { key: 'ok', label: '正常', sub: '>3天', icon: '✅', color: '#065f46', bg: '#d1fae5' } ]; function remCounts() { const c = { all: 0, over: 0, today: 0, warn: 0, ok: 0, done: 0 }; for (const o of remOrders) { c.all++; if (o._bucket === 'done') c.done++; else if (c[o._bucket] != null) c[o._bucket]++; } return c; } function remCardTip(key, c) { if (key === 'all') { return '共 ' + c.all + ' 单' + (c.done ? '(已发货/完成/取消 ' + c.done + ' 单)' : '') + ' 点击查看订单清单'; } const labels = { over: '已超时', today: '今天必发', warn: '剩余≤3天', ok: '剩余>3天' }; return (labels[key] || key) + ':' + (c[key] || 0) + ' 单 点击查看订单清单'; } // 🆕 v6.22 首次出数据时显示「订单剩余发货时间」区块的折叠行 function showRemFoldRow() { const fr = document.getElementById('erp-bin-rem-fold-row'); if (fr) fr.classList.remove('erp-rem-hidden'); } function renderRemCards() { const host = document.getElementById('erp-bin-rem-cards'); if (!host) return; host.classList.remove('erp-rem-hidden'); showRemFoldRow(); const c = remCounts(); let html = ''; REM_CARDS.forEach(f => { const cnt = f.key === 'all' ? c.all : (c[f.key] || 0); const act = remActiveBucket === f.key; const style = 'border-left:3px solid ' + f.color + ';' + (act ? 'background:' + f.bg + ';border-color:' + f.color + ';' : ''); html += '
' + '
' + f.icon + '
' + '
' + '
' + cnt + '
' + '
' + f.label + '
' + (f.sub ? '
' + f.sub + '
' : '') + '
' + '
'; }); host.innerHTML = html; host.querySelectorAll('.erp-rem-card').forEach(el => { el.addEventListener('click', (ev) => { ev.stopPropagation(); const k = el.getAttribute('data-rbk'); if (remActiveBucket === k) { remListOpen = !remListOpen; } else { remActiveBucket = k; remListOpen = true; } renderRemCards(); }); }); renderRemList(); } function renderRemList() { const list = document.getElementById('erp-bin-rem-list'); if (!list) return; if (!remListOpen) { list.style.display = 'none'; list.innerHTML = ''; return; } let arr = remOrders.filter(o => remActiveBucket === 'all' ? true : o._bucket === remActiveBucket); arr = arr.slice().sort((a, b) => { const ad = a._bucket === 'done' ? 1 : 0; const bd = b._bucket === 'done' ? 1 : 0; if (ad !== bd) return ad - bd; if (ad === 1) return 0; return (a._ms || 0) - (b._ms || 0); }); const cardDef = REM_CARDS.filter(f => f.key === remActiveBucket)[0] || REM_CARDS[0]; const head = '
' + cardDef.label + ' · ' + arr.length + ' 单' + (remActiveBucket === 'all' ? '(按紧急度排序)' : '') + '
'; if (!arr.length) { list.innerHTML = head + '
该分类下暂无订单
'; list.style.display = 'block'; return; } const LIMIT = 60; let rows = ''; arr.slice(0, LIMIT).forEach(o => { const stShow = o._bucket === 'done' ? '已发出' : o._text; rows += '
' + '' + o.oId + '' + '' + o.shop + '' + '' + o.status + '' + '' + o.qty + '' + '' + stShow + '' + '
'; }); if (arr.length > LIMIT) { rows += '
仅显示前 ' + LIMIT + ' 单,还有 ' + (arr.length - LIMIT) + ' 单…
'; } list.innerHTML = head + rows; list.style.display = 'block'; } // 加载并渲染某个 SKU 的剩余发货时间卡片 async function loadRemCards(sku_id) { let host = document.getElementById('erp-bin-rem-cards'); if (!host) { // 自动触发场景下面板可能尚未创建,稍后重试一次 await new Promise(r => setTimeout(r, 300)); host = document.getElementById('erp-bin-rem-cards'); if (!host) return; } const my = ++remReqToken; remOrders = []; remActiveBucket = 'all'; remListOpen = false; host.classList.remove('erp-rem-hidden'); showRemFoldRow(); host.innerHTML = '
⏳ 正在统计订单剩余发货时间…
'; const list = document.getElementById('erp-bin-rem-list'); if (list) { list.style.display = 'none'; list.innerHTML = ''; } let res = null; try { res = await fetchSkuOrders(sku_id); } catch (e) { res = null; } if (my !== remReqToken) return; // 已有更新的查询,丢弃本次结果 if (!res) { host.innerHTML = '
⚠️ 订单剩余发货时间获取失败
'; return; } remOrders = res.orders || []; renderRemCards(); } // 启动加载订单占有数据 setTimeout(() => { loadLockData().catch(e => console.warn('[仓位信息栏] 初始化加载失败:', e.message)); }, 1000); /*********************** * 状态管理 ***********************/ const state = { ui: null }; /*********************** * 注入全局样式 ***********************/ const STYLE_ID = 'erp-bin-info-styles'; function injectStyles() { if (document.getElementById(STYLE_ID)) return; const style = document.createElement('style'); style.id = STYLE_ID; style.textContent = ` #erp-bin-info-panel { font-family: -apple-system, 'Microsoft YaHei', 'PingFang SC', sans-serif !important; font-size: 12px !important; transition: box-shadow 0.2s ease, opacity 0.3s ease, transform 0.3s ease; } #erp-bin-info-panel:hover { box-shadow: 0 6px 24px rgba(0,0,0,0.15) !important; } #erp-bin-info-panel.hiding { opacity: 0; transform: translateX(20px); pointer-events: none; } #erp-bin-info-header { padding: 8px 12px !important; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important; color: #fff !important; font-weight: 600 !important; border-radius: 10px 10px 0 0 !important; display: flex !important; justify-content: space-between !important; align-items: center !important; letter-spacing: 0.5px; } #erp-bin-info-header span:first-child { font-size: 13px !important; } #erp-bin-sku-search-row { display: flex !important; align-items: center !important; gap: 6px !important; padding: 8px 12px !important; background: #f8fafc !important; border-bottom: 1px solid #e2e8f0 !important; } #erp-bin-sku-search-input { flex: 1 !important; padding: 6px 10px !important; border: 1.5px solid #e2e8f0 !important; border-radius: 6px !important; font-size: 12px !important; background: #fff !important; color: #334155 !important; outline: none !important; transition: all 0.2s ease !important; } #erp-bin-sku-search-input:focus { border-color: #667eea !important; box-shadow: 0 0 0 3px rgba(102,126,234,0.1) !important; } #erp-bin-sku-search-input::placeholder { color: #94a3b8 !important; } #erp-bin-sku-search-btn { padding: 6px 12px !important; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important; color: #fff !important; border: none !important; border-radius: 6px !important; font-size: 12px !important; font-weight: 600 !important; cursor: pointer !important; transition: all 0.2s ease !important; white-space: nowrap !important; } #erp-bin-sku-search-btn:hover { transform: translateY(-1px) !important; box-shadow: 0 2px 8px rgba(102,126,234,0.3) !important; } #erp-bin-sku-search-btn:disabled { opacity: 0.6 !important; cursor: not-allowed !important; } #erp-bin-sku-row { display: flex !important; flex-wrap: wrap !important; gap: 8px !important; padding: 10px 12px !important; background: linear-gradient(180deg, #f0f4ff 0%, #fafbff 100%) !important; border-bottom: 2px solid #e8edf5 !important; } .erp-bin-badge { display: inline-flex !important; align-items: center !important; gap: 4px !important; padding: 4px 10px !important; border-radius: 6px !important; font-weight: 600 !important; font-size: 12px !important; line-height: 1.4 !important; white-space: nowrap !important; box-shadow: 0 1px 3px rgba(0,0,0,0.06) !important; transition: transform 0.15s ease, box-shadow 0.15s ease !important; } .erp-bin-badge:hover { transform: translateY(-1px) !important; box-shadow: 0 2px 6px rgba(0,0,0,0.12) !important; } .erp-bin-badge-sku { background: #e8f0fe !important; color: #1a56db !important; border: 1px solid #c5d9f7 !important; } .erp-bin-badge-qty { background: #ecfdf5 !important; color: #047857 !important; border: 1px solid #a7f3d0 !important; } .erp-bin-badge-lock { background: #fef2f2 !important; color: #b91c1c !important; border: 1px solid #fecaca !important; animation: erp-bin-pulse 2s ease-in-out infinite; } @keyframes erp-bin-pulse { 0%, 100% { box-shadow: 0 1px 3px rgba(185,28,28,0.08) !important; } 50% { box-shadow: 0 1px 8px rgba(185,28,28,0.2) !important; } } .erp-bin-badge-icon { font-size: 13px !important; flex-shrink: 0 !important; } /* ====== 🆕 v6.17 订单剩余发货时间卡片 ====== */ #erp-bin-rem-cards { display: flex !important; flex-wrap: wrap !important; gap: 5px !important; padding: 8px 12px !important; background: #fff !important; border-bottom: 1px solid #e2e8f0 !important; } #erp-bin-rem-cards.erp-rem-hidden { display: none !important; } .erp-rem-card { flex: 1 1 62px !important; min-width: 58px !important; box-sizing: border-box !important; display: flex !important; align-items: center !important; gap: 5px !important; background: #fff !important; border: 1px solid #e5e7eb !important; border-radius: 8px !important; padding: 5px 7px !important; cursor: pointer !important; transition: transform 0.18s ease, box-shadow 0.18s ease !important; } .erp-rem-card:hover { transform: translateY(-1px) !important; box-shadow: 0 3px 8px rgba(0,0,0,0.1) !important; } .erp-rem-card.erp-rem-on { font-weight: 700 !important; } .erp-rem-icon { font-size: 14px !important; line-height: 1 !important; flex-shrink: 0 !important; } .erp-rem-body { flex: 1 !important; min-width: 0 !important; line-height: 1.15 !important; } .erp-rem-n { font-size: 16px !important; font-weight: 800 !important; color: #1a1a2e !important; line-height: 1 !important; } .erp-rem-l { font-size: 10px !important; color: #6b7280 !important; margin-top: 2px !important; white-space: nowrap !important; } .erp-rem-sub { font-size: 9px !important; color: #9ca3af !important; white-space: nowrap !important; } .erp-rem-loading { font-size: 11px !important; color: #94a3b8 !important; padding: 2px 0 !important; } #erp-bin-rem-list { display: none; max-height: 108px !important; overflow-y: auto !important; padding: 5px 12px 6px !important; background: #fafbfc !important; border-bottom: 1px solid #e2e8f0 !important; } #erp-bin-rem-list::-webkit-scrollbar { width: 4px; } #erp-bin-rem-list::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 2px; } .erp-rem-list-head { font-size: 10.5px !important; color: #64748b !important; font-weight: 700 !important; padding: 1px 0 3px !important; } .erp-rem-row { display: flex !important; align-items: center !important; gap: 6px !important; font-size: 11px !important; padding: 2.5px 0 !important; border-bottom: 1px dashed #eef2f7 !important; } .erp-rem-oid { flex: 0 0 84px !important; color: #334155 !important; font-weight: 600 !important; overflow: hidden !important; text-overflow: ellipsis !important; white-space: nowrap !important; } .erp-rem-shop { flex: 1 1 auto !important; min-width: 0 !important; color: #64748b !important; overflow: hidden !important; text-overflow: ellipsis !important; white-space: nowrap !important; } .erp-rem-st { flex: 0 0 auto !important; color: #94a3b8 !important; font-size: 10px !important; white-space: nowrap !important; } .erp-rem-q { flex: 0 0 auto !important; color: #475569 !important; font-weight: 600 !important; } .erp-rem-time { flex: 0 0 auto !important; font-weight: 700 !important; white-space: nowrap !important; } .erp-rem-time.erp-rem-over, .erp-rem-time.erp-rem-today { color: #b91c1c !important; } .erp-rem-time.erp-rem-warn { color: #b45309 !important; } .erp-rem-time.erp-rem-ok { color: #047857 !important; } .erp-rem-time.erp-rem-done { color: #94a3b8 !important; font-weight: 600 !important; } .erp-rem-none { font-size: 10.5px !important; color: #94a3b8 !important; padding: 3px 0 !important; } #erp-bin-info-panel.single-mode-active #erp-bin-rem-cards { padding: 6px 12px !important; } #erp-bin-select-row { display: flex !important; flex-direction: column !important; align-items: stretch !important; gap: 0 !important; padding: 8px 12px !important; background: #f8fafc !important; border-bottom: 2px solid #e2e8f0 !important; } .erp-bin-section-label { display: flex !important; align-items: center !important; gap: 6px !important; padding: 6px 12px !important; background: #f1f5f9 !important; border-bottom: 1px solid #e2e8f0 !important; color: #475569 !important; font-size: 11px !important; font-weight: 600 !important; } #erp-bin-detail-row { padding: 0 !important; background: #fff !important; display: flex !important; flex-direction: column !important; /* 🆕 v6.24 占满 panel 剩余高度(之前没设置 flex,列表随内容撑高导致溢出被面板 hidden 切掉) */ flex: 1 1 auto !important; min-height: 0 !important; overflow: hidden !important; } /* 🆕 v6.24 其他固定子项加 flex-shrink:0,确保仓位明细能稳定拿到剩余空间 */ #erp-bin-info-panel > #erp-bin-info-header, #erp-bin-info-panel > #erp-bin-sku-search-row, #erp-bin-info-panel > #erp-bin-sku-row, #erp-bin-info-panel > #erp-bin-rem-wrap, #erp-bin-info-panel > #erp-bin-select-row, #erp-bin-info-panel > #erp-bin-single-row, #erp-bin-info-panel > #erp-bin-exclude-row, #erp-bin-info-panel > #erp-bin-section-label, #erp-bin-info-panel > #erp-bin-lock-bar-wrap { flex-shrink: 0 !important; } /* 🆕 v6.24 滚动条样式搬到真正滚动的 .erp-bin-list 上 */ .erp-bin-list::-webkit-scrollbar { width: 6px; height: 6px; } .erp-bin-list::-webkit-scrollbar-track { background: transparent; } .erp-bin-list::-webkit-scrollbar-thumb { background: #c7d2fe; border-radius: 3px; } .erp-bin-list::-webkit-scrollbar-thumb:hover { background: #818cf8; } /* Firefox */ .erp-bin-list { scrollbar-width: thin; scrollbar-color: #c7d2fe transparent; } .erp-bin-table { width: 100% !important; border-collapse: collapse !important; font-size: 12px !important; } .erp-bin-table thead th { background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%) !important; color: #475569 !important; font-weight: 700 !important; font-size: 11px !important; padding: 8px 12px !important; text-align: left !important; border-bottom: 2px solid #e2e8f0 !important; position: sticky !important; top: 0 !important; z-index: 1 !important; } .erp-bin-table tbody tr { transition: all 0.2s ease !important; } .erp-bin-table tbody tr:hover { background: #f0f4ff !important; } .erp-bin-table tbody tr:nth-child(even) { background: #fafbff !important; } .erp-bin-table td { padding: 8px 12px !important; border-bottom: 1px solid #f1f5f9 !important; vertical-align: middle !important; } .erp-bin-table .cell-bin { color: #0f766e !important; font-weight: 600 !important; } .erp-bin-table .cell-qty { color: #1e40af !important; font-weight: 700 !important; } .erp-bin-table .cell-wh { color: #64748b !important; font-size: 11px !important; } .erp-bin-empty { color: #94a3b8 !important; font-style: italic !important; padding: 12px !important; display: block !important; text-align: center !important; } .erp-bin-loading { color: #667eea !important; padding: 12px !important; display: block !important; text-align: center !important; } .erp-bin-lock-bar-wrap { margin-top: 6px; padding: 8px 12px 10px !important; background: #fff8f8 !important; border-top: 1px dashed #fecaca !important; } .erp-bin-lock-bar-label { display: flex !important; justify-content: space-between !important; align-items: center !important; margin-bottom: 4px !important; font-size: 11px !important; color: #64748b !important; } .erp-bin-lock-bar-label strong { color: #b91c1c !important; font-size: 13px !important; } .erp-bin-lock-bar-track { height: 6px !important; background: #fee2e2 !important; border-radius: 3px !important; overflow: hidden !important; } .erp-bin-lock-bar-fill { height: 100% !important; border-radius: 3px !important; transition: width 0.5s ease !important; background: linear-gradient(90deg, #f87171, #dc2626) !important; } #erp-bin-toggle-btn { position: fixed; z-index: 9997; width: 44px; height: 100px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 12px 0 0 12px; display: flex; flex-direction: column; align-items: center; justify-content: center; cursor: pointer; box-shadow: -4px 0 20px rgba(102, 126, 234, 0.4); transition: all 0.3s ease; color: #fff; user-select: none; border: 2px solid rgba(255,255,255,0.2); border-right: none; } #erp-bin-toggle-btn:hover { width: 52px; box-shadow: -6px 0 30px rgba(102, 126, 234, 0.6); transform: translateX(-4px); } #erp-bin-toggle-btn .toggle-icon { font-size: 22px; margin-bottom: 4px; } #erp-bin-toggle-btn .toggle-text { font-size: 10px; font-weight: 600; letter-spacing: 1px; writing-mode: vertical-rl; text-orientation: mixed; } #erp-bin-close { display: inline-flex !important; align-items: center !important; justify-content: center !important; width: 24px !important; height: 24px !important; border-radius: 6px !important; background: rgba(255,255,255,0.15) !important; cursor: pointer !important; opacity: 0.8 !important; transition: all 0.2s ease !important; } #erp-bin-close:hover { opacity: 1 !important; background: rgba(255,255,255,0.25) !important; } /* ====== 🆕 v6.20 折叠按钮 ====== */ #erp-bin-fold { display: inline-flex !important; align-items: center !important; justify-content: center !important; width: 24px !important; height: 24px !important; border-radius: 6px !important; background: rgba(255,255,255,0.15) !important; cursor: pointer !important; opacity: 0.8 !important; transition: all 0.2s ease !important; font-size: 13px !important; line-height: 1 !important; margin-right: 6px !important; } #erp-bin-fold:hover { opacity: 1 !important; background: rgba(255,255,255,0.25) !important; } /* ====== 🆕 v6.20 折叠态:只留标题栏 + 缩放手柄 ====== 用 !important 盖掉子元素内联 display(数据更新时 lock-bar 等会被 JS 改回 flex, 纯 JS 恢复会被覆盖,CSS !important 才稳);高度 auto 让面板缩成一条 */ #erp-bin-info-panel.folded > *:not(#erp-bin-info-header):not(#erp-bin-resize-handle) { display: none !important; } #erp-bin-info-panel.folded { height: auto !important; max-height: none !important; } #erp-bin-lock-pct-topright { font-size: 12px !important; font-weight: 700 !important; background: rgba(255,255,255,0.25) !important; padding: 2px 8px !important; border-radius: 4px !important; margin-right: 6px !important; } #erp-bin-wh-bar { display: flex !important; align-items: center !important; justify-content: space-between !important; width: 100% !important; margin-bottom: 6px !important; } .erp-bin-wh-title { font-size: 11px !important; font-weight: 600 !important; color: #475569 !important; } .erp-bin-wh-tool { font-size: 11px !important; color: #667eea !important; cursor: pointer !important; margin-left: 8px !important; } .erp-bin-wh-tool:hover { text-decoration: underline !important; } #erp-bin-wh-list { display: flex !important; flex-wrap: wrap !important; gap: 6px !important; width: 100% !important; max-height: 96px; overflow-y: auto; } #erp-bin-wh-list::-webkit-scrollbar { width: 4px; } #erp-bin-wh-list::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 2px; } .erp-bin-wh-chip { display: inline-flex !important; align-items: center !important; gap: 4px !important; padding: 3px 8px !important; border-radius: 6px !important; cursor: pointer !important; background: #fff !important; border: 1.5px solid #cbd5e1 !important; color: #334155 !important; font-size: 11px !important; font-weight: 600 !important; white-space: nowrap !important; user-select: none !important; transition: all 0.15s ease !important; } .erp-bin-wh-chip:hover { border-color: #667eea !important; background: #f5f7ff !important; } .erp-bin-wh-chip.active { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important; border-color: #667eea !important; color: #fff !important; box-shadow: 0 2px 6px rgba(102,126,234,0.35) !important; } .erp-bin-wh-chip.nodata { opacity: 0.55 !important; } .erp-bin-wh-chip input { margin: 0 !important; cursor: pointer !important; } .erp-bin-wh-nodata { font-size: 10px !important; font-weight: 400 !important; opacity: 0.9 !important; } .erp-bin-pin-hint { padding: 6px 12px !important; font-size: 11px !important; font-weight: 600 !important; color: #667eea !important; background: #f5f7ff !important; border-bottom: 1px solid #e2e8f0 !important; } #erp-bin-section-label { justify-content: space-between !important; } #erp-bin-sec-stats { display: inline-flex !important; align-items: center !important; gap: 10px !important; font-weight: 500 !important; color: #64748b !important; font-size: 11px !important; } #erp-bin-sec-stats strong { color: #1e40af !important; font-weight: 700 !important; } .erp-bin-sec-pin { color: #667eea !important; font-weight: 600 !important; } .erp-bin-table thead th, .erp-bin-table tbody td, .erp-bin-table tfoot td { padding: 9px 12px !important; } .erp-bin-table .cell-bin { color: #0f766e !important; font-weight: 700 !important; font-size: 13px !important; border-left: 3px solid transparent !important; padding-left: 9px !important; } .erp-bin-row-stock .cell-bin { border-left-color: #10b981 !important; } .erp-bin-row-empty .cell-bin { border-left-color: #e2e8f0 !important; color: #94a3b8 !important; } .erp-bin-table .cell-qty { color: #1e40af !important; font-weight: 800 !important; font-size: 14px !important; } .erp-bin-table .cell-lock { color: #b91c1c !important; font-weight: 700 !important; font-size: 13px !important; } .erp-bin-table tfoot td { background: linear-gradient(135deg, #f0f4ff 0%, #fafbff 100%) !important; border-top: 2px solid #c7d2fe !important; } .erp-bin-tfoot { display: flex !important; align-items: center !important; gap: 14px !important; font-size: 12px !important; color: #475569 !important; font-weight: 500 !important; } .erp-bin-tfoot strong { color: #1e40af !important; font-size: 13px !important; } .erp-bin-detail-empty { padding: 32px 16px !important; text-align: center !important; color: #94a3b8 !important; font-size: 13px !important; font-weight: 500 !important; display: flex !important; flex-direction: column !important; gap: 4px !important; } .erp-bin-detail-empty span { color: #cbd5e1 !important; font-size: 11px !important; font-weight: 400 !important; } /* 🆕 v6.19 「清除筛选,显示全部仓位」一键解除按钮 */ .erp-bin-unblock-btn { margin: 10px auto 0 !important; padding: 5px 12px !important; border: 1px solid #475569 !important; border-radius: 6px !important; background: #1e293b !important; color: #e2e8f0 !important; font-size: 11px !important; cursor: pointer !important; } .erp-bin-unblock-btn:hover { background: #334155 !important; } #erp-bin-info-header { padding: 5px 10px !important; font-size: 12px !important; } #erp-bin-info-header span:first-child { font-size: 12px !important; } #erp-bin-close { width: 22px !important; height: 22px !important; } #erp-bin-fold { width: 22px !important; height: 22px !important; font-size: 12px !important; } #erp-bin-sku-search-row { padding: 4px 10px !important; } #erp-bin-sku-search-input { padding: 3px 8px !important; font-size: 12px !important; } #erp-bin-sku-search-btn { padding: 3px 10px !important; font-size: 11px !important; } #erp-bin-sku-row { padding: 4px 10px !important; gap: 6px !important; } .erp-bin-badge { padding: 3px 8px !important; font-size: 11px !important; } .erp-bin-badge-icon { font-size: 12px !important; } #erp-bin-select-row { padding: 4px 10px !important; } #erp-bin-wh-bar { margin-bottom: 3px !important; } .erp-bin-wh-title { font-size: 10px !important; } .erp-bin-wh-tool { font-size: 10px !important; } .erp-bin-wh-chip { padding: 3px 7px !important; font-size: 10px !important; } .erp-bin-section-label { padding: 3px 10px !important; font-size: 10px !important; } #erp-bin-sec-stats { font-size: 10px !important; gap: 8px !important; } #erp-bin-sec-stats strong { font-size: 11px !important; } .erp-bin-table thead th { padding: 10px 12px !important; font-size: 12px !important; } .erp-bin-table tbody td { padding: 11px 13px !important; } .erp-bin-table tfoot td { padding: 10px 13px !important; } .erp-bin-table .cell-bin { font-size: 14px !important; padding-left: 11px !important; } .erp-bin-table .cell-qty { font-size: 16px !important; } .erp-bin-table .cell-lock { font-size: 14px !important; } .erp-bin-tfoot { font-size: 12px !important; gap: 16px !important; } .erp-bin-tfoot strong { font-size: 14px !important; } .erp-bin-list { flex: 1 1 auto !important; overflow-y: auto !important; /* 🆕 v6.24 父容器 #erp-bin-detail-row 已限定高度,子层 max-height:100% 才能触发滚动 */ max-height: 100% !important; display: grid !important; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)) !important; gap: 12px !important; padding: 14px !important; background: linear-gradient(180deg, #fafbff 0%, #f5f3ff 100%) !important; align-content: start !important; min-height: 0 !important; /* 🆕 v6.24 启用原生平滑滚动(跨平台) */ -webkit-overflow-scrolling: touch; scroll-behavior: smooth; } .erp-bin-row { display: flex !important; flex-direction: column !important; gap: 8px !important; padding: 14px 16px !important; position: relative !important; overflow: hidden !important; background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%) !important; border: 1px solid rgba(99, 102, 241, 0.12) !important; border-left: 4px solid #6366f1 !important; border-radius: 12px !important; box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04), 0 2px 4px rgba(99, 102, 241, 0.06) !important; min-height: 118px !important; transition: all 0.2s ease !important; } .erp-bin-row::after { content: '' !important; position: absolute !important; top: 0 !important; right: 0 !important; width: 50px !important; height: 100% !important; pointer-events: none !important; background: linear-gradient(135deg, transparent 40%, rgba(99, 102, 241, 0.08) 100%) !important; } .erp-bin-row.empty { background: linear-gradient(135deg, #fafbfc 0%, #f1f5f9 100%) !important; border-color: #e2e8f0 !important; border-left-color: #cbd5e1 !important; box-shadow: 0 1px 2px rgba(15, 23, 42, 0.03) !important; } .erp-bin-row.empty::after { background: linear-gradient(135deg, transparent 40%, rgba(148, 163, 184, 0.12) 100%) !important; } .row-head { display: flex !important; align-items: center !important; gap: 6px !important; position: relative !important; z-index: 1 !important; overflow: hidden !important; } .row-bin { display: flex !important; align-items: center !important; gap: 7px !important; flex: 0 1 auto !important; min-width: 0 !important; font-size: 16px !important; font-weight: 700 !important; color: #1e1b4b !important; letter-spacing: 0.2px !important; overflow: hidden !important; text-overflow: ellipsis !important; white-space: nowrap !important; padding-right: 150px !important; } .row-bin::before { content: '' !important; flex-shrink: 0 !important; width: 8px !important; height: 8px !important; border-radius: 50% !important; background: linear-gradient(135deg, #34d399 0%, #10b981 100%) !important; box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.18), 0 1px 2px rgba(16, 185, 129, 0.4) !important; } .erp-bin-row.empty .row-bin { color: #94a3b8 !important; } .erp-bin-row.empty .row-bin::before { background: #cbd5e1 !important; box-shadow: 0 0 0 3px rgba(203, 213, 225, 0.4) !important; } .row-mid { display: flex !important; align-items: baseline !important; gap: 4px !important; position: relative !important; z-index: 1 !important; } .row-qty-num { font-size: 30px !important; font-weight: 800 !important; color: #4f46e5 !important; line-height: 1 !important; letter-spacing: -0.5px !important; } .row-unit { font-size: 13px !important; color: #94a3b8 !important; font-weight: 600 !important; line-height: 1 !important; letter-spacing: 0 !important; } .erp-bin-row.empty .row-qty-num { color: #cbd5e1 !important; } .row-foot { display: flex !important; flex-wrap: wrap !important; gap: 4px !important; align-items: center !important; margin-top: auto !important; position: relative !important; z-index: 1 !important; } .row-wh { font-size: 12px !important; color: #4338ca !important; font-weight: 600 !important; padding: 3px 9px !important; background: linear-gradient(135deg, rgba(99, 102, 241, 0.1) 0%, rgba(139, 92, 246, 0.08) 100%) !important; border: 1px solid rgba(99, 102, 241, 0.15) !important; border-radius: 6px !important; letter-spacing: 0.2px !important; max-width: 100% !important; overflow: hidden !important; text-overflow: ellipsis !important; white-space: nowrap !important; } .row-lock { font-size: 11px !important; color: #be123c !important; font-weight: 600 !important; background: linear-gradient(135deg, #fef2f2 0%, #ffe4e6 100%) !important; padding: 3px 8px !important; border-radius: 5px !important; border: 1px solid rgba(244, 63, 94, 0.18) !important; box-shadow: 0 1px 2px rgba(244, 63, 94, 0.08) !important; white-space: nowrap !important; } .row-lock-loading { opacity: 0.55 !important; animation: row-lock-pulse 1.4s ease-in-out infinite !important; } .row-lock-dots { display: inline-block !important; letter-spacing: 1px !important; } .row-lock-empty { opacity: 0.5 !important; color: #94a3b8 !important; background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%) !important; border-color: #e2e8f0 !important; } @keyframes row-lock-pulse { 0%, 100% { opacity: 0.35; } 50% { opacity: 0.75; } } .erp-bin-detail-empty { flex: 1 1 auto !important; display: flex !important; flex-direction: column !important; align-items: center !important; justify-content: center !important; padding: 40px 16px !important; color: #94a3b8 !important; font-size: 13px !important; font-weight: 600 !important; gap: 8px !important; background: linear-gradient(180deg, #fafbff 0%, #f5f3ff 100%) !important; } .erp-bin-detail-empty span { color: #cbd5e1 !important; font-size: 11px !important; font-weight: 500 !important; } /* ====== 面板缩放 handle ====== */ #erp-bin-resize-handle { position: absolute !important; right: 0 !important; bottom: 0 !important; width: 18px !important; height: 18px !important; cursor: nwse-resize !important; z-index: 20 !important; overflow: hidden !important; touch-action: none !important; user-select: none !important; } #erp-bin-resize-handle::before { content: '' !important; position: absolute !important; right: 3px !important; bottom: 3px !important; width: 12px !important; height: 12px !important; border-right: 2px solid #94a3b8 !important; border-bottom: 2px solid #94a3b8 !important; transform: rotate(-45deg) !important; transform-origin: right bottom !important; } #erp-bin-resize-handle:hover::before { border-right-color: #6366f1 !important; border-bottom-color: #6366f1 !important; } /* ====== 单选工具条 ====== */ #erp-bin-single-row { display: flex !important; align-items: center !important; gap: 8px !important; padding: 6px 12px !important; background: linear-gradient(135deg, #fff7ed 0%, #fed7aa 100%) !important; border-bottom: 1px solid #fb923c !important; font-size: 11px !important; } #erp-bin-single-row.active { background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%) !important; border-bottom-color: #b45309 !important; } .erp-bin-single-label { font-weight: 700 !important; color: #9a3412 !important; flex-shrink: 0 !important; } #erp-bin-single-row.active .erp-bin-single-label { color: #fff !important; } #erp-bin-single-current { flex: 1 1 auto !important; color: #9a3412 !important; font-weight: 600 !important; overflow: hidden !important; text-overflow: ellipsis !important; white-space: nowrap !important; } #erp-bin-single-row.active #erp-bin-single-current { color: #fff !important; } #erp-bin-single-clear { padding: 3px 10px !important; font-size: 11px !important; background: rgba(255,255,255,0.85) !important; border: 1px solid #c2410c !important; color: #9a3412 !important; border-radius: 4px !important; cursor: pointer !important; font-weight: 600 !important; flex-shrink: 0 !important; transition: all 0.15s ease !important; } #erp-bin-single-clear:hover { background: #fff !important; color: #7c2d12 !important; transform: translateY(-1px) !important; } /* ====== 卡片单选按钮 ====== */ .row-single-btn { position: absolute !important; top: 8px !important; right: 8px !important; background: rgba(99, 102, 241, 0.08) !important; border: 1px solid rgba(99, 102, 241, 0.3) !important; color: #6366f1 !important; padding: 3px 8px !important; border-radius: 6px !important; cursor: pointer !important; font-size: 10px !important; font-weight: 700 !important; z-index: 5 !important; transition: all 0.15s ease !important; line-height: 1.2 !important; white-space: nowrap !important; } .row-single-btn:hover { background: #6366f1 !important; color: #fff !important; border-color: #6366f1 !important; transform: translateY(-1px) !important; } .erp-bin-row.single-active { border-color: #f59e0b !important; border-left-color: #f59e0b !important; box-shadow: 0 0 0 2px rgba(245, 158, 11, 0.25), 0 4px 10px rgba(245, 158, 11, 0.18) !important; background: linear-gradient(135deg, #fffbeb 0%, #fef3c7 100%) !important; } .erp-bin-row.single-active::after { background: linear-gradient(135deg, transparent 40%, rgba(245, 158, 11, 0.22) 100%) !important; } .erp-bin-row.single-active .row-bin::before { background: linear-gradient(135deg, #fbbf24 0%, #f59e0b 100%) !important; box-shadow: 0 0 0 3px rgba(245, 158, 11, 0.25), 0 1px 2px rgba(245, 158, 11, 0.4) !important; } .erp-bin-row.single-active .row-single-btn { background: #f59e0b !important; color: #fff !important; border-color: #b45309 !important; } /* ====== 排除计算:卡片按钮 / 样式 ====== */ .row-exclude-btn { position: absolute !important; top: 8px !important; right: 84px !important; background: rgba(100, 116, 139, 0.08) !important; border: 1px solid rgba(100, 116, 139, 0.3) !important; color: #64748b !important; padding: 3px 8px !important; border-radius: 6px !important; cursor: pointer !important; font-size: 10px !important; font-weight: 700 !important; z-index: 5 !important; transition: all 0.15s ease !important; line-height: 1.2 !important; white-space: nowrap !important; } .row-exclude-btn:hover { background: #64748b !important; color: #fff !important; border-color: #475569 !important; } .erp-bin-row.excluded { opacity: 0.5 !important; border-color: #cbd5e1 !important; border-left-color: #cbd5e1 !important; background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%) !important; } .erp-bin-row.excluded .row-bin { text-decoration: line-through !important; color: #94a3b8 !important; } .erp-bin-row.excluded .row-bin::before { background: linear-gradient(135deg, #cbd5e1 0%, #94a3b8 100%) !important; box-shadow: none !important; } .erp-bin-row.excluded .row-exclude-btn { background: #64748b !important; color: #fff !important; border-color: #475569 !important; } /* ====== 排除工具条 ====== */ #erp-bin-exclude-row { display: none !important; align-items: center !important; gap: 8px !important; padding: 6px 12px !important; background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%) !important; border-bottom: 1px solid #94a3b8 !important; font-size: 11px !important; } #erp-bin-exclude-row.has-exclude { display: flex !important; } .erp-bin-exclude-label { font-weight: 700 !important; color: #475569 !important; flex-shrink: 0 !important; } #erp-bin-exclude-current { flex: 1 1 auto !important; color: #475569 !important; font-weight: 600 !important; overflow: hidden !important; text-overflow: ellipsis !important; white-space: nowrap !important; } #erp-bin-exclude-clear { padding: 3px 10px !important; font-size: 11px !important; background: rgba(255,255,255,0.85) !important; border: 1px solid #475569 !important; color: #334155 !important; border-radius: 4px !important; cursor: pointer !important; font-weight: 600 !important; flex-shrink: 0 !important; transition: all 0.15s ease !important; } #erp-bin-exclude-clear:hover { background: #fff !important; transform: translateY(-1px) !important; } /* ====== 单选激活时的精简视图:只显示选中的仓位 ====== */ #erp-bin-info-panel.single-mode-active #erp-bin-sku-row, #erp-bin-info-panel.single-mode-active #erp-bin-select-row { display: none !important; } #erp-bin-info-panel.single-mode-active { box-shadow: 0 8px 28px rgba(245, 158, 11, 0.28), 0 4px 12px rgba(0,0,0,0.12) !important; border-color: #fbbf24 !important; } /* 精简视图下,卡片放大更突出 */ #erp-bin-info-panel.single-mode-active .erp-bin-list { grid-template-columns: 1fr !important; padding: 18px !important; gap: 14px !important; } #erp-bin-info-panel.single-mode-active .erp-bin-row { padding: 16px 18px !important; min-height: 88px !important; } #erp-bin-info-panel.single-mode-active .row-bin { font-size: 18px !important; } #erp-bin-info-panel.single-mode-active .row-qty-num { font-size: 26px !important; } /* 精简视图下,底部进度条样式微调:更紧凑、贴底 */ #erp-bin-info-panel.single-mode-active #erp-bin-lock-bar-wrap { margin: 0 14px 12px 14px !important; } /* ====== 面板放大档位:拉大面板时仓位明细同步放大 ====== */ #erp-bin-info-panel.size-lg .erp-bin-list { grid-template-columns: repeat(auto-fill, minmax(360px, 1fr)) !important; gap: 14px !important; padding: 18px !important; } #erp-bin-info-panel.size-lg .erp-bin-row { padding: 18px 20px !important; min-height: 150px !important; gap: 10px !important; border-left-width: 5px !important; } #erp-bin-info-panel.size-lg .row-bin { font-size: 20px !important; padding-right: 170px !important; } #erp-bin-info-panel.size-lg .row-bin::before { width: 10px !important; height: 10px !important; } #erp-bin-info-panel.size-lg .row-qty-num { font-size: 40px !important; } #erp-bin-info-panel.size-lg .row-unit { font-size: 15px !important; } #erp-bin-info-panel.size-lg .row-wh { font-size: 13px !important; padding: 4px 11px !important; } #erp-bin-info-panel.size-lg .row-lock { font-size: 12px !important; padding: 4px 10px !important; } #erp-bin-info-panel.size-lg .row-single-btn, #erp-bin-info-panel.size-lg .row-exclude-btn { font-size: 12px !important; padding: 5px 11px !important; } #erp-bin-info-panel.size-lg .row-exclude-btn { right: 10px !important; top: 44px !important; } #erp-bin-info-panel.size-lg .row-single-btn { right: 10px !important; top: 10px !important; } #erp-bin-info-panel.size-xl .erp-bin-list { grid-template-columns: repeat(auto-fill, minmax(420px, 1fr)) !important; gap: 16px !important; padding: 22px !important; } #erp-bin-info-panel.size-xl .erp-bin-row { padding: 22px 26px !important; min-height: 180px !important; gap: 12px !important; } #erp-bin-info-panel.size-xl .row-bin { font-size: 24px !important; } #erp-bin-info-panel.size-xl .row-qty-num { font-size: 52px !important; } #erp-bin-info-panel.size-xl .row-unit { font-size: 17px !important; } #erp-bin-info-panel.size-xl .row-wh { font-size: 14px !important; padding: 5px 13px !important; } #erp-bin-info-panel.size-xl .row-lock { font-size: 13px !important; padding: 5px 12px !important; } #erp-bin-info-panel.size-xl .row-single-btn, #erp-bin-info-panel.size-xl .row-exclude-btn { font-size: 13px !important; padding: 6px 13px !important; } /* 精简视图(单选锁定)始终单列大卡:置于放大档位之后以覆盖其多列设置 */ #erp-bin-info-panel.single-mode-active .erp-bin-list { grid-template-columns: 1fr !important; } @media screen and (max-width: 768px) { #erp-bin-info-panel { min-width: calc(100vw - 20px) !important; max-width: calc(100vw - 20px) !important; left: 10px !important; right: 10px !important; border-radius: 12px !important; font-size: 14px !important; } #erp-bin-toggle-btn { width: 52px !important; height: 120px !important; } #erp-bin-resize-handle { display: none !important; } } /* ====== 🆕 v6.21 每张仓位卡片:单独折叠 + 单独拉大小 ====== */ /* 标题行可点击折叠(排除/单选按钮为绝对定位、不在标题行内,不冲突); 预留右侧空间从 row-bin 挪到 row-head,给折叠摘要和箭头腾位 */ #erp-bin-info-panel .row-head { cursor: pointer !important; padding-right: 150px !important; } #erp-bin-info-panel .row-bin { padding-right: 8px !important; } /* 折叠态摘要(数量/占有)与 ▾/▸ 箭头 */ .row-fold-summary { display: none !important; margin-left: auto !important; font-size: 12px !important; font-weight: 700 !important; color: #4f46e5 !important; white-space: nowrap !important; } #erp-bin-info-panel.size-lg .row-fold-summary { font-size: 14px !important; } #erp-bin-info-panel.size-xl .row-fold-summary { font-size: 16px !important; } .row-fold-ind { flex-shrink: 0 !important; margin-left: 8px !important; font-size: 11px !important; color: #94a3b8 !important; } /* 折叠:隐藏数量/仓库明细,高度 auto,不拉伸占满网格行 */ #erp-bin-info-panel .erp-bin-row.folded { min-height: auto !important; height: auto !important; align-self: start !important; padding: 8px 16px !important; gap: 0 !important; } #erp-bin-info-panel.size-lg .erp-bin-row.folded { padding: 10px 18px !important; } #erp-bin-info-panel.size-xl .erp-bin-row.folded { padding: 12px 22px !important; } #erp-bin-info-panel .erp-bin-row.folded .row-mid, #erp-bin-info-panel .erp-bin-row.folded .row-foot { display: none !important; } #erp-bin-info-panel .erp-bin-row.folded .row-fold-summary { display: inline !important; } /* 每卡片右下角缩放手柄 */ .row-resize-handle { position: absolute !important; right: 0 !important; bottom: 0 !important; width: 16px !important; height: 16px !important; cursor: nwse-resize !important; z-index: 6 !important; border-bottom-right-radius: 12px !important; background: linear-gradient(135deg, transparent 50%, rgba(99, 102, 241, 0.35) 50%) !important; } .row-resize-handle:hover { background: linear-gradient(135deg, transparent 50%, rgba(99, 102, 241, 0.8) 50%) !important; } #erp-bin-info-panel.size-lg .row-resize-handle, #erp-bin-info-panel.size-xl .row-resize-handle { width: 20px !important; height: 20px !important; } /* 拖动缩放期间关闭过渡动画,避免跟手延迟 */ .erp-bin-row.resizing { transition: none !important; } /* ====== 🆕 v6.22 区块折叠箭头 + 一键折叠全部卡片 ====== */ .erp-sec-fold { cursor: pointer !important; flex-shrink: 0 !important; font-size: 12px !important; color: #94a3b8 !important; padding: 0 4px !important; user-select: none !important; line-height: 1 !important; transition: color 0.15s ease !important; } .erp-sec-fold:hover { color: #4f46e5 !important; } /* 商品信息徽章行(编码/数量/占有/供应商) */ #erp-bin-sku-row { align-items: center !important; } #erp-bin-sku-row.sec-folded .erp-bin-badge { display: none !important; } /* 订单剩余发货时间区块(折叠行 + 卡片 + 清单) */ #erp-bin-rem-wrap { display: block !important; } #erp-bin-rem-fold-row { display: flex !important; align-items: center !important; gap: 6px !important; padding: 8px 14px 0 14px !important; font-size: 12px !important; font-weight: 600 !important; color: #475569 !important; } #erp-bin-rem-fold-row.erp-rem-hidden { display: none !important; } #erp-bin-rem-wrap.sec-folded > *:not(#erp-bin-rem-fold-row) { display: none !important; } /* 固定显示仓位区块 */ #erp-bin-wh-bar { align-items: center !important; } #erp-bin-select-row.sec-folded #erp-bin-wh-list { display: none !important; } /* 一键折叠/展开全部仓位卡片按钮 */ #erp-bin-fold-all { cursor: pointer !important; user-select: none !important; font-size: 11px !important; font-weight: 600 !important; color: #4f46e5 !important; background: rgba(99, 102, 241, 0.08) !important; border: 1px solid rgba(99, 102, 241, 0.35) !important; border-radius: 6px !important; padding: 2px 8px !important; margin-left: 8px !important; white-space: nowrap !important; transition: all 0.15s ease !important; } #erp-bin-fold-all:hover { background: #6366f1 !important; color: #fff !important; } /* ====== 🆕 v6.23 关闭/开启自动显示按钮 + 右侧📦开关的「关」徽章 ====== */ #erp-bin-autoshow { cursor: pointer !important; user-select: none !important; font-size: 12px !important; padding: 0 3px !important; color: #94a3b8 !important; transition: color 0.15s ease !important; border-radius: 4px !important; } #erp-bin-autoshow:hover { color: #ef4444 !important; } #erp-bin-autoshow.off { color: #ef4444 !important; background: rgba(239, 68, 68, 0.08) !important; } #erp-bin-toggle-btn .toggle-auto-off-badge { display: inline-block !important; margin-left: 4px !important; font-size: 10px !important; font-weight: 700 !important; background: #ef4444 !important; color: #fff !important; border-radius: 8px !important; padding: 0 5px !important; line-height: 14px !important; cursor: pointer !important; } `; document.head.appendChild(style); } /*********************** * UI 组件 ***********************/ const UI = { createToggleBtn() { if (document.getElementById('erp-bin-toggle-btn')) return; const savedTogglePos = pStorage.get('erpBinTogglePos', { top: '150px', right: '0px' }); const isClosed = pStorage.get('erpBinInfoClosed', false); // 🆕 v6.23 关闭自动显示(默认开) const autoShow = pStorage.get('erpBinAutoShow', true); const toggleBtn = createEl('div', { id: 'erp-bin-toggle-btn' }, ` position: fixed; top: ${savedTogglePos.top}; right: ${savedTogglePos.right}; `); toggleBtn.innerHTML = ` 📦 仓位 `; toggleBtn.title = '点击展开仓位信息栏'; document.body.appendChild(toggleBtn); // 🆕 v6.23 「关」徽章点击:重新启用自动显示 const badge = toggleBtn.querySelector('#erp-bin-toggle-autoshow-badge'); if (badge) { badge.addEventListener('click', (e) => { e.stopPropagation(); pStorage.set('erpBinAutoShow', true); pStorage.set('erpBinInfoClosed', false); this.showPanel(); this.refreshToggleAutoShowBadge(); const btn = document.getElementById('erp-bin-autoshow'); if (btn) btn.click(); // 触发 applyAutoShowBtn 重绘图标/状态 }); } if (!isClosed && autoShow) { toggleBtn.style.display = 'none'; } // 拖拽功能 let isDragging = false, startY, startTop, hasMoved = false; const handleStart = (e) => { isDragging = true; hasMoved = false; const clientY = e.touches ? e.touches[0].clientY : e.clientY; startY = clientY; startTop = parseInt(toggleBtn.style.top); e.preventDefault(); }; const handleMove = (e) => { if (!isDragging) return; hasMoved = true; const clientY = e.touches ? e.touches[0].clientY : e.clientY; const newTop = Math.max(0, Math.min(window.innerHeight - (isMobile ? 120 : 100), startTop + (clientY - startY))); toggleBtn.style.top = newTop + 'px'; }; const handleEnd = () => { if (!isDragging) return; isDragging = false; if (!hasMoved) { this.showPanel(); return; } pStorage.set('erpBinTogglePos', { top: toggleBtn.style.top, right: toggleBtn.style.right }); }; toggleBtn.addEventListener('mousedown', handleStart); document.addEventListener('mousemove', handleMove); document.addEventListener('mouseup', handleEnd); toggleBtn.addEventListener('touchstart', handleStart, { passive: false }); document.addEventListener('touchmove', handleMove, { passive: false }); document.addEventListener('touchend', handleEnd); // 🆕 v6.23 初始根据 erpBinAutoShow 决定右侧📦是否显示「关」徽章 this.refreshToggleAutoShowBadge(); return toggleBtn; }, showPanel() { const panel = document.getElementById('erp-bin-info-panel'); const toggleBtn = document.getElementById('erp-bin-toggle-btn'); if (panel) { panel.classList.remove('hiding'); panel.style.display = ''; } if (toggleBtn) { toggleBtn.style.display = 'none'; } pStorage.set('erpBinInfoClosed', false); this.refreshToggleAutoShowBadge(); }, // 🆕 v6.23 根据 erpBinAutoShow 状态刷新右侧📦按钮上的「关」徽章 refreshToggleAutoShowBadge() { const badge = document.getElementById('erp-bin-toggle-autoshow-badge'); const toggleBtn = document.getElementById('erp-bin-toggle-btn'); if (!badge) return; const autoShow = pStorage.get('erpBinAutoShow', true); badge.style.display = autoShow ? 'none' : ''; if (toggleBtn) toggleBtn.title = autoShow ? '点击展开仓位信息栏' : '当前页面已关闭自动显示 — 点这里重新启用'; }, hidePanel() { const panel = document.getElementById('erp-bin-info-panel'); const toggleBtn = document.getElementById('erp-bin-toggle-btn'); if (panel) { panel.classList.add('hiding'); setTimeout(() => { panel.style.display = 'none'; }, 300); } if (toggleBtn) { toggleBtn.style.display = 'flex'; } pStorage.set('erpBinInfoClosed', true); this.refreshToggleAutoShowBadge(); }, createInfoBar() { if (document.getElementById('erp-bin-info-panel')) return; injectStyles(); this.createToggleBtn(); const savedPos = pStorage.get('erpBinInfoPos', { top: '80px', left: 'auto', right: '10px' }); const savedSize = pStorage.get('erpBinInfoSize', { width: '360px', height: '560px' }); const isClosed = pStorage.get('erpBinInfoClosed', false); // 🆕 v6.23 关闭自动显示(默认开):false 时面板本页面不再自动弹出 const autoShow = pStorage.get('erpBinAutoShow', true); const startHidden = !autoShow || isClosed; // 读取「记忆的固定仓位」勾选 selectedWhIds = new Set(pStorage.get('erpBinSelectedWhIds', [])); // 读取「记忆的单仓位锁定」(兼容旧版残留:旧值是纯 wh_id,无 B:/W: 前缀) const _rawSingleWhId = pStorage.get('erpBinSingleWhId', null); if (_rawSingleWhId && typeof _rawSingleWhId === 'string' && _rawSingleWhId.indexOf(':') === -1) { singleWhId = 'W:' + _rawSingleWhId; // 旧值默认按仓库 ID 处理 } else { singleWhId = _rawSingleWhId; } singleMode = pStorage.get('erpBinSingleMode', false) === true; // 🆕 读取「记忆的计算排除清单」(排除的仓库/仓位不计入占有计算) excludedKeys = new Set(pStorage.get('erpBinExcludedKeys', [])); // 视口尺寸保护 const maxW = Math.max(280, window.innerWidth - 40); const maxH = Math.max(400, window.innerHeight - 40); const safeW = Math.min(parseInt(savedSize.width) || 360, maxW); const safeH = Math.min(parseInt(savedSize.height) || 560, maxH); const panelStyle = isMobile ? `position: fixed; top: ${savedPos.top}; left: 10px; right: 10px; width: auto;` : `position: fixed; top: ${savedPos.top}; ${savedPos.left !== 'auto' ? 'left: ' + savedPos.left : 'right: ' + savedPos.right}; width: ${safeW}px; height: ${safeH}px; min-width: 280px; min-height: 360px; max-width: ${maxW}px; max-height: ${maxH}px;`; const panel = createEl('div', { id: 'erp-bin-info-panel' }, ` ${panelStyle} display: flex; flex-direction: column; background: #fff; border: 1px solid #e2e8f0; border-radius: 10px; box-shadow: 0 4px 16px rgba(0,0,0,0.1); z-index: 9998; cursor: move; user-select: none; overflow: hidden; resize: none; ${startHidden ? 'display: none;' : ''} `); // 缩放手柄(绝对定位在右下角) const resizeHandle = createEl('div', { id: 'erp-bin-resize-handle', title: '拖拽调整面板大小' }); panel.appendChild(resizeHandle); const header = createEl('div', { id: 'erp-bin-info-header' }); header.innerHTML = '📦 仓位占有信息'; const skuSearchRow = createEl('div', { id: 'erp-bin-sku-search-row' }); skuSearchRow.innerHTML = ` `; const skuRow = createEl('div', { id: 'erp-bin-sku-row' }); skuRow.innerHTML = ` 🔑 编码: - 📊 数量: - `; // 🆕 v6.17 订单剩余发货时间卡片区 + 订单清单 // 🆕 v6.22 用 remWrap 包住「折叠行 + 卡片区 + 清单」,区块整体可折叠并记忆 const remFoldRow = createEl('div', { id: 'erp-bin-rem-fold-row', className: 'erp-rem-hidden' }); remFoldRow.innerHTML = '' + '⏱ 订单剩余发货时间(点卡片看明细)'; const remCards = createEl('div', { id: 'erp-bin-rem-cards', className: 'erp-rem-hidden' }); remCards.innerHTML = '
搜索商品编码后显示订单剩余发货时间
'; const remList = createEl('div', { id: 'erp-bin-rem-list' }); const remWrap = createEl('div', { id: 'erp-bin-rem-wrap' }); remWrap.appendChild(remFoldRow); remWrap.appendChild(remCards); remWrap.appendChild(remList); const selectRow = createEl('div', { id: 'erp-bin-select-row' }); selectRow.innerHTML = `
📌 固定显示仓位(可多选,自动记忆) 清空
搜索后显示仓位列表
`; // 🆕 单仓位记忆锁定工具条 const singleRow = createEl('div', { id: 'erp-bin-single-row' }); singleRow.innerHTML = ` 📍 单选锁定: 未锁定(显示全部) `; // 🆕 计算排除工具条(排除的仓库/仓位不计入占有计算) const excludeRow = createEl('div', { id: 'erp-bin-exclude-row' }); excludeRow.innerHTML = ` ⊘ 计算排除: 未排除 `; const sectionLabel = createEl('div', { className: 'erp-bin-section-label', id: 'erp-bin-section-label' }); // 🆕 v6.22 一键折叠/展开全部仓位卡片 sectionLabel.innerHTML = '仓位明细⇕ 全部折叠'; const binRow = createEl('div', { id: 'erp-bin-detail-row' }); binRow.innerHTML = '输入商品编码搜索仓位...'; const lockBarWrap = createEl('div', { id: 'erp-bin-lock-bar-wrap', className: 'erp-bin-lock-bar-wrap' }); lockBarWrap.style.display = 'none'; lockBarWrap.innerHTML = `
占有占比 0%
`; panel.appendChild(header); panel.appendChild(skuSearchRow); panel.appendChild(skuRow); // 🆕 v6.22 remWrap = 折叠行 + 卡片区 + 订单清单(整体可折叠) panel.appendChild(remWrap); panel.appendChild(selectRow); panel.appendChild(singleRow); panel.appendChild(excludeRow); panel.appendChild(sectionLabel); panel.appendChild(binRow); panel.appendChild(lockBarWrap); document.body.appendChild(panel); state.ui = { panel, header, binRow }; // 事件绑定 const closeBtn = document.getElementById('erp-bin-close'); const skuSearchInput = document.getElementById('erp-bin-sku-search-input'); const skuSearchBtn = document.getElementById('erp-bin-sku-search-btn'); async function doSkuSearch() { const sku_id = skuSearchInput.value.trim(); if (!sku_id) { skuSearchInput.focus(); return; } skuSearchBtn.disabled = true; skuSearchBtn.textContent = '⏳ 搜索中'; binRow.innerHTML = '正在查询商品编码: ' + sku_id + '...'; try { const lockInfo = lockData[sku_id]; const lockQty = lockInfo ? lockInfo.order_lock : 0; // 🆕 v6.17 并行加载订单剩余发货时间卡片(不阻塞仓位查询) loadRemCards(sku_id).catch(() => {}); const { items: binItems, whInfo, ok: binOk, reason: binReason, hint: binHint } = await queryBinStock(sku_id); lastBinHint = binHint || ''; let totalQty = 0; for (const item of binItems) { totalQty += parseFloat(item.qty) || 0; } UI.updateInfoBar(sku_id, totalQty > 0 ? totalQty : '-', lockQty, binItems, whInfo); // 🆕 v6.19 查询失败不再静默显示“无数据”,明确提示原因 if (!binOk) { binRow.innerHTML = '⚠️ 仓位接口未返回数据(' + (binReason || '未知原因') + ')
刚切换过仓库/货主或重新登录时,按 F5 刷新页面后再搜一次
'; } } catch (error) { console.error('[仓位信息栏] 搜索失败:', error); binRow.innerHTML = '❌ 查询失败,请重试'; } finally { skuSearchBtn.disabled = false; skuSearchBtn.textContent = '🔍 搜索'; } } skuSearchBtn.addEventListener('click', doSkuSearch); skuSearchInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') doSkuSearch(); }); // 固定仓位:勾选/取消勾选(会自动记忆,跨商品保留) const whList = document.getElementById('erp-bin-wh-list'); if (whList) { whList.addEventListener('change', (e) => { const el = e.target; if (!el || !el.classList || !el.classList.contains('erp-bin-wh-check')) return; const id = el.value; if (el.checked) selectedWhIds.add(id); else selectedWhIds.delete(id); pStorage.set('erpBinSelectedWhIds', [...selectedWhIds]); const chip = el.closest('.erp-bin-wh-chip'); if (chip) chip.classList.toggle('active', el.checked); UI.updateSelectedBinDetail(); UI.updateWhCount(); }); } const whClear = document.getElementById('erp-bin-wh-clear'); if (whClear) { whClear.addEventListener('click', () => { selectedWhIds.clear(); pStorage.set('erpBinSelectedWhIds', []); document.querySelectorAll('.erp-bin-wh-check').forEach(c => { c.checked = false; const chip = c.closest('.erp-bin-wh-chip'); if (chip) chip.classList.remove('active'); }); UI.updateSelectedBinDetail(); UI.updateWhCount(); }); } // 🆕 单选锁定:清除按钮(恢复显示全部) const singleClear = document.getElementById('erp-bin-single-clear'); if (singleClear) { singleClear.addEventListener('click', () => { singleMode = false; singleWhId = null; pStorage.set('erpBinSingleMode', false); pStorage.set('erpBinSingleWhId', null); UI.updateSingleToolbar(); UI.updateSelectedBinDetail(); }); } // 🆕 计算排除:清除按钮(恢复全量计算) const excludeClear = document.getElementById('erp-bin-exclude-clear'); if (excludeClear) { excludeClear.addEventListener('click', () => { excludedKeys.clear(); pStorage.set('erpBinExcludedKeys', []); UI.updateExcludeToolbar(); UI.updateSelectedBinDetail(); if (currentSkuId && typeof currentTotalQty !== 'undefined') { // 重新计算面板顶部的占有比例(用缓存数据) const lockQty = lockData[currentSkuId]?.order_lock || 0; UI.updateInfoBar(currentSkuId, currentTotalQty, lockQty, currentBinItems, currentWhInfo); } }); } // 拖拽功能 let isDragging = false, startX, startY, startTop, startLeft, hasMoved = false; const handleDragStart = (e) => { if (e.target.id === 'erp-bin-close' || e.target.id === 'erp-bin-fold' || e.target.id === 'erp-bin-sku-search-input' || e.target.id === 'erp-bin-sku-search-btn') return; isDragging = true; hasMoved = false; const clientX = e.touches ? e.touches[0].clientX : e.clientX; const clientY = e.touches ? e.touches[0].clientY : e.clientY; startX = clientX; startY = clientY; const rect = panel.getBoundingClientRect(); startTop = rect.top; startLeft = rect.left; panel.style.right = 'auto'; panel.style.left = rect.left + 'px'; panel.style.top = rect.top + 'px'; e.preventDefault(); }; const handleDragMove = (e) => { if (!isDragging) return; hasMoved = true; const clientX = e.touches ? e.touches[0].clientX : e.clientX; const clientY = e.touches ? e.touches[0].clientY : e.clientY; const panelWidth = panel.offsetWidth; const panelHeight = panel.offsetHeight; const maxTop = window.innerHeight - panelHeight; const maxLeft = window.innerWidth - panelWidth; let newTop = Math.max(0, Math.min(maxTop, startTop + (clientY - startY))); let newLeft = Math.max(0, Math.min(maxLeft, startLeft + (clientX - startX))); panel.style.top = newTop + 'px'; panel.style.left = newLeft + 'px'; }; const handleDragEnd = () => { if (!isDragging) return; isDragging = false; // 🆕 v6.20 修复「位置不记忆」:原代码 if (!hasMoved) 才保存 —— 条件写反了! // 实际拖动(hasMoved=true)后反而**不**保存,只有单纯点击才保存, // 导致用户拖到哪下次打开都回到旧位置。现在拖动结束一律保存。 pStorage.set('erpBinInfoPos', { top: panel.style.top, left: panel.style.left || 'auto' }); }; header.addEventListener('mousedown', handleDragStart); document.addEventListener('mousemove', handleDragMove); document.addEventListener('mouseup', handleDragEnd); header.addEventListener('touchstart', handleDragStart, { passive: false }); document.addEventListener('touchmove', handleDragMove, { passive: false }); document.addEventListener('touchend', handleDragEnd); // 🆕 缩放手柄:拖拽右下角调整面板大小 let isResizing = false, resizeStartX, resizeStartY, resizeStartW, resizeStartH; const minW = 280, minH = 360; const maxWAllowed = Math.max(minW, window.innerWidth - 40); const maxHAllowed = Math.max(minH, window.innerHeight - 40); const handleResizeStart = (e) => { e.preventDefault(); e.stopPropagation(); isResizing = true; const clientX = e.touches ? e.touches[0].clientX : e.clientX; const clientY = e.touches ? e.touches[0].clientY : e.clientY; resizeStartX = clientX; resizeStartY = clientY; resizeStartW = panel.offsetWidth; resizeStartH = panel.offsetHeight; document.body.style.userSelect = 'none'; document.body.style.cursor = 'nwse-resize'; }; const handleResizeMove = (e) => { if (!isResizing) return; e.preventDefault(); e.stopPropagation(); const clientX = e.touches ? e.touches[0].clientX : e.clientX; const clientY = e.touches ? e.touches[0].clientY : e.clientY; const dx = clientX - resizeStartX; const dy = clientY - resizeStartY; const newW = Math.max(minW, Math.min(maxWAllowed, resizeStartW + dx)); const newH = Math.max(minH, Math.min(maxHAllowed, resizeStartH + dy)); panel.style.width = newW + 'px'; // 🆕 v6.20:折叠态高度由 CSS height:auto!important 接管,只调宽度, // 避免「折叠条被拉出一条空白高度」 if (!panel.classList.contains('folded')) { panel.style.height = newH + 'px'; } applySizeTier(); }; const handleResizeEnd = (e) => { if (!isResizing) return; isResizing = false; document.body.style.userSelect = ''; document.body.style.cursor = ''; // 🆕 v6.20:折叠态只持久化宽度,高度保留旧值(展开后恢复原高度) const prevSize = pStorage.get('erpBinInfoSize', { width: '360px', height: '560px' }); pStorage.set('erpBinInfoSize', { width: panel.style.width, height: panel.classList.contains('folded') ? prevSize.height : panel.style.height }); }; // 🆕 面板放大档位:宽度越大,仓位明细字体/卡片越大(拉大面板明细跟着变大) const applySizeTier = () => { const w = panel.offsetWidth; panel.classList.toggle('size-lg', w >= 560); panel.classList.toggle('size-xl', w >= 860); }; applySizeTier(); resizeHandle.addEventListener('mousedown', handleResizeStart); document.addEventListener('mousemove', handleResizeMove); document.addEventListener('mouseup', handleResizeEnd); resizeHandle.addEventListener('touchstart', handleResizeStart, { passive: false }); document.addEventListener('touchmove', handleResizeMove, { passive: false }); document.addEventListener('touchend', handleResizeEnd); // 阻止 resize 手柄上的事件冒泡触发 panel 拖拽 resizeHandle.addEventListener('click', (e) => e.stopPropagation()); closeBtn.addEventListener('click', () => this.hidePanel()); // ====== 🆕 v6.20 折叠功能(状态自动记忆 erpBinInfoFolded) ====== // 折叠后:只留标题条(CSS .folded 用 !important 隐藏除 header/缩放手柄外的全部子元素, // 高度 auto)。标题条仍可拖动换位置(位置照常记忆),右下角手柄仍可拉宽度。 const foldBtn = document.getElementById('erp-bin-fold'); const applyFold = (folded) => { panel.classList.toggle('folded', folded); if (foldBtn) { foldBtn.textContent = folded ? '▸' : '▾'; foldBtn.title = folded ? '展开面板' : '折叠面板(只留标题栏,状态自动记忆)'; } pStorage.set('erpBinInfoFolded', folded === true); }; if (foldBtn) { foldBtn.addEventListener('click', (e) => { e.stopPropagation(); applyFold(!panel.classList.contains('folded')); }); } // 初始恢复上次折叠状态 if (pStorage.get('erpBinInfoFolded', false) === true) { applyFold(true); } // ====== 🆕 v6.22 一键折叠全部卡片 + 三个区块折叠(商品信息/发货时间/固定仓位) ====== const foldAllBtn = document.getElementById('erp-bin-fold-all'); if (foldAllBtn) { foldAllBtn.addEventListener('click', (e) => { e.stopPropagation(); this.toggleFoldAllCards(); }); } this.bindSectionFolds(); this.applySectionFolds(); // ====== 🆕 v6.23 关闭/开启自动显示按钮(标题栏 ⏰) ====== const autoShowBtn = document.getElementById('erp-bin-autoshow'); const applyAutoShowBtn = () => { if (!autoShowBtn) return; const on = pStorage.get('erpBinAutoShow', true); autoShowBtn.classList.toggle('off', !on); autoShowBtn.textContent = on ? '⏰' : '🚫'; autoShowBtn.title = on ? '点击关闭自动显示(关掉后面板不再自动弹出,下次打开本页也不会出现)' : '点击开启自动显示(恢复面板在每次打开本页时自动弹出)'; }; applyAutoShowBtn(); if (autoShowBtn) { autoShowBtn.addEventListener('click', (e) => { e.stopPropagation(); const cur = pStorage.get('erpBinAutoShow', true); if (cur) { // 关闭自动显示:立即隐藏面板 + 显示右侧📦按钮 + 加「关」徽章 pStorage.set('erpBinAutoShow', false); pStorage.set('erpBinInfoClosed', true); this.hidePanel(); } else { // 开启自动显示:打开面板 pStorage.set('erpBinAutoShow', true); pStorage.set('erpBinInfoClosed', false); this.showPanel(); } applyAutoShowBtn(); this.refreshToggleAutoShowBadge(); }); } // 初始渲染「记忆的固定仓位」(即使尚未搜索也显示上次的勾选) this.updateSelectOptions(currentBinItems); // 🆕 初始渲染「单选锁定」工具条(恢复持久化状态) this.updateSingleToolbar(); // 🆕 初始渲染「计算排除」工具条(恢复持久化状态) this.updateExcludeToolbar(); return panel; }, updateSelectOptions(binItems) { const whList = document.getElementById('erp-bin-wh-list'); if (!whList) return; // 当前数据中实际存在的仓位 const presentSet = new Set(); if (binItems && binItems.length) { for (const item of binItems) { presentSet.add(item.wh_id || '-'); } } // 合并「记忆的固定仓位」:即使当前商品没有该仓位数据,也固定显示(标记“无数据”) const allIds = new Set([...presentSet, ...selectedWhIds]); if (allIds.size === 0) { whList.innerHTML = '暂无仓位'; this.updateWhCount(); return; } let html = ''; for (const id of [...allIds].sort()) { const checked = selectedWhIds.has(id) ? 'checked' : ''; const hasData = presentSet.has(id); const cls = 'erp-bin-wh-chip' + (checked ? ' active' : '') + (hasData ? '' : ' nodata'); const tag = hasData ? '' : '无数据'; const safeId = String(id).replace(/"/g, '"'); html += ``; } whList.innerHTML = html; this.updateWhCount(); }, updateWhCount() { const title = document.querySelector('.erp-bin-wh-title'); if (!title) return; title.textContent = selectedWhIds.size > 0 ? `📌 已固定 ${selectedWhIds.size} 个仓位(可多选,自动记忆)` : '📌 固定显示仓位(可多选,自动记忆)'; }, updateSelectedBinDetail() { const binRow = document.getElementById('erp-bin-detail-row'); const statsEl = document.getElementById('erp-bin-sec-stats'); if (!binRow) return; // 🆕 过滤优先级:单选锁定 > 多选固定 > 全部(排除的仓位仍显示,但标记灰色) let filtered; if (singleMode && singleWhId) { filtered = (currentBinItems || []).filter(it => keyOfItem(it) === singleWhId); } else if (selectedWhIds.size > 0) { filtered = currentBinItems.filter(it => selectedWhIds.has(it.wh_id || '-')); } else { filtered = (currentBinItems || []); } const filtQty = filtered.reduce((s, it) => s + (parseFloat(it.qty) || 0), 0); const filtExcluded = filtered.filter(it => excludedKeys.has(keyOfItem(it))).length; // 顶部统计条(精简:共 N 行 + 数量 + 状态徽章) if (statsEl) { const pinTxt = selectedWhIds.size > 0 && !(singleMode && singleWhId) ? `📌 已固定 ${selectedWhIds.size}` : ''; const singleTxt = singleMode && singleWhId ? `📍 单选锁定中` : ''; const excludeTxt = filtExcluded > 0 ? `⊘ 已排除 ${filtExcluded}` : ''; statsEl.innerHTML = `${filtered.length}` + `数量 ${filtQty.toLocaleString()}` + pinTxt + singleTxt + excludeTxt; } if (!currentBinItems || currentBinItems.length === 0) { binRow.innerHTML = '
📭 无仓位库存数据' + (lastBinHint ? '⚠️ ' + lastBinHint : '输入商品编码搜索仓位') + '
'; this.updateSingleToolbar(); return; } if (filtered.length === 0) { const tag = singleWhId && singleWhId.startsWith('B:') ? '仓位' : '仓库'; const msg = singleMode && singleWhId ? '锁定的' + tag + '(' + singleWhId.slice(2) + ')当前商品无数据' : '所选固定仓位无数据'; // 🆕 v6.19 切换仓库后最容易踩这个坑:接口其实有数据,只是被上一次的「锁定/固定」筛选挡住了, // 面板却显示“无数据”,看起来就像接口查不到。这里给出可点的一键解除。 const blocked = (currentBinItems || []).length > 0; binRow.innerHTML = `
📭 ${msg}` + (blocked ? `接口其实返回了 ${currentBinItems.length} 条仓位记录,只是被记忆的筛选条件挡住了` + `` : `输入商品编码搜索仓位`) + `
`; const unblockBtn = binRow.querySelector('.erp-bin-unblock-btn'); if (unblockBtn) { unblockBtn.addEventListener('click', () => { singleMode = false; singleWhId = null; selectedWhIds.clear(); pStorage.set('erpBinSingleMode', false); pStorage.set('erpBinSingleWhId', null); pStorage.set('erpBinSelectedWhIds', []); this.updateSingleToolbar(); this.updateSelectedBinDetail(); }); } this.updateSingleToolbar(); return; } let totalQty = 0; let rows = ''; for (const item of filtered) { const qty = parseFloat(item.qty) || 0; const rawBin = getBin(item); const binName = (!rawBin || rawBin === '-') ? '未指定仓位' : rawBin; const whId = item.wh_id || '-'; const itemKey = keyOfItem(item); const lockV = getItemLock(item); const lockNum = lockV != null ? (parseFloat(lockV) || 0) : null; totalQty += qty; const isEmpty = qty <= 0; const isSingleActive = singleMode && singleWhId === itemKey; const isExcluded = excludedKeys.has(itemKey); // 总是渲染占有徽章,三种状态: let lockTxt; if (!lockLoaded) { lockTxt = `占有 `; } else if (lockNum != null && lockNum > 0) { lockTxt = `占有 ${lockNum.toLocaleString()}`; } else { lockTxt = `占有 —`; } // 🆕 卡片右上角:「⊘ 排除」+「📍 单选」按钮 const singleBtnTxt = isSingleActive ? '✓ 已锁定' : '📍 单选此仓'; const singleBtnTitle = isSingleActive ? '取消单选锁定' : '仅显示该仓位(自动记忆)'; const excludeBtnTxt = isExcluded ? '✓ 已排除' : '⊘ 排除'; const excludeBtnTitle = isExcluded ? '取消排除,该仓位重新计入占有计算' : '排除该仓位:库存与占有都不计入顶部占有比例计算(自动记忆)'; // 🆕 v6.21 折叠态摘要:折叠后标题行仍能看到数量与占有 const foldLockTxt = (lockNum != null && lockNum > 0) ? ` · 占有 ${lockNum.toLocaleString()}` : ''; const foldSummary = `${qty.toLocaleString()} 件${foldLockTxt}`; rows += `
` + `
` + `${binName}` + `${foldSummary}` + `` + `
` + `
` + `${qty.toLocaleString()}` + `` + `
` + `
` + `${whId}` + lockTxt + `
` + `` + `` + `
` + `
`; } binRow.innerHTML = `
${rows}
`; // 🆕 绑定单选按钮事件(keyOfItem 仓位优先) binRow.querySelectorAll('.row-single-btn').forEach(btn => { btn.addEventListener('click', (e) => { e.stopPropagation(); const key = btn.getAttribute('data-key'); if (singleMode && singleWhId === key) { // 再次点击 → 取消单选 singleMode = false; singleWhId = null; } else { singleMode = true; singleWhId = key; } pStorage.set('erpBinSingleMode', singleMode); pStorage.set('erpBinSingleWhId', singleWhId); this.updateSingleToolbar(); this.updateSelectedBinDetail(); }); }); // 🆕 绑定排除按钮事件(切换排除状态并重算占有比例) binRow.querySelectorAll('.row-exclude-btn').forEach(btn => { btn.addEventListener('click', (e) => { e.stopPropagation(); const key = btn.getAttribute('data-key'); if (excludedKeys.has(key)) { excludedKeys.delete(key); } else { excludedKeys.add(key); } pStorage.set('erpBinExcludedKeys', [...excludedKeys]); this.updateExcludeToolbar(); this.updateSelectedBinDetail(); // 用缓存数据立即重算顶部徽章与进度条 if (currentSkuId) { const lockQty = (lockData[currentSkuId] && lockData[currentSkuId].order_lock) || 0; UI.updateInfoBar(currentSkuId, currentTotalQty, lockQty, currentBinItems, currentWhInfo); } }); }); // 🆕 v6.21 应用记忆的每卡片状态(折叠/大小)并绑定折叠+缩放交互 this.applyCardStates(binRow); this.bindCardInteractions(binRow); this.updateSingleToolbar(); this.updateExcludeToolbar(); }, // 🆕 更新单选锁定工具条显示(区分仓位 / 仓库) updateSingleToolbar() { const row = document.getElementById('erp-bin-single-row'); const cur = document.getElementById('erp-bin-single-current'); const clearBtn = document.getElementById('erp-bin-single-clear'); const panel = document.getElementById('erp-bin-info-panel'); if (!row || !cur) return; const isActive = singleMode && !!singleWhId; // 🆕 精简视图:单选激活时给 panel 加 single-mode-active class if (panel) panel.classList.toggle('single-mode-active', isActive); if (isActive) { row.classList.add('active'); if (singleWhId.startsWith('B:')) { cur.textContent = '🔒 仅显示仓位:' + singleWhId.slice(2); } else if (singleWhId.startsWith('W:')) { cur.textContent = '🔒 仅显示仓库:' + singleWhId.slice(2); } else { // 兼容旧值(没有前缀) cur.textContent = '🔒 仅显示:' + singleWhId; } if (clearBtn) clearBtn.style.display = ''; } else { row.classList.remove('active'); cur.textContent = '未锁定(显示全部仓位,可点击卡片右上角「📍 单选此仓」锁定单个仓位)'; if (clearBtn) clearBtn.style.display = 'none'; } }, // 🆕 更新排除工具条显示 updateExcludeToolbar() { const row = document.getElementById('erp-bin-exclude-row'); const cur = document.getElementById('erp-bin-exclude-current'); if (!row || !cur) return; if (excludedKeys.size > 0) { row.classList.add('has-exclude'); const names = [...excludedKeys].map(k => { if (k.startsWith('B:')) return k.slice(2); if (k.startsWith('W:')) return '仓库:' + k.slice(2); return k; }); cur.textContent = '已排除 ' + excludedKeys.size + ' 项:' + names.join('、'); cur.title = names.join('\n'); } else { row.classList.remove('has-exclude'); cur.textContent = '未排除'; cur.title = ''; } }, // ====== 🆕 v6.21 每张仓位卡片:折叠 + 拉大小(按仓位 key 记忆 erpBinCardStates) ====== getCardStates() { return pStorage.get('erpBinCardStates', {}) || {}; }, saveCardState(key, patch) { if (!key) return; const all = this.getCardStates(); all[key] = Object.assign({}, all[key], patch); pStorage.set('erpBinCardStates', all); }, // 当前列表实际网格列数(auto-fill 随面板宽度变化) listColumnCount(listEl) { try { const cols = getComputedStyle(listEl).gridTemplateColumns; if (cols && cols !== 'none' && cols !== '') { return Math.max(1, cols.trim().split(/\s+/).length); } } catch (e) {} return 1; }, // 卡片当前横跨列数(解析内联 grid-column: span N) currentCardSpan(card) { const m = /span\s+(\d+)/i.exec(card.style.gridColumn || ''); return m ? Math.max(1, parseInt(m[1], 10)) : 1; }, // 折叠/展开单张卡片(persist=false 时只改样式不落盘,用于渲染后恢复) setCardFolded(card, folded, persist) { card.classList.toggle('folded', folded === true); const ind = card.querySelector('.row-fold-ind'); if (ind) ind.textContent = (folded === true) ? '▸' : '▾'; if (folded) { // 折叠时暂存自定义高度:内联 min-height:!important 会顶掉 CSS 的 height:auto!important if (card.style.minHeight) { card.dataset.prevMinH = card.style.minHeight; card.style.removeProperty('min-height'); } } else if (card.dataset.prevMinH) { card.style.setProperty('min-height', card.dataset.prevMinH, 'important'); delete card.dataset.prevMinH; } if (persist !== false) { const key = card.getAttribute('data-key'); if (key) this.saveCardState(key, { folded: folded === true }); // 🆕 v6.22 同步「全部折叠/全部展开」按钮文案 this.updateFoldAllBtn(); } }, // 渲染后恢复每张卡片记忆的折叠状态与大小 applyCardStates(binRow) { const states = this.getCardStates(); binRow.querySelectorAll('.erp-bin-row').forEach(card => { const key = card.getAttribute('data-key'); const st = key ? states[key] : null; if (!st) return; if (st.folded === true) this.setCardFolded(card, true, false); if (st.span && st.span > 1) { const list = card.closest('.erp-bin-list'); const maxCols = list ? this.listColumnCount(list) : 1; // 面板变窄列数变少时,按当前列数封顶,避免卡片溢出容器 if (st.span <= maxCols) card.style.gridColumn = 'span ' + st.span; } if (st.minHeight && st.folded !== true) { card.style.setProperty('min-height', st.minHeight + 'px', 'important'); } }); // 🆕 v6.22 渲染后同步一键折叠按钮文案 this.updateFoldAllBtn(); }, // 绑定每张卡片的折叠(点标题行)与缩放(右下角手柄)交互 bindCardInteractions(binRow) { const list = binRow.querySelector('.erp-bin-list'); if (!list) return; // 折叠:点击卡片标题行切换(排除/单选按钮为绝对定位,不在标题行内) list.querySelectorAll('.erp-bin-row .row-head').forEach(head => { head.addEventListener('click', (e) => { if (e.target.closest('.row-exclude-btn, .row-single-btn, .row-resize-handle')) return; const card = head.closest('.erp-bin-row'); if (!card) return; this.setCardFolded(card, !card.classList.contains('folded'), true); }); }); // 拉大小:右下角手柄拖拽(横向=横跨列数 span,纵向=最小高度;折叠态只调宽度) list.querySelectorAll('.erp-bin-row .row-resize-handle').forEach(handle => { const startCardResize = (e) => { const card = handle.closest('.erp-bin-row'); if (!card) return; e.preventDefault(); e.stopPropagation(); const key = card.getAttribute('data-key'); const startX = e.touches ? e.touches[0].clientX : e.clientX; const startY = e.touches ? e.touches[0].clientY : e.clientY; const startSpan = this.currentCardSpan(card); const startW = card.offsetWidth; const startH = card.offsetHeight; let gap = 12; try { gap = parseFloat(getComputedStyle(list).gap) || 12; } catch (err) {} // 单列宽度(含间距):由当前跨列宽度反推 const colUnit = startSpan > 0 ? ((startW - (startSpan - 1) * gap) / startSpan) + gap : startW + gap; const maxCols = this.listColumnCount(list); let resizing = true; card.classList.add('resizing'); document.body.style.userSelect = 'none'; document.body.style.cursor = 'nwse-resize'; const onMove = (ev) => { if (!resizing) return; if (ev.cancelable) ev.preventDefault(); const cx = ev.touches ? ev.touches[0].clientX : ev.clientX; const cy = ev.touches ? ev.touches[0].clientY : ev.clientY; const targetW = Math.max(60, startW + (cx - startX)); const newSpan = Math.max(1, Math.min(maxCols, Math.round(targetW / colUnit))); card.style.gridColumn = 'span ' + newSpan; if (!card.classList.contains('folded')) { const newH = Math.max(60, Math.min(1600, startH + (cy - startY))); // 基础 CSS 的 min-height:118px!important 会盖过普通内联值,必须带 important card.style.setProperty('min-height', newH + 'px', 'important'); } }; const onEnd = () => { if (!resizing) return; resizing = false; card.classList.remove('resizing'); document.body.style.userSelect = ''; document.body.style.cursor = ''; document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onEnd); document.removeEventListener('touchmove', onMove); document.removeEventListener('touchend', onEnd); if (key) { const prev = this.getCardStates()[key] || {}; this.saveCardState(key, { span: this.currentCardSpan(card), // 折叠态只持久化宽度(span),高度保留旧值(展开后恢复) minHeight: card.classList.contains('folded') ? (prev.minHeight || null) : (parseInt(card.style.minHeight, 10) || null) }); } }; document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onEnd); document.addEventListener('touchmove', onMove, { passive: false }); document.addEventListener('touchend', onEnd); }; handle.addEventListener('mousedown', startCardResize); handle.addEventListener('touchstart', startCardResize, { passive: false }); handle.addEventListener('click', (e) => e.stopPropagation()); // 双击手柄:恢复默认大小 handle.addEventListener('dblclick', (e) => { e.stopPropagation(); const card = handle.closest('.erp-bin-row'); if (!card) return; card.style.gridColumn = ''; card.style.removeProperty('min-height'); delete card.dataset.prevMinH; const key = card.getAttribute('data-key'); if (key) this.saveCardState(key, { span: 1, minHeight: null }); }); }); }, // ====== 🆕 v6.22 一键折叠/展开全部仓位卡片 ====== toggleFoldAllCards() { const cards = Array.prototype.slice.call(document.querySelectorAll('#erp-bin-detail-row .erp-bin-row')); if (!cards.length) return; const anyOpen = cards.some(c => !c.classList.contains('folded')); cards.forEach(c => this.setCardFolded(c, anyOpen, true)); this.updateFoldAllBtn(); }, updateFoldAllBtn() { const btn = document.getElementById('erp-bin-fold-all'); if (!btn) return; const cards = Array.prototype.slice.call(document.querySelectorAll('#erp-bin-detail-row .erp-bin-row')); const allFolded = cards.length > 0 && cards.every(c => c.classList.contains('folded')); btn.textContent = allFolded ? '⊞ 全部展开' : '⇕ 全部折叠'; btn.title = allFolded ? '一键展开全部仓位卡片' : '一键折叠全部仓位卡片(自动记忆)'; }, // ====== 🆕 v6.22 区块折叠:商品信息 / 订单剩余发货时间 / 固定显示仓位 ====== // 折叠状态存 erpBinSecFolded = { sku, rem, select } setSectionFolded(wrapId, btnId, folded, key) { const wrap = document.getElementById(wrapId); if (!wrap) return; wrap.classList.toggle('sec-folded', folded === true); const btn = document.getElementById(btnId); if (btn) { btn.textContent = (folded === true) ? '▸' : '▾'; btn.title = (folded === true) ? '展开本区块' : '折叠本区块(自动记忆)'; } if (key) { const all = pStorage.get('erpBinSecFolded', {}) || {}; all[key] = (folded === true); pStorage.set('erpBinSecFolded', all); } }, applySectionFolds() { const saved = pStorage.get('erpBinSecFolded', {}) || {}; [['sku', 'erp-bin-sku-row', 'erp-bin-fold-sku'], ['rem', 'erp-bin-rem-wrap', 'erp-bin-fold-rem'], ['select', 'erp-bin-select-row', 'erp-bin-fold-select']].forEach(([key, wrapId, btnId]) => { this.setSectionFolded(wrapId, btnId, saved[key] === true, key); }); }, bindSectionFolds() { [['sku', 'erp-bin-sku-row', 'erp-bin-fold-sku'], ['rem', 'erp-bin-rem-wrap', 'erp-bin-fold-rem'], ['select', 'erp-bin-select-row', 'erp-bin-fold-select']].forEach(([key, wrapId, btnId]) => { const btn = document.getElementById(btnId); if (!btn) return; btn.addEventListener('click', (e) => { e.stopPropagation(); const wrap = document.getElementById(wrapId); if (!wrap) return; this.setSectionFolded(wrapId, btnId, !wrap.classList.contains('sec-folded'), key); }); }); }, updateInfoBar(sku_id, qty, lockQty, binItems, whInfo) { currentBinItems = binItems || []; currentSkuId = sku_id; currentLockQty = lockQty || 0; // 🆕 缓存上次查询参数,供订单占有到达后全量重渲染 currentTotalQty = qty; currentWhInfo = whInfo || null; // 🆕 计算当前过滤后的实际显示数量(单选锁定 > 多选固定 > 全部) let displayQty = parseFloat(qty) || 0; if (singleMode && singleWhId) { const singleItem = currentBinItems.find(it => keyOfItem(it) === singleWhId); displayQty = singleItem ? (parseFloat(singleItem.qty) || 0) : 0; } else if (selectedWhIds.size > 0) { displayQty = currentBinItems .filter(it => selectedWhIds.has(it.wh_id || '-')) .reduce((s, it) => s + (parseFloat(it.qty) || 0), 0); } // 🆕 排除计算:被排除的仓库/仓位不计入占有比例(分母=有效库存,分子=有效占有) const excludedItems = excludedKeys.size > 0 ? currentBinItems.filter(it => excludedKeys.has(keyOfItem(it))) : []; const excludedQty = excludedItems.reduce((s, it) => s + (parseFloat(it.qty) || 0), 0); let excludedLock = 0; for (const it of excludedItems) { const rawLock = getItemLockRaw(it); // 仅取 item 自身字段,不回退 SKU 级 if (rawLock != null && rawLock > 0) excludedLock += rawLock; } const rawTotalQty = parseFloat(qty) || 0; // 有效库存:优先用逐条数据求和(与排除逻辑精确对应),否则用传入总数量减去排除量 let effectiveTotalQty; if (currentBinItems.length > 0) { effectiveTotalQty = currentBinItems .filter(it => !excludedKeys.has(keyOfItem(it))) .reduce((s, it) => s + (parseFloat(it.qty) || 0), 0); } else { effectiveTotalQty = Math.max(0, rawTotalQty - excludedQty); } // 有效占有:SKU 总占有 - 被排除仓位的逐条占有(仅当能取到逐条数据时扣减) const effectiveLockQty = Math.max(0, (parseFloat(lockQty) || 0) - excludedLock); const skuEl = document.getElementById('erp-bin-sku'); const qtyEl = document.getElementById('erp-bin-qty'); const lockEl = document.getElementById('erp-bin-lock'); const lockBarWrap = document.getElementById('erp-bin-lock-bar-wrap'); const lockPctEl = document.getElementById('erp-bin-lock-pct'); const lockBarFill = document.getElementById('erp-bin-lock-bar-fill'); const lockPctTopRight = document.getElementById('erp-bin-lock-pct-topright'); const lockInfo = lockData[sku_id] || null; const supplierName = lockInfo ? lockInfo.supplier_name : ''; if (skuEl) skuEl.innerHTML = `🔑 编码: ${sku_id || '-'}`; // 🆕 数量徽章:优先显示「排除后有效库存」,单选/多选过滤时显示过滤数量 const isFilteredView = (singleMode && singleWhId) || selectedWhIds.size > 0; const hasExclusion = excludedKeys.size > 0; let qtyDisplay, qtyTitle; if (isFilteredView) { qtyDisplay = displayQty; qtyTitle = `已过滤显示(原总计 ${qty || '-'})`; if (hasExclusion) qtyTitle += `;已排除 ${excludedKeys.size} 项不计入计算`; } else if (hasExclusion) { qtyDisplay = effectiveTotalQty; qtyTitle = `已排除 ${excludedKeys.size} 项(原总计 ${rawTotalQty},排除库存 ${excludedQty})`; } else { qtyDisplay = (qty || '-'); qtyTitle = ''; } if (qtyEl) { qtyEl.innerHTML = `📊 数量: ${qtyDisplay}`; qtyEl.title = qtyTitle; } // 供应商信息 const skuRow = document.getElementById('erp-bin-sku-row'); let supplierBadge = document.getElementById('erp-bin-supplier'); if (supplierName && skuRow) { if (!supplierBadge) { supplierBadge = document.createElement('span'); supplierBadge.id = 'erp-bin-supplier'; supplierBadge.className = 'erp-bin-badge'; supplierBadge.style.cssText = 'background: #f0f5ff !important; color: #1890ff !important; border: 1px solid #91d5ff !important;'; skuRow.appendChild(supplierBadge); } supplierBadge.innerHTML = `🏭 ${supplierName}`; supplierBadge.style.display = 'inline-flex'; } else if (supplierBadge) { supplierBadge.style.display = 'none'; } // ✅ 进度条/右上百分比:用「排除后」的有效库存与有效占有计算 const totalQty = effectiveTotalQty; const hasLockData = effectiveLockQty > 0; const hasTotal = totalQty > 0; // 占有百分比(右上角) if (lockPctTopRight) { if (hasLockData && hasTotal) { const pct = Math.min(100, Math.round((effectiveLockQty / totalQty) * 100)); lockPctTopRight.textContent = `占有 ${pct}%`; lockPctTopRight.style.background = pct >= 80 ? 'rgba(239,68,68,0.85)' : pct >= 50 ? 'rgba(245,158,11,0.85)' : 'rgba(255,255,255,0.25)'; lockPctTopRight.style.display = 'block'; } else { lockPctTopRight.style.display = 'none'; } } // 占有徽章(显示有效占有;有排除时 title 提示原始值) if (lockEl) { if (effectiveLockQty > 0) { lockEl.innerHTML = `🔒 占有: ${effectiveLockQty}`; lockEl.title = hasExclusion && excludedLock > 0 ? `原总占有 ${lockQty},已排除仓位占有 ${excludedLock}` : ''; lockEl.style.display = 'inline-flex'; } else { lockEl.style.display = 'none'; } } // 底部进度条(始终显示:有效占有=0 时显示空条+0%,>0 时显示实际比例) if (lockBarWrap) { if (hasTotal) { lockBarWrap.style.display = 'block'; const pct = hasLockData ? Math.min(100, Math.round((effectiveLockQty / totalQty) * 100)) : 0; if (lockPctEl) lockPctEl.textContent = pct + '%'; if (lockBarFill) { lockBarFill.style.width = pct + '%'; lockBarFill.style.background = pct >= 80 ? 'linear-gradient(90deg, #ef4444, #b91c1c)' : pct >= 50 ? 'linear-gradient(90deg, #f59e0b, #d97706)' : pct > 0 ? 'linear-gradient(90deg, #f87171, #dc2626)' : 'linear-gradient(90deg, #cbd5e1, #94a3b8)'; } // 修改 label 显示:含有效占有数 const lbl = lockBarWrap.querySelector('.erp-bin-lock-bar-label'); if (lbl) { const leftSpan = lbl.querySelector('span:first-child'); if (leftSpan) { leftSpan.textContent = hasLockData ? `订单占有 (${effectiveLockQty} / ${totalQty})` : `订单占有 (无 / ${totalQty})`; if (hasExclusion) leftSpan.title = `已排除 ${excludedKeys.size} 项:库存-${excludedQty}${excludedLock > 0 ? ',占有-' + excludedLock : ''}`; } } } else { // 总库存未知:占位(搜索前) lockBarWrap.style.display = 'none'; } } this.updateSelectOptions(binItems); this.updateSelectedBinDetail(); } }; /*********************** * 数据处理 ***********************/ const DataProcessor = { extractProductInfo(obj) { const info = { sku_id: null, qty: null }; function search(obj) { if (obj && typeof obj === 'object') { if (obj.hasOwnProperty('sku_id') && !info.sku_id) info.sku_id = obj.sku_id; if (obj.hasOwnProperty('id') && !info.sku_id) info.sku_id = obj.id; if (obj.hasOwnProperty('qty') && !info.qty) info.qty = obj.qty; if (Array.isArray(obj)) { for (const item of obj) { if (search(item)) return true; } } else { for (const key in obj) { if (obj.hasOwnProperty(key)) { if (search(obj[key])) return true; } } } } return false; } search(obj); return info; }, parseRequestData(data) { if (!data) return ''; if (typeof data === 'string') return data; if (data instanceof FormData) { let result = ''; for (let [key, value] of data.entries()) { if (result) result += '&'; result += `${encodeURIComponent(key)}=${encodeURIComponent(value)}`; } return result; } try { return JSON.stringify(data); } catch (e) { return String(data); } }, isRelevantRequest(url, data) { const urlStr = typeof url === 'string' ? url : url instanceof URL ? url.href : ''; if (urlStr.includes('PackItems.aspx')) return false; const urlContainsTarget = urlStr.includes('__CALLBACKPARAM') && ((urlStr.includes('"Method":"CheckQty"') || urlStr.includes("'Method':'CheckQty'")) || (urlStr.includes('"Method":"LoadDataToJSON"') || urlStr.includes("'Method':'LoadDataToJSON'"))); if (urlContainsTarget) { if (urlStr.includes('sku_id') || urlStr.includes('[p].bin')) return false; return true; } const dataStr = this.parseRequestData(data); if (!dataStr.includes('__CALLBACKPARAM')) return false; const callbackParamMatch = dataStr.match(/__CALLBACKPARAM=([^&]+)/); if (!callbackParamMatch) return false; const callbackParam = decodeURIComponent(callbackParamMatch[1]); if (callbackParam.includes('[p].bin') || callbackParam.includes('waitpay')) return false; return callbackParam.includes('"Method":"CheckQty"') || callbackParam.includes("'Method':'CheckQty'") || callbackParam.includes('"Method":"LoadDataToJSON"') || callbackParam.includes("'Method':'LoadDataToJSON'"); } }; /*********************** * 请求监控 ***********************/ const RequestMonitor = { init() { this.monitorXMLHttpRequest(); this.monitorFetch(); }, monitorXMLHttpRequest() { const originalXHRSend = XMLHttpRequest.prototype.send; const self = this; XMLHttpRequest.prototype.send = function(data) { const xhr = this; const isRelevant = DataProcessor.isRelevantRequest(xhr.responseURL, data); if (isRelevant) { xhr.addEventListener('readystatechange', function() { if (xhr.readyState === 4) { try { const response = parseResponseData(xhr.responseText); if (response.ReturnValue) { const productInfo = DataProcessor.extractProductInfo(response.ReturnValue); if (productInfo.sku_id) { self.updateInfo(productInfo); } } } catch (error) { // 静默处理,不影响聚水潭 } } }); } originalXHRSend.call(xhr, data); }; }, monitorFetch() { const originalFetch = window.fetch; const self = this; window.fetch = async function(url, options) { const requestUrl = typeof url === 'string' ? url : url.href; const isRelevant = DataProcessor.isRelevantRequest(requestUrl, options?.body); const response = await originalFetch.call(this, url, options); if (isRelevant) { try { const clonedResponse = response.clone(); const textResponse = await clonedResponse.text(); const responseJson = parseResponseData(textResponse); if (responseJson.ReturnValue) { const productInfo = DataProcessor.extractProductInfo(responseJson.ReturnValue); if (productInfo.sku_id) { self.updateInfo(productInfo); } } } catch (error) { // 静默处理,不影响聚水潭 } } return response; }; }, async updateInfo(productInfo) { try { if (!state.ui) { UI.createInfoBar(); } if (productInfo.sku_id) { const sku_id = productInfo.sku_id; const lockInfo = lockData[sku_id]; const lockQty = lockInfo ? lockInfo.order_lock : 0; // 🆕 v6.17 自动捕获编码时同样加载订单剩余发货时间卡片 loadRemCards(sku_id).catch(() => {}); const { items: binItems, whInfo } = await queryBinStock(sku_id); let totalQty = 0; for (const item of binItems) { totalQty += parseFloat(item.qty) || 0; } UI.updateInfoBar(sku_id, totalQty > 0 ? totalQty : '-', lockQty, binItems, whInfo); } } catch (error) { console.error('[仓位信息栏] updateInfo 异常:', error); } } }; // 启动 try { RequestMonitor.init(); console.log('[仓位信息栏] 已启动 v6.10(统一兼容版)', isMobile ? '[移动端模式]' : '[桌面端模式]'); } catch (error) { console.error('[仓位信息栏] 初始化失败:', error); } })();