520a427b → ded04ef8本增量属于「库存域残留批 1b」+「D61 §5.5 续开」两块能力的收口,主线是把服务端已定稿的口径搬到前端与 mock:
| 模块 | 关键变更 | 文件 |
|---|---|---|
| 入库凭证数组化(#176 → §7.8.2) | 单图 image_url → 1–9 张 image_urls;新增九宫格上传组件;详情抽屉九宫格只读 | src/pages/stock/photoGrid.tsx(新)、src/api/stock.ts、src/api/stock.mock.ts、src/pages/stock/in/index.tsx、src/pages/stock/shared.tsx |
| 库存查询在途显示(D6b) | 在途列改用服务端下发基本单位显示串 transit_in_display/transit_out_display;缺失降级裸数字(不拼 unit) | src/pages/stock/context.ts、src/pages/stock/list/index.tsx、src/api/stock.mock.ts |
| 调拨在途时长(§22.2.3) | 时长列/详情/工作台 widget 改用服务端 transit_text;缺失降级本地时长串 | src/api/stock.ts、src/pages/stock/transfer/rules.ts、transfer/index.tsx、src/widgets/stock-transfer/index.tsx |
| 出库台账筛选(PRD §13) | 新增 type 列/筛选 + 开关;新增 参数转换 |
口径:base 有、head 无(已修复/已收敛)。
src/api/stock.mock.ts:552src/api/stock.mock.ts:552src/api/stock.mock.ts:597src/api/stock.mock.ts:820src/api/stock.mock.ts:893src/api/stock.ts:179src/api/stock.ts:232src/pages/pos/workbench/index.tsx:1055(结果页 JSX 抽出为 OrderResultPanel 后消除)src/pages/stock/out/index.tsx:323src/pages/stock/out/index.tsx:404src/pages/stock/out/index.tsx:404src/pages/stock/out/index.tsx:404src/pages/stock/out/index.tsx:404src/pages/stock/shared.tsx:424口径:head 有、base 无(本次新增/由拆分造成)。多数为复杂度与重复代码类,不影响功能,但建议在下个批次收敛。
src/api/stock.mock.ts:574(count=19)src/api/stock.mock.ts:574src/api/stock.mock.ts:623(count=19)src/api/stock.mock.ts:846(count=39)src/api/stock.mock.ts:989(count=20)src/pages/stock/out/index.tsx:452(CreateStockOutButton,count=17)src/pages/stock/out/index.tsx:452src/pages/stock/out/index.tsx:452(CreateStockOutButton 159 行)src/pages/stock/photoGrid.tsx:68(StockInPhotoGrid,count=24)src/pages/stock/photoGrid.tsx:68src/pages/stock/photoGrid.tsx:68(196 行)src/pages/pos/workbench/__tests__/continueSales.test.tsx:119src/api/__tests__/stock.contract.test.ts:1(直接依赖 11 个函数)src/pages/stock/photoGrid.tsx:68(StockInPhotoGrid fan-out 22)src/pages/stock/photoGrid.tsx:68(直接依赖 22 个函数)src/api/stock.ts:213(3 处重复)src/api/stock.ts:323src/pages/pos/workbench/OrderResultPanel.tsx:57src/pages/pos/workbench/__tests__/continueSales.test.tsx:33src/pages/pos/workbench/__tests__/continueSales.test.tsx:153src/pages/pos/workbench/__tests__/continueSales.test.tsx:184src/pages/pos/workbench/index.tsx:1001src/pages/shared/orderStatus.ts:34src/pages/stock/out/index.tsx:452(私有函数用了导出名风格)点评:
StockInPhotoGrid(196 行 / 复杂度 24 / 依赖 22)与 mock 中fetchStocks/fetchStockOuts的复杂度抬升,主要来自把「上传 + 预览 + 删除 + 只读」四态塞进一个组件、以及 mock 里不断叠加聚合逻辑。功能正确,但建议下批把上传逻辑抽useStockInPhotoUploadhook、把只读渲染拆为StockInPhotoGridReadOnly。
以下是人工阅读变更代码后的补充发现(非工具 issues)。已按影响面从高到低排列。
wholeDisplay,数量 ≥ 整件换算量时输出包装单位,违背 D6b「耗材=次」契约src/api/stock.mock.ts:894-896(transit_out_display / transit_in_display)调用 wholeDisplay(src/api/stock.mock.ts:386-395)wholeDisplay(200, 耗材 product#1 base_unit_qty=100) 返回 "2 支"(余数为 0 时整件段独占),wholeDisplay(150, ...) 返回 "1 支 50 次"。而 StockRow.transit_*_display 的契约注释、库存列表 Tooltip 明确写「数量按基本单位显示(耗材=次,服务端下发显示串)」,恰好是要避免「把 200 次写成 2 支」的误导。VITE_AUTH_MOCK=1 下的前端联调/演示数据源,QA 与演示会看到包装单位,与真实 server 口径不一致;契约测试只用了 50/10(< base_unit_qty)所以未暴露。N 次;零售/仪器 N unit),并为「在途数量 ≥ base_unit_qty」补一条契约断言。useResolvedPhotoUrls 用 Promise.all,单张预签名失败会让整组缩略图退化为占位src/pages/stock/photoGrid.tsx:46-62Promise.all(keys.map(fetchFileDownloadUrl)) 任一张抛错即整体 reject → query.data 为 undefined → resolved = {},所有已有照片都显示 FileImageOutlined 占位。而同一处注释承诺「单张失败只回退占位(不阻塞其余照片展示)」。Promise.allSettled 或逐张 try/catch,失败项不写入结果 map(保留占位),成功项照常展示。stockInPhotoUrls 对空数组不回落(?? 只处理 null/undefined)src/api/stock.ts:116-124const list = row.imageUrls ?? row.image_urls。若详情接口下发 imageUrls: [](空数组而非 null),同时列表字段 image_urls 有值,?? 不会回落到 image_urls,导致照片被误判为「无照片」。StockDocDrawer 传的是列表行(imageUrls 为 undefined),暂不触发,属潜在隐患。const list = (row.imageUrls?.length ? row.imageUrls : row.image_urls)。value 覆盖,可能把已删照片加回src/pages/stock/photoGrid.tsx handleFiles(约 L94-145)与 removeAt(约 L147-160)handleFiles 在开始时快照 const nextKeys = [...value],await 上传完成后再 onChange(nextKeys)。若期间用户删除了一张既有照片,父级 photoKeys 已变短,但完成回调会写回「旧列表 + 新上传」,已删的照片被重新加回(且 remain 也按旧 value 计算)。editable && !uploading 才渲染删除入口),或在完成回调里用 setState(prev => merge(prev, addedKeys)) 的函数式更新而非闭包快照。image_url,未适配入库凭证 1–9 张数组化src/components/import/ImportWizard.tsx:345(TransferPhotoUpload bizType="stock-in" 单图)+ 约 L262 createImportBatch({ ... image_url: isStockInitial ? photoKey! : undefined })image_urls,但「期初库存导入」生成的「期初导入」入库单仍只带一张 legacy image_url;TransferPhotoUpload 头注释也仍写「入库页同样复用」(实际入库页已改用 StockInPhotoGrid)。StockInPhotoGrid 并把 image_urls 透传到 createImportBatch;若暂不纳入,至少在注释与页面上写明「导入路径暂用单图」,并清理 transfer/photo.tsx 的过期注释。customerId 不会重新预选src/pages/pos/workbench/index.tsx presetAppliedRef(约 L564-577)presetAppliedRef.current 一旦置 true 就不再应用 URL 客户参数。这是为了「更换客户后不被带参查询顶回来」的正确取舍;但副作用是:若停留在同一 kind 的工作台(WorkbenchInner key={kind} 未重挂载)时再次通过带参 URL 导航到另一个 customerId(如先后从两张服务单详情点「继续开销售单」),新的客户不会被预选。useRef<number | null>,变化时才重置;或让 ContinueSalesOrderButton 导航时追加一个非 kind 的 key(如 x=<timestamp>)触发重挂载,或在 WorkbenchInner 的 key 中加入 customerId。src/pages/stock/photoGrid.tsx(隐藏 <input type="file"> 无条件渲染)aria-label="入库凭证照片文件选择" 的 input(onChange 因 !onChange 直接 return)。{editable && <input ... />}。stockOutQueryParams:store_id → store、空筛选项不下发、excludeAuto 仅 true 时下发,与契约测试断言一致。fetchStockOuts:type 用归一化类型(手动/自动/期初)、excludeAuto 排除自动出库、total 与过滤同口径;purpose 与 type 组合过滤逻辑正确。transitDisplay:显示串优先 → 裸数字降级 → 无在途 —;未拼 unit,符合「不二次拼包装单位」要求。transferTransitText:transit_text 优先且原样展示、空串降级本地时长、非开放态且无串返回 null;列表/详情/widget 三处共用,口径一致。canContinueSalesOrder:仅「服务单」且状态 ∈ {待服务, 服务中, 已完成};销售单/待支付不出现,符合 D61 §5.5(销售单不出反向入口)。if (result && customer):doSubmit 保证有 customer 才会 setResult,guard 不会造成结果页丢失。StockInPhotoGrid:满 9 张硬拦、超选只取剩余额度、删除可重拍、只读态无操作入口,与新增单测一致。photoGrid 上传链路使用 uploadFileViaPresign({name,size,mime,blob,biz_type:'stock-in'}),与 files.ts 签名一致;失败逐张提示且不落 value。type 三值 + excludeAuto 开关 + purpose 组合;确认 total 与列表同口径;确认「产品」Tab 不受筛选影响;「重置」恢复全量。transit_text 缺失降级、超时标注「(超时)」。?kind=销售单&customerId= 进入工作台客户已预选;同 kind 二次带参导航(对应 P3)。transit_text、type/excludeAuto、续开显隐、凭证数组化和历史回退)与 PRD 一致,未发现阻断级缺陷。StockInPhotoGrid 与 mock 聚合函数复杂度偏高,建议下批拆分;同时清理 transfer/photo.tsx 过期注释、统一 imageUrls/image_urls 回落策略。excludeAutostockOutQueryParamssrc/api/stock.ts、src/pages/stock/out/index.tsx、src/pages/stock/context.ts |
| 续开(D61 §5.5) | 服务单收款完成/完结后可「继续开销售单」;结果页抽为 OrderResultPanel;工作台按 customerId 预选客户 | src/components/ContinueSalesOrderButton.tsx(新)、OrderResultPanel.tsx(新)、OrderRecordDrawer.tsx、src/pages/pos/workbench/index.tsx、src/pages/shared/orderStatus.ts |
| 测试 | 新增 6 个单测/契约测试覆盖上述口径 | src/**/__tests__/*、stock.contract.test.ts |