diff --git a/inventory-backend/app/api/v1/warehouse.py b/inventory-backend/app/api/v1/warehouse.py index ae94f64..8b24af6 100644 --- a/inventory-backend/app/api/v1/warehouse.py +++ b/inventory-backend/app/api/v1/warehouse.py @@ -34,14 +34,36 @@ def build_tree(nodes, parent_id=None): def get_tree(): """ 获取库位树形结构 + + 查询参数: + prefixes —— 可选,逗号分隔的顶层前缀,例如 ?prefixes=Y 或 ?prefixes=C,L + 只返回**顶层** name / full_path 命中这些前缀的根节点及其完整子树; + 不传则返回全量。 + + 用途:前端按公司精简拉取(IRIS 只要 Y*,LICA 只要 C*/L*), + 在**保留完整子树**的前提下减少节点数与传输量 —— 不能退回懒加载, + 因为 setCheckedKeys / getCheckedNodes 依赖全树已构建。 """ try: + raw_prefixes = request.args.get('prefixes', '', type=str) + prefixes = [p.strip().upper() for p in raw_prefixes.split(',') if p.strip()] + # 查询所有库位,按 name 升序排序 all_locations = SysWarehouseLocation.query.order_by(SysWarehouseLocation.name.asc()).all() # 构建树形结构(O(N) 内存组装,见 build_tree) tree_data = build_tree(all_locations, parent_id=None) + # ★ 只在**顶层**做前缀过滤:命中即整棵子树保留,不递归裁剪, + # 避免把子树打散导致前端勾选语义错乱 + if prefixes: + def _hit(node): + name = str(node.get('name') or '').upper() + path = str(node.get('full_path') or '').upper() + return any(name.startswith(p) or path.startswith(p) for p in prefixes) + + tree_data = [n for n in tree_data if _hit(n)] + return jsonify({ 'code': 200, 'msg': 'success', diff --git a/inventory-web/src/api/common/warehouse.ts b/inventory-web/src/api/common/warehouse.ts index c3685bc..2c6f0b3 100644 --- a/inventory-web/src/api/common/warehouse.ts +++ b/inventory-web/src/api/common/warehouse.ts @@ -1,10 +1,13 @@ import request from '@/utils/request' // 获取库位树形结构 -export function getWarehouseTree() { +// prefixes 可选:只返回顶层命中这些前缀的根节点及其完整子树(后端过滤), +// 用于按公司精简拉取(IRIS 传 ['Y'],LICA 传 ['C','L']);不传则全量 +export function getWarehouseTree(prefixes?: string[]) { return request({ url: '/v1/warehouse/tree', - method: 'get' + method: 'get', + params: prefixes && prefixes.length ? { prefixes: prefixes.join(',') } : undefined }) } diff --git a/inventory-web/src/views/stock/stocktake/index.vue b/inventory-web/src/views/stock/stocktake/index.vue index 809c20a..fac26a3 100644 --- a/inventory-web/src/views/stock/stocktake/index.vue +++ b/inventory-web/src/views/stock/stocktake/index.vue @@ -791,22 +791,16 @@ const showCreateForm = computed(() => // 抽盘配置区是左右双栏,400px 的窄容器装不下,此时把欢迎页放宽 const idleWide = computed(() => showCreateForm.value && newScopeType.value === 'active') -// 库位树按公司前缀过滤(IRIS 只看 Y,LICA 看 C/L;未配置的公司不过滤) -const filterTreeByCompany = (nodes: any[], company: string) => { - const prefixes = getAllowedLocPrefixes(company) - if (!prefixes.length) return nodes - return nodes.filter((n: any) => - prefixes.some((p: string) => String(n?.name || '').toUpperCase().startsWith(p.toUpperCase())) - ) -} - const loadLocationTree = async () => { if (locTreeData.value.length) return treeLoading.value = true try { - const res: any = await getWarehouseTree() - const raw = res?.data || [] - locTreeData.value = filterTreeByCompany(raw, selectedCompany.value) + // ★ 按公司前缀**后端过滤**顶层节点(子树完整保留):IRIS 只拉 Y*, + // LICA 只拉 C*/L*,显著减少节点数与传输量。 + // 注意不能改成懒加载 —— setCheckedKeys / getCheckedNodes 依赖全树已构建。 + const prefixes = getAllowedLocPrefixes(selectedCompany.value) + const res: any = await getWarehouseTree(prefixes) + locTreeData.value = res?.data || [] } catch (e) { console.error('获取库位树失败', e) ElMessage.error('获取库位树失败') @@ -837,16 +831,21 @@ const fetchRecommendLocations = async () => { } recLoading.value = true try { - // 先把树准备好,否则 setCheckedKeys 对尚未渲染的节点无效 - await loadLocationTree() + // ★ 树与推荐**并行**:两者互不依赖,串行会把两段网络等待直接叠加。 + // loadLocationTree 内部有「已加载则短路返回」,重复调用无额外开销。 + const [, res] = await Promise.all([ + loadLocationTree(), + request({ + url: '/v1/inbound/stock/stocktake/recommend-locations', + method: 'get', + params: withCompany({ days: recommendDays.value, top_n: recTopN.value }) + }) + ]) + + // 等树渲染完再勾选,否则 setCheckedKeys 对尚未渲染的节点无效 await nextTick() - const res: any = await request({ - url: '/v1/inbound/stock/stocktake/recommend-locations', - method: 'get', - params: withCompany({ days: recommendDays.value, top_n: recTopN.value }) - }) - const locs: string[] = res?.data?.locations || [] + const locs: string[] = (res as any)?.data?.locations || [] if (!locs.length) { ElMessage.warning(`最近 ${recommendDays.value} 天没有找到活跃库位`) return @@ -858,9 +857,12 @@ const fetchRecommendLocations = async () => { syncSelectedPaths() // 推荐里有、但树上勾不到的(不在当前公司前缀范围内等)要如实告知, - // 否则这些库位会静默落选 —— 工人以为盘到了,其实没进范围 - const checkedPaths = new Set(selectedLocationPaths.value) - const missing = locs.filter(l => !checkedPaths.has(l)) + // 否则这些库位会静默落选 —— 工人以为盘到了,其实没进范围。 + // ★ 注意不能逐字比对:右侧只存**末级**路径,而推荐返回的是库存级路径, + // 可能是非末级(勾它会级联勾中其子节点),故需按「自身或其祖先」判定覆盖。 + const isCovered = (rec: string) => + selectedLocationPaths.value.some(p => p === rec || p.startsWith(rec + '/')) + const missing = locs.filter(l => !isCovered(l)) if (missing.length) { ElMessage.warning(`推荐中 ${missing.length} 个库位不在当前公司的库位树上,未能勾选`) console.warn('未能勾选的推荐库位:', missing)