# 仓储首页看板口径 依据 2026-10-22 的 V2.3/V2.4 变更记录实施;随附文档虽然标为 V2.2,正文中旧版的六卡、库位地图和五类动态说明不再适用。 - 首页为四格单行待办、五张 KPI(编号 01、02、03、04、06)、近 30 天出入库柱状与净趋势、分类横条,以及库龄环图、预警 TOP5、四类最近作业动态。 - 仓库筛选只用已授权仓库;默认显示全部基本单位,但数量混合单位时显示“—”并提示用户选择单位,不默认套用列表第一单位。库存数量、分类、库龄均扣锁。 - 库存总量、库存金额及库存分类分布的“查看台账”和图表点击均打开现有 /warehouseManagement/stockLedger 列表;首页不再提供自建台账明细视图。分类横条点击会带 `categoryLevelId`(分类层级 id)打开台账并预选左侧树节点,口径见 D2。 - 呆滞占比按批次数,不按金额;库存周转按数量快照计算并剔除调拨出库。C2 没有核实的移动加权不含税成本源时显示“—”,切勿把入库价估值当成账面金额。 - 库存总量卡片的「较上周」=本期数量 − 7 天前**日末同刻**快照(后端 `weekOverWeekQuantity`/`weekOverWeekUnit`/`comparisonBaseAt`),仅单一基本单位且两侧快照齐全时才有值;缺基准、跨单位或后端未上线该字段时显示 `comparisonReason`(缺省“暂无上周同时点可比快照”),差值不得以 0 代替。 - 多基本单位只在卡片内按单位平铺小计(如“个 12,000 · kg 3,400”),**不跨基本单位加总**;单位数超过 3 个折叠为前 3 个并注明其余单位数;`units` 缺省或只有 1 个单位时不渲染小计行。 - 锁定数量单列展示:单一时显示“其中锁定 X 个”,多单位时逐个单位附锁定数;`lockedQuantity` 为 0 是真实零按“0”显示,为 null 表示口径缺失显示“—”。 - 文档中 B2 计数指向出库单,但下钻指定 V14 入总库申请,两类单据数据源不同。代码暂按文档保留“待出库单”计数与 V14 跳转;待业务确认后再统一口径。 - 安全库存预警的三色分级、缺口量/缺口金额、影响生产工单、防重复下单与超储调拨预填口径见 A1。**预警可用量与其它 KPI 不同源(含待检)**,共享片段 `batchRows`/`costRows` 不得改动,理由写在 A1。 当前只做 Vue 模板/脚本、MyBatis XML 与差异静态校验;实际数据对账和浏览器布局仍需在有授权数据的环境核验。 ## D1 出入库动态图 - 横轴窗口=**近 30 天滚动窗口(含今日,共 30 天)**,卡片副标必须写明窗口名与起止日期,不得显示成自然月或固定周期。 - 柱体=数量,Y 轴取基本单位;后端 `/wms/index/home-summary` 的 `trend` 只返回有作业的日期,缺的日期前端补 `null`、**绝不用 0 冒充**。 - 无作业日=当日入库与出库**都**无数据;只要任一方向有数据(含真实的 0)就不算无作业日。无作业日不画柱,给浅灰 `markArea` 底纹,hover 提示「该日无作业」,净趋势在该日断线。 - 净趋势=当日前端重算的「入库量 − 出库量」,紫色虚线 `#9B7DF7`,绑右侧第二 Y 轴「单日净差」,与左侧数量轴分开,读数不混用刻度。 - 连续同号风险:取窗口末尾最近连续同号段(末尾无作业日跳过),`>= 2` 天时提示——连续为正=积压风险(橙)、连续为负=缺货风险(红);0(持平)与中间的无作业日都打断连续段,没有连续段不提示。 - 点击柱体与无作业日都带该日 `startTime=00:00:00`/`endTime=23:59:59` 打开 V4 出入库动态;无作业日列表为空态,但日期照带。 - 卡片右上角「查看动态 ›」跳**系统菜单真实路由** `/warehouseManagement/inboundRequests`(出入库申请,独立页签),不是首页内嵌的 ioapp 视图、也不是自建的 V4 动态列表:`HomeOverview.vue` 发 `$emit('navigate', '/warehouseManagement/inboundRequests', { warehouseId })`,由 `dashboard.vue` 的 `@navigate="navigate"` 走 `$router.push`。只带当前仓库,状态/时间/页签全部交给目标页自己的默认值(`search.vue` 的 `defaultWhere.status=''` 即全部状态,默认入库页签);不得带 `dashboard=1`,否则会误开看板的批量收货勾选列。V4 仍由「最近作业动态 › 全部动态」、趋势柱体与无作业日、以及调拨行的「查看单据」进入,入口不丢。 - 口径实现集中在 `dashboard-utils.js`:`TREND_WINDOW_DAYS`、`trendWindowDates`、`trendQuantity`、`isIdleTrendDay`、`trendNetOf`、`buildTrendWindow`、`trendStreak`、`trendRisk`,由 `scripts/check-warehouse-dashboard.cjs` 静态断言守护。 ## D2 库存分类分布图 - 卡片副标固定写明「按物料分类 TOP8(前 7 + 其他兜底)· 按基本单位」;只做数量口径,不做「数量/金额」切换。 - 行数据来自后端 `categories`:`{ id, name, quantity, ratio }`。`id` 是物料分类根层级 `t_main_category_level.id`(字符串),可直接作为 V2 台账的 `categoryLevelId`;「其他」聚合行的 `id` 固定为字符串 `'other'`,不是任何真实分类层级。 - 点击非「其他」条形:`HomeOverview.categorySelect(row)` 发 `open('stockLedger', { categoryLevelId: row.id })`,`dashboard.vue` 的 `open()` 把 `extra` 经 `cleanQuery` 合并进路由 query(path 仍是 `/warehouseManagement/stockLedger`),台账页据此选中左侧树并过滤右侧列表。 - 点击「其他」:只打开台账、**不带分类**,绝不把聚合兜底行下钻成具体物料。 - 台账接受 `query.categoryLevelId`:树加载完成后在 `treeList` 中**递归**查找 id 匹配的节点,`$nextTick` 后 `treeRef.setCurrentKey(id)` 并展开祖先;`current` 置为 `{ id, data }` 的形状,同时满足 item-list 的 `current.id`(首屏 datasource)与 `current.data?.id`(reload)两种读取。 - 降级策略:树里找不到该 id(分类停用、无权限、树接口异常)时**不报错、不白屏**,清空选中回到台账默认的「不过滤」,并打印一条 `[wms-stock-ledger]` 说明日志;台账页日志前缀不得复用首页的 `[wms-home]`。 - tooltip 口径:分类名 / 在库数量(单位取首页「基本单位」筛选 `props.unit`,为空时不加单位后缀)/ 占比%,并提示「点击进入台账」;占比或数量缺失显示「—」,不以 0 冒充。 - 断言守护:`scripts/check-warehouse-dashboard.cjs` 覆盖「非 other 行带 `categoryLevelId`」「other 行不带」「`open('stockLedger', extra)` 把 query 传给 router」「台账页含 `categoryLevelId` 处理与递归查找」「tooltip 单位取 `props.unit`」。 ## A1 安全库存预警 本节固化安全库存预警改造后的唯一口径。后端实现在 `WmsHomeMapper.xml` 的 `alertRows` 片段;前端只做归一、配色兜底与文案,**不复算任何阈值**。回归守卫见 `scripts/check-warehouse-dashboard.cjs` 的 F1 断言块。 ### 触发条件与短路顺序 `alertRows` 对每个「(仓库,物料)」行按下列顺序判定,命中即停: 1. `unknown`(待核实):没有唯一的有效阈值配置(缺精确仓库配置、同仓同物料存在多条、阈值或基本单位不可核实),或可用量为负(锁定量超过余额)。 2. `critical`(缺料):可用量 < 安全库存 `secure_inventory`。 3. `warning`(偏低):安全库存 ≤ 可用量 < 安全库存 × K1。 4. `excess`(超储):可用量 > COALESCE(最大库存, 安全库存 × K2)。 5. `normal`(正常):其余全部。 - 系数:**K1=偏低系数** `wms.home.alert.warning-ratio`(默认 1.2)、**K2=超储兜底系数** `wms.home.alert.excess-ratio`(默认 3)。两者由 `WmsHomeAlertRatioConfig` 解析后以 Mapper 参数 `#{warningRatio}`/`#{excessRatio}` 下发,非法值回落默认并打 WARN。前端不得内置 1.2/3——写死会让改系数后前后端静默分叉。 - 最低库存 `min_inventory` **只作展示列,不参与任何触发判定**。用最低库存判缺料是本次废弃的旧口径,不得回退。 - **超储兜底只在最大库存为空时生效**:配了最大库存时超储线就是最大库存本身,`COALESCE` 的第二项不会被用到;把 K2 当成「最大库存之外再加一层」是错的。 ### 可用量口径(与 batchRows 不同源,必须长期并存) - 预警可用量在 `alertRows` 内一个**独立的 av 子查询**里算:`SUM(stock_count_base) − SUM(COALESCE(lock_count_base,0))`,按(仓库,物料)聚合,批次过滤条件与预警行集一致。**含待检、未放行批次**,不套 `pre_warehousing`/`quality_control` 过滤。 - 其它 KPI(库存总量、分类分布、库龄、周转)走 `batchRows` 投影的 `availableQuantity`,定义是 `available_count_base − COALESCE(lock_count_base,0)`,会把未放行/待检批次减掉。 - **为什么两套口径要并存**:预警判的是「这个物料还能不能发料」,锁定量才是真实占用,未放行批次仍在库里、后续会放行,把它当成不存在会漏报缺料;库存/库龄口径回答的是「现在可动用的量」,必须排除未放行。两者目的不同,强行统一只会让一边算错。 - **护栏:`batchRows` 与 `costRows` 片段不得改动。** `batchRows` 的 `availableQuantity` 被库存总量、分类分布、库龄、周转等四个既有消费者同时读取,`costRows` 是库存金额(C2)的单价定义;为了预警去改这两处,会静默改掉四个 KPI 的数字且没有任何报错。预警需要不同口径时只能新增独立子查询,不能复用或改写共享片段。 - 单价侧相反:`costUnitPriceRows`(从 `costRows` 抽出)与库存金额共享同一定义,缺价时保持 NULL,不兜底成 0。 - 断言:F1-(5) 直接读 `WmsHomeMapper.xml`,校验 `batchRows` 的 `available_count_base-COALESCE(b.lock_count_base,0)` 与 `alertRows` 的 `SUM(...)−SUM(...)` 同时存在,且 `alertRows` 内不得出现 `available_count_base`(出现即意味着预警复用了排除未放行的口径)。 ### 缺口量与缺口金额 - 缺口量 `gapQuantity = GREATEST(安全库存 − 可用量, 0)`,**只在 critical/warning 行有意义**,其余状态为 0。 - 缺口金额 `gapAmount = gapQuantity × 移动加权不含税单价`(单价取 `costUnitPriceRows`,与库存金额同源)。 - **缺价语义**:单价不可用时 `gapAmount` 为 **NULL** 并下发 `gapAmountReason`,前端显示「金额待补价」且打一条去重日志,**绝不用 0 冒充**——0 会被读成「缺口不值钱」,直接误导补货优先级。真实 0 照实显示 `0.00 元`;底层格式化层对缺失给「—」、对 0 给 `0.00`。 - 行文案统一由 `alertGapText` 生成:`缺口 <数量><单位> / <金额>`,缺价/缺口量缺失分别落到「金额待补价」与「—」。 ### 排序与 TOP5 - 预警列表与首页卡片共用同一条 `ORDER BY gapAmount IS NULL, gapAmount DESC, FIELD(status,'critical','warning','excess','normal','unknown'), id`:缺口金额倒序、缺价行(NULL)排最后、再按状态严重度、最后 id 稳定收尾(分页时同额同状态的行不会跳位)。显式写 `gapAmount IS NULL` 是为了让排序意图不依赖 MySQL DESC 的隐式 NULL 规则。 - 首页卡片只展示前 `ALERT_TOP_LIMIT = 5` 条;超出部分由 `buildAlertRows` 返回 `overflow` 并在控制台 warn 一次,`total` 仍是后端原始条数,右上角「全部 N 项」取 `alertCount`。 - **前端不重排**:`dashboard-utils` 的比较器只用于核对后端顺序、偏离时 warn,不改变渲染顺序——清单是分页的,页内重排会让「第 2 页第一条」错位。 ### 影响生产工单 - 口径:预警物料作为 **BOM 子件**,经 BOM 主表/明细关联到成品,再关联 `t_mes_work_order`;未完工定义固定为 `deleted=0 AND status IN (4 待生产, 5 生产中, 7 已延期)`,已完工(6)/待下达(8)/暂停(9)/终止(10) 都不算影响生产,不得放宽。 - 汇总条 `alertWorkOrderCount`=全部缺料(critical)行关联工单**跨行去重**后的计数(同一张工单被多个缺料物料影响只算一张);算不出时为 `null` + `alertWorkOrderCountReason`,前端显示「—」并给原因,**绝不当 0**。 - 行级 `workOrderCount` 用 `CASE WHEN status='critical' THEN ... END`:非缺料行为 **NULL 而不是 0**(0 会被读成「确定没有工单受影响」)。 - 下钻:点「影响生产工单」按 `level=critical` 打开缺料清单,保证数字与清单同源。 ### 已生成申请(防重复下单台账) - 行级 `purchaseRequestCode`/`purchaseRequestId` 来自 `t_wms_dashboard_command`(`command_type='PURCHASE'`,`command_key = warehouseId:categoryId`),`business_id` 用 `CAST AS CHAR`——大整数不得进 JS number。 - 列表侧 `detail.vue` 的 `alertActionOf` 优先级:`unknown`→「核对阈值」(**不给补货入口**,阈值不成立时补货只会被后端拒绝);`excess`→「发起调拨」;有 `purchaseRequestCode`→「打开采购需求」,**绝不出现「生成采购申请」**;critical/warning 且无单号→「生成采购申请」;`normal`→不给动作。 - 弹层侧 `DashboardActions.vue` 的 `purchaseLocked` 三重拦截:数量输入禁用、提交按钮禁用并改文案「已生成申请,禁止重复提交」、`submit` 内再拦一次并打 `[wms-home]` warn;后端 `hasReplenishmentAlert` 与新缺料口径同源,是最后一道。 ### 超储调拨预填 - 超储行的闭环是把多的调给缺的:目标仓库=**缺口最大的缺料行**所在仓库(只认 critical/warning,按 `gapAmount` 降序、缺价行最后,再按 `gapQuantity` 降序),目标库区=该仓第一个启用库区,并显式标注「**默认库区,请核对**」。 - 无缺料行时目标仓库与库区**留空**并给提示,不拿超储行自己的仓库/库区冒充目标。 ### 补货与阈值弹层 - 补货默认数量=`positiveQuantity(row.gapQuantity)`:非正数留空,**不得用安全库存或最大库存冒充缺口量**。 - 阈值弹层:最高库存**非必填**,留空提交 `maxInventory: null`;后端 insert/update 均 `jdbcType=DECIMAL` 无条件写,留空写 NULL(为空即走 ×K2 兜底)。 ### 汇总数字口径 - `alertCount` 只含 critical + warning + excess;`alertUnknownCount` 单列且不计入 `alertCount`(卡片显示「待核实 X 项(不计入 N 项)」)。 - `alertGapAmount` = Σ gapAmount;全缺价时后端给 `null` + `alertGapAmountReason`,前端显示「—」。 - 前端**不用可见行数自算合计**:自己加总只能得到「当前可见行」的合计,与清单页条数、缺价排除规则立刻分叉。F1-(1) 用「alerts 给 3 行但 alertCount 为 null → 显示「—」而不是 3」守住这条。 ### 上线前必须核对的数据库前置项 - `t_main_category_wms.max_inventory` **必须允许 NULL**。本口径依赖「最大库存为空 → 按安全库存 × K2 兜底」;列若是 `NOT NULL`,留空保存会直接失败,超储兜底这条分支永远触发不到。 - 本会话无数据库连接,**该列的 NULL 属性未能在本环境验证**,上线前必须在目标库核对: ```sql SHOW COLUMNS FROM t_main_category_wms LIKE 'max_inventory'; ``` - 判读:`Null` 必须是 `YES`。若为 `NO`,先做列可空迁移——用 `SHOW CREATE TABLE t_main_category_wms` 取现有精度与注释后,再执行形如 `ALTER TABLE t_main_category_wms MODIFY COLUMN max_inventory DECIMAL(24,4) NULL COMMENT '最高库存,为空表示未配置(超储判定按安全库存×K2 兜底)';` 的变更,否则「留空保存」整条不可用。 - 顺带核对:`secure_inventory`/`min_inventory` 的存在与可空性(阈值缺失会落到 `unknown`);`t_wms_dashboard_command` 上 `(tenant_id, command_type, command_key)` 是否有可用索引(防重复下单的查询走这条路)。 ### 断言守护清单 F1 断言块覆盖:statusLabel/color 优先级与三色兜底(normal 不造色)、缺价「金额待补价」而 0→「0.00 元」、TOP5 截断与 `overflow`、状态不复算、汇总条全 null→「—」且不自算合计、`alertActionOf` 五分支(含「有单号绝不出现生成采购申请」「unknown 不给补货入口」)、卡片模板守卫(三色点、影响生产工单、已生成申请、金额待补价)、`screens.alerts` 文案守卫(含待检/偏低系数/超储兜底,且不得再出现「排除未放行」)、后端 SQL 静态守卫(critical 判定行不得引用 minInventory、必须用 secureInventory、K1/K2 走参数、`gapAmount DESC`、batchRows 与 av 子查询两条护栏)。后端仓库不在同级目录时该组打印 SKIP 并说明原因,不判失败。 ## E3 最近作业动态 本节固化首页「最近作业动态」卡片的唯一口径。数据由 `/wms/index/home-summary` 的 `activities` 下发,取数在后端 `WmsHomeMapper.xml` 的 `recentActivities`(`WmsHomeMapper.xml:226-248`),前端归一在 `dashboard-utils.js` 的 E3 段(`dashboard-utils.js:597-925`),模板在 `HomeOverview.vue` 的 feed 块(`HomeOverview.vue:118-157`)。 ### 聚合范围与数据来源 - 只聚合四类:入库单、出库单(同为 `activityType=io`,按 `direction` 1/2 区分,`dashboard-utils.js:767-773`)、调拨单(`activityType=transfer`)、盘点任务(`activityType=inventory`,取自 `t_wms_plan_order`,`WmsHomeMapper.xml:240-246`)。入/出/调拨行来自 `ioRows` 共享片段(`WmsHomeMapper.xml:177-202`)。后端没给的类型不许在前端补一条。 - 「旧版五类动态说明不再适用」是本文件开头第 3 行的既有结论(「正文中旧版的六卡、库位地图和五类动态说明不再适用」)。含义有两层:不许按旧口径补第五类(质检、审批之类),也不许把旧五类的取数沿用过来;四类就是当前契约。 - 调拨的识别写在 SQL 里(`WmsHomeMapper.xml:235-236`:出库方向 `biz_type=2` 或入库方向 `biz_type=15`),前端只认后端给的 `activityType`,不得自己按 `businessType` 再猜一遍——两套判据迟早分叉。 - 时间窗:盘点分支只取近 30 天(`WmsHomeMapper.xml:246`),入/出/调拨沿用 `ioRows` 的 30 天滚动窗(`WmsHomeMapper.xml:196-197`),刷新时由服务层清空 `startTime`/`endTime`/`unit` 免得被首页筛选改写(`WmsIndexServiceImpl.java:1547-1550`)。 ### 已知限制:定义里的部分状态在当前契约下不可达 - `ioRows` 把「已完成」硬编码成主单审核通过(`AND o.verify_status=2`,`WmsHomeMapper.xml:190`)。该片段同时被 D1 趋势(`WmsHomeMapper.xml:221`)、V4 明细 io 页签(`WmsHomeMapper.xml:817`)、E3(`WmsHomeMapper.xml:237`)共用,是「已完成」的**唯一定义点**(`WmsHomeMapper.xml:166-171`)。因此入/出/调拨行恒为「已完成/绿」(`dashboard-utils.js:748-749`),盘点只有 `status` 0/1/2 → 待执行/进行中/已完成(`dashboard-utils.js:740-744`)。 - **结论:状态定义里的「待质检/待审批/已驳回」以及「超时角标」在当前契约下不可达。** 它们只存在于色表(`dashboard-utils.js:611-618`)与兜底分支(`dashboard-utils.js:751-756`),而兜底只在后端新增了未识别 `activityType` 且同时下发中文状态词时才生效。今天唯一可能被标超时的行是盘点 `status=0`「待执行」(`dashboard-utils.js:829-830`)。 - 前端已做前向兼容:状态词表 + neutral 中性回落(`dashboard-utils.js:608-619`、`621-626`),后端一旦扩展取数即可直接生效;**但不得为了凑出橙色/红色在前端伪造状态**——伪造的「待审批」会让用户去催一个不存在的审批,比不显示更糟。 - **未来若要覆盖这些状态,必须新增 E3 专用查询,绝不能改 `ioRows`。** 动共享片段等于静默改掉 D1 的柱子和 V4 明细的数字,且一条报错都不会有;同理调拨剔除只能留在 trend 语句(`WmsHomeMapper.xml:212-213`),不能下沉进片段。 ### 四段结构与对应纯函数 - ①时间(`formatActivityTime`,`dashboard-utils.js:711-720`):今日 `今日 HH:mm`(`:717`)、昨日 `昨日`(`:718`)、更早 `MM-DD`(`:719`);时间非法或缺失一律 `—`(`:713`)并排到最后(`:872`),**绝不用「现在」兜底**——那会把一条陈年流水顶到列表最前面。hover 给完整时间(`activityTimeTitle`,`:724-730`)。 - ②单据类型与单号(`activityDocTypeOf`,`dashboard-utils.js:762-775`;模板 `HomeOverview.vue:142-151`):类型名固定「入库单/出库单/调拨单/盘点任务」,未识别回落「作业单」而不是猜成四类之一(猜错会让用户点进错误的单据类型);单号缺失显示「单号待核实」(`HomeOverview.vue:150`)。单号段单独可点,用 `@click.prevent.stop`(`HomeOverview.vue:148`)——少了 `.stop` 会冒泡到整行的 `@click`(`HomeOverview.vue:138`),一次点击触发两次下钻。 - ③动作描述(`activityActionTextOf`,`dashboard-utils.js:785-815`):按类型套模板,见下一节。 - ④状态(`activityStatusOf`,`dashboard-utils.js:737-757`):渲染在 `HomeOverview.vue:153`,超时角标同格追加。 ### 动作描述模板的两条硬约束 - **盘点行不得渲染数量。** 后端盘点分支把 `itemCount` 写成占位常量 `1`(`WmsHomeMapper.xml:241`,该行第 4 个投影值),它既不是盘点明细行数也不是盘点数量;当数量渲染会得到「盘点 1 项」这种伪造数字。前端对 `inventory` 直接返回 null(`dashboard-utils.js:691`),盘点只拼「盘点 · 库区(· 进度 xx%)」(`:798-814`)。盘点行的 `description` 是状态词(服务层按 `status` 拼「已完成/执行中/待处理」,`WmsIndexServiceImpl.java:1576-1578`),属于第四段,绝不能当动作文案(`dashboard-utils.js:793-796`),否则一行会读成「单号 → 已完成」。 - **数量缺失不写「0 项」,进度为 0 或缺失不拼「0%」。** `activityItemCount` 只认有限且 >0 的数(`dashboard-utils.js:688-694`),`activityProgressOf` 对 0、负数、非数字一律返回空串(`:699-707`)。0 会被读成「这一单没有任何明细」「一点都没做」,与「不知道」是两件事;与 C2 缺价、E1 unknown 同一条铁律——不可核实就省略,绝不用 0 冒充。 ### 状态着色 - 五档:done 绿 / waiting 橙 / doing 青 / danger 红 / neutral 灰(`dashboard-theme.scss:155-159`,基础色见 `:142-144`)。文案与颜色解耦:颜色只认 `ACTIVITY_STATUS_STYLES`(`dashboard-utils.js:608-619`),改配色不用改文案,模板里也不许判业务状态。 - neutral 的用途:**未识别状态的兜底**,文案「状态待核实」(`dashboard-utils.js:625-626`)。为什么不能用 done 或 waiting:done 是绿,会被直接读成「已完成」这个业务结论;waiting 是橙,会被读成「在等谁处理」,还会被套上超时角标(`:829-830`),等于替后端凭空造出一个待办。灰只表达「这里有个状态,前端不认识」,与 E1 库龄 unknown、F1 预警「待核实」是同一种视觉语言;样式见 `dashboard-theme.scss:159`。 - 推导规则(唯一来源,组件与模板不得再内联判断):盘点按 `status` 0/1/2 → 待执行/进行中/已完成(`dashboard-utils.js:740-744`);调拨恒「已执行」done(`:748`);出入库恒「已完成」done(`:749`)。 - **必须先按 `activityType` 分流再判 `status`。** io/transfer 的 `status` 恒为 `NULL`(`WmsHomeMapper.xml:234`),而 `Number(null) === 0`,先判 status 会把「已完成」显示成「待执行」(`dashboard-utils.js:735-736`)。未识别的类型或状态值只 warn 一次并回落中性灰(`:745-746`、`:750`、`:756`),不得静默当成 done。 ### 超时角标 - 只有两条同时成立才标:状态是橙色「待*」(`tone === 'waiting'`)且事件时间早于 now−24h(`buildActivityItem`,`dashboard-utils.js:827-830`);阈值 `ACTIVITY_OVERDUE_HOURS = 24`(`:603`)。 - 24 小时与待办条同源:待办条的 overdue 就是 `DATE_ADD(a.create_time, INTERVAL 24 HOUR) < NOW()`(`WmsHomeMapper.xml:509`,入总库申请分支同式 `:532`;盘点行固定不标,`:558`)。**不许在 E3 另立 12h/48h**——同一屏上的两个「超时」不能是两个定义。 - done / doing / danger 不标(`dashboard-utils.js:827`):绿/青/红套「超时」没有业务含义,别给已完成的单据戴超时帽子。 - 纯函数计算、幂等:由 `operateTime` 与 `now` 现算(`:828-830`),重复求值不会因为「已经标过一次」而漂移;**不得做成「标一次就记住」的有状态标记**。 ### 排序、条数与去重 - 去重:同一单据(`orderId`,缺失回落 `id`,两者都没有用行下标兜底)只留事件时间最新一条(`buildActivityFeed`,`dashboard-utils.js:853-867`);同刻不覆盖(`:865-866`)。后端已做 `GROUP BY orderId`(`WmsHomeMapper.xml:238`),但前端不能假设这个口径永远不变。 - 排序:事件时间倒序;同刻按行 key 倒序(`dashboard-utils.js:868-879`),因此顺序确定、可复现——稳定排序才能让刷新后行不跳位。解析不出时间的行排最后(`:872`)。 - 条数:默认 5 条(`ACTIVITY_LIMIT = 5`,`dashboard-utils.js:601`;前端截断 `:880`;后端 `LIMIT #{q.limit}` 取同一值,`WmsHomeMapper.xml:247` 与 `WmsIndexServiceImpl.java:1546`)。**双保险的理由**:前端还要按 `orderId` 再去重一次,去重后条数可能变少;后端口径若调整也可能变多,只靠后端 LIMIT 保不住「卡片正好 5 条」这个视觉契约(`:598-600`)。多出来的条数进 `overflow`(`:881`),不静默丢弃。 - 「全部动态 ›」下钻 V4 出入库动态:`HomeOverview.vue:128` 发 `open('io')`,`dashboard.vue:84-101` 落到 io 屏(`dashboard-screens.js:8`)。整行与单号点击走 `openActivity`(`HomeOverview.vue:701-705`)→ `dashboard.vue:214-216` 按 `activityType` 分流:盘点→盘点工单详情、调拨→V4 带单号、io→入/出库单据详情;缺 `orderId` 时提示核实,不许硬跳到列表首页。 ### 刷新与高亮 - 30 秒轮询:`dashboard.vue:170` 的 `setInterval(() => this.refreshPart('activities'), 30*1000)`,只刷 `activities` 这一片,不整页重算(`:164-172`)。 - 新事件只加 `is-new` 类做 3 秒渐隐(`HomeOverview.vue:135` + `dashboard-theme.scss:162-163`),JS 只负责到点摘类(`HomeOverview.vue:679-682`)。**不移动 DOM、不调用任何滚动 API**:30 秒轮询里主动 `scrollIntoView`/`scrollTop` 会把用户正在看的位置顶走(`HomeOverview.vue:668-669`、`dashboard-theme.scss:160-161`)。 - 首屏不整屏高亮:`diffActivityIds` 在 prev 为空(首次加载)时返回空数组(`dashboard-utils.js:919`),`HomeOverview.vue:670-673` 因此天然不触发——整屏闪一下等于什么都没强调。 - 差分只按行 key 比较,绝不按数组引用(`dashboard-utils.js:920-924`):`activities` 是计算属性,每次求值都返回新数组,引用比较会让 30 秒轮询把同一批行全判成新增、整屏反复闪。 - 连续两批新增重置定时器:`HomeOverview.vue:675` 先 `clearTimeout` 再起新的 3 秒,只保留最后一轮,避免先到的一批提前被摘掉;组件销毁时同一处清理(`:685-688`)。 ### 空态 - 无数据时固定文案「今日暂无作业动态」(`HomeOverview.vue:156`),**禁止空白、禁止只画一个空列表**;有 `activityReason` 时追加「 · 原因」(同处),原因由数据侧给,前端不自造。 - 注意:后端目前不下发 `activityReason`(`WmsHomeMapper.xml:226-248` 无此列,服务层也未拼),该字段实际只在刷新失败时被写入错误信息(`dashboard.vue:158`、`:161`)。所以今天的空态基本就是这一句话,不许为它编造「网络异常」之类的原因。 ### 排查日志清单 - `[wms-home] E3 最近作业动态已解析`(`dashboard-utils.js:907`):模块级快照去重(`:642`、`:902-906`),只在「总条数/去重数/四类分布/待*数/超时数」任一变化时打一条;每次都打会把控制台刷满、反而看不见真实变化。 - `[wms-home] E3 最近作业动态状态未识别`(`:636`):按「类别 + 值」去重,同一句 warn 只打一次(`:631-637`),是发现后端口径漂移的第一信号。 - `[wms-home] E3 最近作业动态下钻`(`HomeOverview.vue:703`):只记 `id / activityType / orderNo / direction`。 - `[wms-home] E3 最近作业动态新增事件`(`HomeOverview.vue:677`):只记新增 id 与条数。 - **禁止打印整行数据或整包响应**(`dashboard-utils.js:641`):日志只放聚合数字与关键字段,不 `console.log(row)`、不打印 `summary`;长列表会淹掉控制台,单号也是敏感数据。日志前缀必须是 `[wms-home]`,其它页面不得复用。 ### 断言守护 E3 的关键口径由 `scripts/check-warehouse-dashboard.cjs` 的 E3 断言块守护,写法与同文件里 D1、E1、F1 各块一致(`check-warehouse-dashboard.cjs:554`、`:804`、结尾 F1-(5) 段)。被守护的关键点: - 时间四段文案、「—」兜底、不得出现「现在」兜底; - 盘点不渲染数量、数量缺失不写「0 项」、进度 0 或缺失不拼「0%」; - 状态推导先按 `activityType` 分流再判 `status`,`Number(null)===0` 的错位必须锁死; - 五档 tone 与 neutral 兜底、未识别不得落成 done; - 超时只标 waiting 且超过 24h,done/doing/danger 不标; - 去重、事件时间倒序、同刻 key 倒序、5 条截断与 `overflow`; - `diffActivityIds` 首屏返回空、按 id 比较不按引用; - 模板守卫:`@click.prevent.stop`、`is-new` 类、空态文案; - 后端 SQL 静态守卫:`ioRows` 的 `verify_status=2` 唯一门槛、盘点分支 `itemCount` 常量、`LIMIT` 与 24h 口径。 **不写死断言条数**:断言块会随口径增补继续加,条数写进文档立刻过期,以脚本实际执行为准。 ### 未验证边界 - 浏览器实际渲染未核验:`.feed-doc` 是 `inline-flex` + `text-overflow:ellipsis`(`dashboard-theme.scss:148`),而 ellipsis 作用于块容器的行内内容,对 flex 容器里的裸文本节点(`{{ item.docType }}`,`HomeOverview.vue:143`)未必生效;超长单号(`.feed-no`,`dashboard-theme.scss:149-150`)在 `overflow:hidden` 下可能被硬切而没有省略号。当前只有静态断言,真实宽度下的表现必须在浏览器里看一眼。 - 真实 `summary.activities` 数据未对账:本环境无数据库连接,四类分布、`itemCount`、`status` 的实际取值只能靠联调确认,上述推断全部来自 Mapper 与前端代码。 - 已知次要不一致(防御性路径,当前后端不可达):`activityActionTextOf`(`dashboard-utils.js:785-814`)在 `activityType=io` 且 `direction` 既非 1 也非 2 时把动作词兜底成「入库」,而 `activityDocTypeOf`(`:762-774`)对同一情况兜底成「出入库单」并 warn 一次。`ioRows` 的 `direction` 取自 `t_wms_snapshot.out_type`,正常数据非空,只有历史脏值可能命中;命中时同一行会读成「出入库单 RK… · 入库 N 项」。若以后要覆盖,应让两处共用一个方向判定助手,不要各自兜底。 - `progressText` 生产环境恒为空:后端 `recentActivities` 不下发进度字段(`WmsHomeMapper.xml:226-248`),前端是防御性读取(`dashboard-utils.js:696-707`)。因此「进度 xx%」今天不会出现,只有后端补了进度列才生效——**不得为了让它显示而在前端算一个进度**。