diff --git a/AGENTS.md b/AGENTS.md index 36f4458..de970b5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -73,11 +73,12 @@ ## UI 与交互 +- 后续客户端新增、新建、下单操作统一执行 [Client 界面样式规范](../../docs/Client界面样式规范.md) 的“新增页面”章节,作为当前通用外观依据;列表按同文列表条款。只读取本轮适用章节,不因规范更新批量改造其他操作或公共外框。 - 视觉还原、布局/交互承载方式变更或真实页面验收时读取 `../../docs/Admin-Client前端原型还原验收规则.md`,只为受影响操作形成原型核对矩阵;纯文案、逻辑或测试修改不自动读取。自动检查与人工视觉验收分开报告,无视觉变化时标记“无需视觉验收”。 -- 已向客户讲解或确认过的产品原型是页面视觉与布局实现的直接事实来源。字段顺序、字段分组、字段摆放、列宽、控件宽高、对齐方式、换行方式、区块层级、上下左右边距、内边距、间隙、留白、圆角、边框、阴影、背景色、文字颜色和字号均必须按原型还原,不得只参考大致风格后自行重新排版。 +- 已向客户讲解或确认过的产品原型是页面字段、分组、摆放及承载方式的直接依据;新增页的通用外观以当前 Client 样式规范明确替代的条款为准。未被替代的列宽、控件宽高、对齐方式、换行方式、区块层级、边距、圆角、边框、阴影、背景色、文字颜色和字号仍按原型还原,不得只参考大致风格后自行重新排版。 - 原型中的按钮位置、顺序、尺寸、圆角、边框、背景色、文字色、图标、悬停态、激活态、禁用态和危险语义必须直接按原型实现;不得擅自改为文字链接、不同颜色、不同按钮类型、折叠菜单或自创交互效果,也不得增加原型没有的上浮、缩放、渐变线、装饰图标或其他视觉元素。 - 原型已经给出明确图标、字符图标、图片资源或按钮内图标时,直接使用原型中的同一方案和资源,不得以 Lucide、Element Plus 默认图标、手绘图标或个人偏好替换。仅当原型没有定义对应图标时,才按本节的默认图标语义补齐。 -- 上述原型还原规则优先于本文件中“延续现有视觉体系”、默认组件样式和默认图标库等一般性规则。只有用户当前明确要求、阶段范围、真实字段长度、响应式、可访问性或接口事实与原型冲突时才允许做最小调整,并须在修改前说明具体差异、原因和影响范围;不得以技术实现方便为由偏离原型。 +- 当前 Client 样式规范明确替代的外观及上述原型还原规则,优先于本文件中“延续现有视觉体系”、默认组件样式和默认图标库等一般性规则。后续用户明确确认的例外优先;阶段范围、真实字段长度、响应式、可访问性或接口事实与原型冲突时只做最小调整,并须在修改前说明具体差异、原因和影响范围,不得以技术实现方便为由偏离。 - 详情、新增、编辑、审核等操作采用独立页面、居中弹框、侧滑面板或其他承载方式,必须逐页面按对应原型确认,不得全局写死为某一种容器。原型为独立页面时使用正式路由和页面布局;原型为居中弹框时使用模态弹框;只有原型明确为侧滑面板时才使用抽屉。不得因为已有公共组件或改动方便,把原型中的独立页面改成弹框、把弹框改成抽屉,或反向替换。 - 延续现有客户工作台的可信、简洁和服务型视觉语言,与内部管理后台保持品牌关联但不照搬高密度管理界面。 - 优先使用 Element Plus 和 Lucide 图标;不使用表情符号代替功能图标,不引入第二套组件库。 diff --git a/docs/business-index.md b/docs/business-index.md index 44898eb..f98f01a 100644 --- a/docs/business-index.md +++ b/docs/business-index.md @@ -23,10 +23,10 @@ | 统一订单展示 | `src/modules/order-management/` | `src/modules/first-leg/` 与对应后端订单契约 | | 客户财务账单、入仓码联查、收款、未收余额与客户确认 | `src/modules/finance/`;确认与明细入口 `views/FinanceBillsView.vue`、`api/finance.ts`,入仓码通过 `src/modules/first-leg/public.ts` 复用 `InboundCodeAssociationDialog` | Backend `finance/`;账单行只显示服务端 `inboundCode` 并按登录客户查询业务关系,缺失显示“—”,不得回退内部 ID。收款方式由服务端枚举映射中文。出账或已出账调整后为 `PENDING_CONFIRMATION`,客户确认提交登录成员、`expectedVersion` 和幂等键 | | 客户资金账户、授信与充值申请 | `src/modules/finance/views/FinanceAccountView.vue`、`api/account.ts` | 显示当前企业 CNY 实际余额、冻结金额、授信额度、授信有效期、已用授信、冻结授信、总可用余额和不可变流水;数据加载前显示“—”,不得先伪装为零。授信到期日当天仍有效,次日已用授信并入实际余额,负数实际余额会阻止提交新订单。充值只展示服务端启用的平台统一静态收款码,客户上传 JPG/PNG 付款截图后进入待审核。客户范围只取登录态,无收款码时禁止提交,不模拟支付成功 | -| 海外采购 | `src/modules/cross-border/routes.ts`、`views/ProcurementView.vue`、`components/ProcurementDetailPanel.vue`、`api/procurement.ts` | 独立列表、新建及详情路由;客户范围只取 Token,创建的 `customerName` 只取认证会话企业名称。详情消费独立客户投影,展示履约、已确认箱明细、实际运单和费用结果,不开放内部文件或平台操作。Backend `lastleg-service/procurement/` | +| 海外采购 | `src/modules/cross-border/routes.ts`、`views/ProcurementView.vue`、`components/ProcurementDetailPanel.vue`、`api/procurement.ts` | 独立列表、新建及详情路由;客户范围只取 Token,创建的 `customerName` 只取认证会话企业名称。七状态简化流程展示分批收货、分批出库与费用,待客户确认时提供当前客户确认动作;不开放内部文件或平台操作。Backend `lastleg-service/procurement/`;规则见 `baiyue-firstleg/lastleg-service/docs/procurement-workflow.md`(工作区路径) | | 美国本土卡与美本土散板 | `src/modules/cross-border/routes.ts`、`components/BusinessOrdersView.vue`、`api/businessOrders.ts` | 两个运输业务共用列表/表单组件,客户范围只取 Token;不消费采购字段或状态。Backend `lastleg-service/` 的 `trucking/`、`looseboard/` 客户接口 | | 海外仓入库订单 | `src/modules/order-management/routes.ts`、`views/InboundOrdersView.vue`、`api/inboundPlans.ts` | `orders/inbound` 为只读列表/详情,权限 `merchant:warehouse:inbound:list`;不展示或提交内部 ID | -| 海外仓贴标、仓储转运、货物废弃 | `src/modules/overseas-warehouse/routes.ts`、`views/WarehouseServiceView.vue`、`api/warehouseServices.ts` | 三个路由均为 `active`;退货预报另见本节独立入口;贴标申请先选国外中转仓和有库存历史入仓单,换标类型为只换 SKU、只换箱唛、SKU+箱唛。涉及箱唛换标时,整张贴标申请只选择一个 FBA 目的仓,全部箱级明细统一提交该目的仓,模板和明细不再逐行选择目的仓。箱唛换标明细一行固定代表一箱,箱数不可编辑,提交也固定为 1。货物明细外层始终提供“SKU 关联”“箱号关联”“SKU 混箱换标”三套模板的下载、上传,不随换标类型切换;上传只进入待导入关系区,客户可勾选部分或全选后导入本次明细,只有进入明细的库存才随申请提交和冻结。明细也支持手动新增编辑,组合换标以仓库唛头连接旧/新 SKU 和新 FBA 箱唛关系。涉及箱唛换标时必须选择卡车、自提或快递;快递可选择打印面单并上传 PDF/图片。PDA 逐箱换标、上传照片并由仓库复核后,客户详情展示完工照片和“换标完成确认”;客户确认前不得继续出库,确认后卡车进入预排车,自提/快递进入换标出库待创建。历史库存位置不能解释成箱唛与具体托盘快照,客户页面不暴露托盘、库位和内部库存 ID | +| 海外仓贴标、仓储转运、货物废弃 | `src/modules/overseas-warehouse/routes.ts`、`views/WarehouseServiceView.vue`、`api/warehouseServices.ts` | 三个路由均为 `active`;退货预报另见本节独立入口;贴标申请先选国外中转仓和有库存历史入仓单,换标类型为只换 SKU、只换箱唛、SKU+箱唛。涉及箱唛换标时,整张贴标申请只选择一个 FBA 目的仓,全部箱级明细统一提交该目的仓,模板和明细不再逐行选择目的仓。箱唛换标明细一行固定代表一箱,箱数不可编辑,提交也固定为 1。货物明细外层提供统一贴标明细模板,使用“贴标类型”列区分只换箱唛、只换 SKU 标签和 SKU+箱唛三种模式;上传只进入待导入关系区,客户可勾选部分或全选后导入本次明细,同一份文件不得混用多种类型,只有进入明细的库存才随申请提交和冻结。明细也支持手动新增编辑,组合换标以仓库唛头连接旧/新 SKU 和新 FBA 箱唛关系。涉及箱唛换标时必须选择卡车、自提或快递;快递可选择打印面单并上传 PDF/图片。PDA 逐箱换标、上传照片并由仓库复核后,客户详情展示完工照片和“换标完成确认”;客户确认前不得继续出库,确认后卡车进入预排车,自提/快递进入换标出库待创建。历史库存位置不能解释成箱唛与具体托盘快照,客户页面不暴露托盘、库位和内部库存 ID | ## Client 全量验收快速索引 @@ -38,3 +38,5 @@ | 履约、海外仓与财务 | 入库计费、柜次进度、船期、海外仓贴标/转运/废弃/退货换标、消息和客户账单 | `src/modules/first-leg/`、`overseas-warehouse/`、`finance/`、`notifications/`;按服务端权限核对客户可见字段、客户隔离和只读/写入边界 | 客户侧边界:仍以服务端权限和客户可见字段为准;PDA、平台后台操作和内部证据不因全量验收自动暴露到 Client。财务按 `../../docs/财务管理/` 与 `src/modules/finance/routes.ts` 定位。 + +- 货物废弃:有箱唛按具体箱选择,备注选填;无箱唛但有托盘时只填申请总箱数与必填备注,预留相关托盘供PDA扫描现场箱唛自动计数。详情展示废弃备注、托盘、实际废弃箱唛与实际数量;现场扫描清单与申请选箱快照分别展示。 diff --git a/src/auto-imports.d.ts b/src/auto-imports.d.ts index 9d24007..e16d21f 100644 --- a/src/auto-imports.d.ts +++ b/src/auto-imports.d.ts @@ -6,5 +6,5 @@ // biome-ignore lint: disable export {} declare global { - + const ElMessageBox: typeof import('element-plus/es').ElMessageBox } diff --git a/src/components/ClientSidebarMenu.vue b/src/components/ClientSidebarMenu.vue index 67e3bd8..ba8a240 100644 --- a/src/components/ClientSidebarMenu.vue +++ b/src/components/ClientSidebarMenu.vue @@ -5,6 +5,7 @@ import { clientNavigation, filterNavigationByPermissions, getNavigationParentKeys, + getNavigationLeaves, } from '@/navigation/clientNavigation' import { useAuthStore } from '@/modules/auth/public' import { useCustomsPendingStore } from '@/modules/first-leg/public' @@ -40,8 +41,11 @@ onBeforeUnmount(() => { }) const visibleNavigation = computed(() => filterNavigationByPermissions(clientNavigation, auth.permissionCodes)) +const activeNavigationPath = computed(() => getNavigationLeaves(visibleNavigation.value) + .filter(node => node.path === props.activePath || (node.path !== '/' && props.activePath.startsWith(`${node.path}/`))) + .sort((left, right) => right.path.length - left.path.length)[0]?.path ?? props.activePath) -const activeParentKeys = computed(() => new Set(getNavigationParentKeys(visibleNavigation.value, props.activePath))) +const activeParentKeys = computed(() => new Set(getNavigationParentKeys(visibleNavigation.value, activeNavigationPath.value))) const openedKeys = ref>(new Set(activeParentKeys.value)) // Expand the order group by default so the three customer order shortcuts are visible. openedKeys.value = new Set([...openedKeys.value, 'order-center']) @@ -73,7 +77,7 @@ function handleSelect(path: string) { :key="node.key" :node="node" :badges="badges" - :active-path="activePath" + :active-path="activeNavigationPath" :opened-keys="openedKeys" :active-parent-keys="activeParentKeys" :level="0" diff --git a/src/components/ClientSidebarMenuNode.vue b/src/components/ClientSidebarMenuNode.vue index 25df696..7c7b853 100644 --- a/src/components/ClientSidebarMenuNode.vue +++ b/src/components/ClientSidebarMenuNode.vue @@ -27,6 +27,7 @@ const groupId = computed(() => `client-nav-group-${props.node.key}`)
  • +
  • { records: T[] @@ -27,6 +27,16 @@ export interface BusinessOrderQuery { } export interface TransportSummary { + deliveryMode?: 'DELIVERY_TO_WAREHOUSE' | 'WAREHOUSE_PICKUP' + operatingWarehouseId?: string + destinationType?: 'FBA' | 'PRIVATE' + destinationPostalCode?: string + deliveryContactName?: string + deliveryContactPhone?: string + estimatedArrivalAt?: string + trackingNo?: string + transferNo?: string + fleetArrangement: import('../fleetArrangement').FleetArrangement | null orderNo: string transportType?: string @@ -59,9 +69,20 @@ export interface TransportDetail { originAddress: string | null destinationAddress: string | null remark: string | null + histories?: Array<{ operationType: string; toStatus: string; remark: string | null }> } export interface TransportCreate { + deliveryMode?: 'DELIVERY_TO_WAREHOUSE' | 'WAREHOUSE_PICKUP' + operatingWarehouseId?: string + destinationType?: 'FBA' | 'PRIVATE' + destinationPostalCode?: string + deliveryContactName?: string + deliveryContactPhone?: string + estimatedArrivalAt?: string + trackingNo?: string + transferNo?: string + productName?: string estimatedPalletCount: number origin: string @@ -95,7 +116,7 @@ export interface TruckCreate { destinationContactPhone: string pickupAt: string deliveryAt: string - packageCount: number + palletCount: number totalWeightKg: number totalVolumeCbm: number budgetAmount?: number @@ -129,6 +150,16 @@ export function getBusinessOrder(kind: CrossBorderBusinessKind, orderNo: stri return apiRequest({ method: 'GET', url: `${endpointByKind[kind]}/${encodeURIComponent(orderNo)}` }) } +export interface TransportPhoto { fileId: string; fileName: string; contentType: string; sizeBytes: number } +export interface TransportPhotos { pickupPhotos: TransportPhoto[]; arrivalPhotos: TransportPhoto[] } +export type TransportPhotoPhase = 'PICKUP' | 'ARRIVAL' +export function getTransportPhotos(kind: CrossBorderBusinessKind, orderNo: string) { + return apiRequest({ method: 'GET', url: `${endpointByKind[kind]}/${encodeURIComponent(orderNo)}/transport-photos` }) +} +export function readTransportPhoto(kind: CrossBorderBusinessKind, orderNo: string, phase: TransportPhotoPhase, fileId: string) { + return apiBlobRequest({ method: 'GET', url: `${endpointByKind[kind]}/${encodeURIComponent(orderNo)}/transport-photos/${phase}/${encodeURIComponent(fileId)}/content` }) +} + export function createTransport(kind: 'trucking', payload: TruckCreate): Promise export function createTransport(kind: 'looseboard', payload: TransportCreate): Promise export function createTransport(kind: CrossBorderBusinessKind, payload: TransportCreate | TruckCreate) { @@ -144,3 +175,8 @@ export function updateTransport(kind: CrossBorderBusinessKind, orderNo: string, data: payload, }) } + +/** 客户按当前版本取消待审核本土卡车订单。 */ +export function cancelTruckOrder(orderNo: string, payload: { version: number; status: 'CANCELLED'; idempotencyKey: string; remark: string }) { + return apiRequest({ method: 'PATCH', url: endpointByKind.trucking + '/' + encodeURIComponent(orderNo) + '/status', data: payload }) +} diff --git a/src/modules/cross-border/api/orderBudgets.ts b/src/modules/cross-border/api/orderBudgets.ts new file mode 100644 index 0000000..16d8eab --- /dev/null +++ b/src/modules/cross-border/api/orderBudgets.ts @@ -0,0 +1,10 @@ +import { apiRequest } from '@/api/http' +export interface BudgetResult { status: string; usable: boolean; amountCny: number | string; message: string; orderNo: string | null } +const base = '/merchant/v1/last-leg/order-budgets' +// 创建订单时后端在同一事务记录冻结请求,客户端只在详情核对状态和重试明确失败。 +export function readOrderFunding(kind: 'procurement' | 'trucking', orderNo: string) { + return apiRequest({ method: 'GET', url: base + '/' + kind + '/orders/' + encodeURIComponent(orderNo) }) +} +export function retryOrderFunding(kind: 'procurement' | 'trucking', orderNo: string) { + return apiRequest({ method: 'POST', url: base + '/' + kind + '/orders/' + encodeURIComponent(orderNo) + '/retry' }) +} diff --git a/src/modules/cross-border/api/procurement.ts b/src/modules/cross-border/api/procurement.ts index 6b1b6ca..e81b6f2 100644 --- a/src/modules/cross-border/api/procurement.ts +++ b/src/modules/cross-border/api/procurement.ts @@ -1,8 +1,9 @@ -import { apiRequest } from '@/api/http' +import { apiBlobRequest, apiRequest } from '@/api/http' import type { PageResult } from './businessOrders' export type Amount = number | string export interface ProcurementSummary { + procurementDirection?: import('../procurementPresentation').ProcurementDirection orderNo: string purchaseCategory: string productName: string @@ -11,6 +12,7 @@ export interface ProcurementSummary { budgetUnitPrice: Amount | null currency: string | null status: string + reviewRemark?: string | null version: number createdAt: string updatedAt: string @@ -24,6 +26,7 @@ export interface ProcurementOrder extends ProcurementSummary { plannedWarehouseName: string | null actualWarehouseCode: string | null actualWarehouseName: string | null + remark: string | null } export interface ProcurementFee { feeNo: string @@ -73,6 +76,7 @@ export interface ProcurementDetail { exceptionSummaries: string[] } export interface ProcurementCreate { + procurementDirection?: import('../procurementPresentation').ProcurementDirection customerName: string purchaseCategory: string productName: string @@ -95,6 +99,37 @@ export interface ProcurementQuery { purchaseCategory?: string status?: string } +export interface ProcurementWorkflowFile { fileId: string; fileName: string; contentType: string } +export interface ProcurementReceipt { + id: string + quantity: number + fileIds: string[] + receivedAt: string +} +export interface ProcurementOutbound { + id: string + outboundNo: string + quantity: number + trackingNo: string + status: string + fileIds: string[] + createdAt: string + completedAt: string | null +} +export interface ProcurementWorkflow { + orderNo: string + version: number + status: string + rejectionReason: string | null + photosNeedUpdate: boolean + receivedQuantity: number + stockQuantity: number + reservedQuantity: number + availableQuantity: number + files: ProcurementWorkflowFile[] + receipts: ProcurementReceipt[] + outbounds: ProcurementOutbound[] +} const endpoint = '/merchant/v1/last-leg/overseas-procurements' export async function listProcurements(params: ProcurementQuery) { const result = await apiRequest>({ method: 'GET', url: endpoint, params }) @@ -106,3 +141,16 @@ export function getProcurement(orderNo: string) { export function createProcurement(data: ProcurementCreate) { return apiRequest({ method: 'POST', url: endpoint, data }) } +export function getProcurementWorkflow(orderNo: string) { + return apiRequest({ method: 'GET', url: `${endpoint}/${encodeURIComponent(orderNo)}/workflow` }) +} +export function confirmProcurement(orderNo: string, data: { version: number; idempotencyKey: string }) { + return apiRequest({ method: 'POST', url: `${endpoint}/${encodeURIComponent(orderNo)}/confirm`, data }) +} + +export function rejectProcurement(orderNo: string, data: { version: number; idempotencyKey: string; reason: string }) { + return apiRequest({ method: 'POST', url: `${endpoint}/${encodeURIComponent(orderNo)}/reject`, data }) +} +export function getProcurementFile(orderNo: string, fileId: string) { + return apiBlobRequest({ method: 'GET', url: `${endpoint}/${encodeURIComponent(orderNo)}/files/${encodeURIComponent(fileId)}/content` }) +} diff --git a/src/modules/cross-border/businessOrders.test.mjs b/src/modules/cross-border/businessOrders.test.mjs index 6279cc0..ee6d66c 100644 --- a/src/modules/cross-border/businessOrders.test.mjs +++ b/src/modules/cross-border/businessOrders.test.mjs @@ -13,6 +13,29 @@ const demandHelpers = runInNewContext(`${demandLogic}\n({ createTransportDemandF const procurementApi = readFileSync(new URL('./api/procurement.ts', import.meta.url), 'utf8') const procurementView = readFileSync(new URL('./views/ProcurementView.vue', import.meta.url), 'utf8') +test('all current truck statuses display Chinese labels in lists and details', () => { + const labelsCode = ts.transpileModule(readFileSync(new URL('./fleetArrangement.ts', import.meta.url), 'utf8'), { compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ESNext } }).outputText.replace(/^export /gm, '') + const { poolOrderStatusLabels } = runInNewContext(`${labelsCode}\n({ poolOrderStatusLabels })`) + const helpers = view.slice(view.indexOf('function read('), view.indexOf('function idempotencyKey(')) + const compiled = ts.transpileModule(helpers, { compilerOptions: { target: ts.ScriptTarget.ES2022 } }).outputText + const { statusLabel, displayCell } = runInNewContext(`${compiled}\n({ statusLabel, displayCell })`, { poolOrderStatusLabels }) + for (const status of ['PENDING_REVIEW', 'FUNDING_PENDING', 'FUNDING_FAILED', 'WAITING_INBOUND', 'RECEIVING', 'WAITING_CONSOLIDATION', 'PALLET_OVERAGE_REVIEW', 'PARTIALLY_ALLOCATED', 'ALLOCATED', 'PENDING_DISPATCH', 'DISPATCHED', 'IN_TRANSIT', 'COMPLETED', 'REJECTED', 'CANCELLED']) { + assert.match(statusLabel(status), /[\u4e00-\u9fff]/, status) + assert.equal(displayCell({ status }, 'status'), statusLabel(status)) + } + assert.equal(statusLabel('DISPATCHED'), '待取货') + assert.equal(statusLabel('IN_TRANSIT'), '派送中') + assert.equal(poolOrderStatusLabels.IN_TRANSIT, '派送中') +}) + +test('truck search excludes warehouse and dispatch preparation stages', () => { + const truckingOptions = view.split("options: props.kind === 'trucking' ? [")[1].split('] : [')[0] + for (const status of ['WAITING_INBOUND', 'RECEIVING', 'WAITING_CONSOLIDATION', 'PALLET_OVERAGE_REVIEW', 'PARTIALLY_ALLOCATED', 'ALLOCATED', 'PENDING_DISPATCH']) { + assert.ok(!truckingOptions.includes(status), status) + } + assert.match(truckingOptions, /label: '派送中', value: 'IN_TRANSIT'/) +}) + test('renders unknown pallet service and container type as dashes while preserving explicit values', () => { const helpers = view.slice(view.indexOf('function read('), view.indexOf('function idempotencyKey(')) const compiled = ts.transpileModule(helpers, { compilerOptions: { target: ts.ScriptTarget.ES2022 } }).outputText @@ -32,7 +55,7 @@ test('renders unknown pallet service and container type as dashes while preservi test('shows merchant-owned arrangement facts without a pool request or editable status control', () => { assert.match(api, /fleetArrangement: import\('\.\.\/fleetArrangement'\)\.FleetArrangement \| null/) assert.match(view, /aria-label="承运安排"/) - for (const field of ['signedPackages', 'unsignedPackages', 'returnedPackages', 'readOnlyReason']) assert.ok(view.includes(`fleetArrangement.${field}`)) + for (const field of ['signedPackages', 'unsignedPackages', 'returnedPackages']) assert.ok(view.includes(`fleetArrangement.${field}`)) assert.doesNotMatch(api, /capacity-offers|fleet\/trips/) assert.match(view, /if \(query\.page !== 1\) query\.page = 1\s+else void load\(\)/) assert.match(view, /if \(reloadDirectly\) void load\(\)/) @@ -75,7 +98,9 @@ test('covers list detail create loading error empty pagination and actions-first assert.match(view, /]*:row-actions="getRowActions"/) assert.match(view, /key: 'actions', label: '操作', width: 90, fixed: 'left'/) assert.match(view, /class="business-table-card"/) - assert.doesNotMatch(procurementView, /inventoryQuantity|outboundQuantity|reviewRemark/) + assert.doesNotMatch(procurementView, /inventoryQuantity|outboundQuantity/) + assert.match(procurementView, /label: '失败 \/ 拒绝原因'/) + assert.match(procurementView, /reviewRemark/) assert.match(procurementView, /procurementStatuses/) assert.match(procurementView, /ProcurementDetailPanel/) }) @@ -119,21 +144,22 @@ test('uses the shared dense order-list and detail visual language', () => { assert.match(view, /\.detail-grid div \{[^}]*min-height: 34px;[^}]*grid-template-columns: 76px minmax\(0, 1fr\)/) }) -function transportHarness(kind = 'trucking', failFirst = false) { +function transportHarness(kind = 'trucking', failFirst = false, budgetCheck = async () => {}) { const calls = [] const script = view.match(/