// ==UserScript==
// @name ERP仓位占有信息栏(统一兼容版 v6.24)
// @namespace https://scriptcat.org/zh-CN/users/214207
// @version 6.25
// @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 = '📦 仓位占有信息占有 0%▾⏰◀';
const skuSearchRow = createEl('div', { id: 'erp-bin-sku-search-row' });
skuSearchRow.innerHTML = `
`;
const skuRow = createEl('div', { id: 'erp-bin-sku-row' });
skuRow.innerHTML = `
▾
🔑 编码: -
📊 数量: -
🔒 占有: 0
`;
// 🆕 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()}` +
`件` +
`
` +
`` +
`
` +
`
` +
`
` +
`
`;
}
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);
}
})();