统一后台状态列顺序、详情布局与测量计费展示
This commit is contained in:
@@ -29,7 +29,7 @@
|
||||
| 订单列表、批量审核预览、详情、商品图片与入库费用 | `modules/unified-orders/`、`modules/first-leg-orders/`;平台商品图片由 `modules/unified-orders/api/unifiedOrders.ts` 按客户与 fileId 下载 | `order/`、`firstleg/`、Finance;旧系统导入固定归属“IT测试公司”,前端不提交或选择客户,逐行结果展示入仓码与失败原因。图片读取归 `file/` 的 `PlatformProductImageController`。Order 状态为 `SUBMITTED` 时统一列表必须保持 `PENDING_REVIEW/APPROVE_ORDER`,不得被已存在的 Firstleg 准备中入库投影覆盖。批量审核弹框通过 `unifiedOrders.ts` 的 `/orders/first-leg/batch-review-preview` 一次取回全部预览,按服务端 `canApprove/blockReason` 展示,不逐票请求详情或用内部 ID 作标题 |
|
||||
| 客户原始申报资料编辑与渠道变更 | `modules/unified-orders/components/OrderDeclarationEditorDialog.vue`、`api/unifiedOrders.ts`、`pages/UnifiedOrdersView.vue` | 使用订单版本和货物 `itemNo` 整体提交,不允许增删货物行;只编辑客户申报快照,不覆盖仓库/PDA 实测、测量审核或计费事实。渠道变化需二次确认并由 Backend `order/` 联查 `firstleg/` 的分柜、同柜、装柜与重新计价门禁;国内收货仓或中转仓变化必须重新下单 |
|
||||
| 入库扣货、解除与凭证历史 | `modules/unified-orders/pages/UnifiedOrdersView.vue`、`modules/inbounds/core/api.ts`、`types.ts` | 页面只消费服务端 `holdFlag/currentHoldReason/canHold/canRelease/actionBlockReason/history`;扣货与解除提交当前入库版本、幂等键、必填原因和可选凭证,凭证下载只走 Firstleg 授权入口。Backend `firstleg/` 的 `InboundHoldService`、`InboundHoldGuard`、`InboundHoldEvidenceService`,文件归 `file/` |
|
||||
| 入库、测量、称重库位状态、客户箱规策略与计价主数据 | 测量弹框/工作区 `modules/inbounds/measurement/InboundMeasurementDialog.vue`、`InboundMeasurementPanel.vue`,称重 `modules/inbounds/weight/components/WeightOcrPanel.vue`;主数据 `modules/fba-pricing/`、`modules/dimension-config/`、`modules/customers/components/CustomerBillingRulesPanel.vue`、`modules/fee-items/` | 客户固定四档箱规,默认计算方式为 `ADD/MULTIPLY`;测量行可选实际尺寸、箱规尺寸策略或客户默认尺寸系数,保存后以 Backend `firstleg/` 返回的外箱尺寸、体积重和计费基数为准,不在页面独立重算。费用确认的实际箱数、毛重和体积优先汇总当前 `actualMeasurementRows`,不得用可能滞后的工作区顶层合计覆盖明细。称重页在每张图片卡片只展示服务端 `locationCode/slotStatus/sharedOverflow`,不渲染或自行推导整票预留引导;删除图片须提示槽位仍归本票保留,不得按图片数组下标推导托盘或库位 |
|
||||
| 入库、测量、称重库位状态、客户箱规策略与计价主数据 | 测量弹框/工作区 `modules/inbounds/measurement/InboundMeasurementDialog.vue`、`InboundMeasurementPanel.vue`,称重 `modules/inbounds/weight/components/WeightOcrPanel.vue`;主数据 `modules/fba-pricing/`、`modules/dimension-config/`、`modules/customers/components/CustomerBillingRulesPanel.vue`、`modules/fee-items/` | 客户固定四档箱规,默认计算方式为 `ACTUAL/ADD/MULTIPLY`;未启用默认调整规则时按实测原尺寸计算,仓库修正值替换对应实测边。测量行可选实际尺寸、箱规尺寸策略或客户默认尺寸系数,保存后以 Backend `firstleg/` 返回的外箱尺寸、体积重和计费基数为准,不在页面独立重算。费用确认的箱数、毛重汇总当前 `actualMeasurementRows`,计费体积默认取已保存的 `outerMeasurementRows` 汇总,最终按用户确认值提交,体积来源标记为 `OUTER`;不得用可能滞后的工作区顶层合计覆盖明细。称重页在每张图片卡片只展示服务端 `locationCode/slotStatus/sharedOverflow`,不渲染或自行推导整票预留引导;删除图片须提示槽位仍归本票保留,不得按图片数组下标推导托盘或库位 |
|
||||
| 一件代发托盘打印 | `modules/unified-orders/pages/UnifiedOrdersView.vue` 的“托盘打印”页签、`modules/inbounds/core/DropshipPalletPrintPanel.vue`、`core/api.ts` | 仅一件代发订单展示;测量审核预生成托盘码后由 Admin 电脑端打印,支持本票全部托盘合并打印和单托补打。PDA 只扫码组托,不承担 PDF 下载或打印机连接 |
|
||||
| 入库单扣货、解除、凭证与历史 | `modules/inbounds/pages/InboundOrdersView.vue`、详情工作区、`modules/inbounds/core/api.ts` | 列表展示服务端 `holdFlag/currentHoldReason` 并支持筛选;详情只按 `canHold/canRelease/actionBlockReason` 开放操作。扣货与解除提交当前 `version`、原因和独立幂等键,可选凭证先按明确客户与入库单上传;历史文件只调用专用下载入口,不把上传成功但未提交命令的文件冒充扣货证据 |
|
||||
| 客户专属价格簿、柜级阶梯运费与受控调价 | 列表/上传 `modules/fba-pricing/carrier-rates/pages/CarrierRatesView.vue`,版本详情/仓库映射/确认 `modules/fba-pricing/carrier-rates/pages/CarrierRateDetailView.vue`,路由 `modules/fba-pricing/routes.ts`;`modules/inbounds/fee/`、`modules/unified-orders/components/InboundFeeDetailManager.vue` | 列表与上传后按 `versionId` 进入 `fba-carrier-rate-detail` 独立详情页,价格行只读;回归入口 `modules/fba-pricing/carrier-rates/pages/CarrierRatesView.test.ts`。价格簿页面显式选择公共报价或具体客户;列表、上传、确认只作用于该 `customerId`,不得混合版本。计价优先消费客户专属 ACTIVE 版本,无专属版本才回退公共 ACTIVE;柜级阶梯金额和各客户分摊只展示服务端结果。调整计费量只能在服务端返回的上下 10% 范围内并提交当前费用版本、原因与二次确认;私人仓尾程派送费使用稳定 `PRIVATE_LAST_MILE_DELIVERY` 代码且只对 `PRIVATE` 目的地开放;一件代发须在预装确认前完成试算,计价后保持服务端 `WAIT_PALLETIZE` 状态 |
|
||||
|
||||
@@ -49,14 +49,14 @@ describe('adminNavigation', () => {
|
||||
{ title: '预约管理', children: ['预约列表'] },
|
||||
{ title: '车辆管理', children: ['排车列表', '车辆列表', '司机管理', '自有业务', '外配任务', '出车记录'] },
|
||||
{ title: '海外业务', children: ['采购订单', '本土卡车', '本土散板', '散板需求'] },
|
||||
{ title: '客户服务', children: ['异常订单'] },
|
||||
{ title: '客户服务', children: ['异常订单', '客户回访'] },
|
||||
{ title: '财务管理', children: ['海外费用明细', '客户账户', '客户应收应付', '应收款统计表', '客户对账单', '结算单应收', '结算单应付'] },
|
||||
{ title: '海外仓', children: ['经营总览', '销量分析', '库存分析', '数据采集'] },
|
||||
{ title: '通用', children: ['费用项目', '汇率管理', '仓库管理', '海外仓报价', '车队报价', '船期价格表', '下载中心', '供应商列表'] },
|
||||
{ title: '系统管理', children: ['用户管理', '角色权限', '菜单权限'] },
|
||||
])
|
||||
expect(visible).toHaveLength(13)
|
||||
expect(visible.flatMap(node => node.children ?? [])).toHaveLength(59)
|
||||
expect(visible.flatMap(node => node.children ?? [])).toHaveLength(60)
|
||||
expect(adminNavigation.find((item) => item.key === 'notifications')).toBeUndefined()
|
||||
})
|
||||
|
||||
|
||||
@@ -37,12 +37,12 @@ let generation = 0, selectionGeneration = 0, controller: AbortController | null
|
||||
const columns: DataTableColumn[] = [
|
||||
{ key: 'select', label: '选择', width: 60, slotName: 'select' },
|
||||
{ prop: 'inboundCode', label: '入仓码', minWidth: 180 },
|
||||
{ key: 'eligibility', label: '预约资格', minWidth: 260, singleLine: false, slotName: 'eligibility', afterColumn: 'inboundCode' },
|
||||
{ prop: 'customerName', label: '客户', minWidth: 140, slotName: 'customer' },
|
||||
{ prop: 'containerNo', label: '柜号', minWidth: 145 },
|
||||
{ prop: 'originWarehouseName', label: '起运仓', minWidth: 140, slotName: 'origin' },
|
||||
{ prop: 'destinationWarehouseCode', label: '目的FBA仓', width: 110 },
|
||||
{ prop: 'packages', label: '箱数', width: 80 },
|
||||
{ key: 'eligibility', label: '预约资格', minWidth: 260, singleLine: false, slotName: 'eligibility' },
|
||||
]
|
||||
function key(row: PlanningCargo) { return JSON.stringify([row.customerId, row.sourceKey]) }
|
||||
function reason(row: PlanningCargo) {
|
||||
|
||||
@@ -55,7 +55,7 @@ onBeforeUnmount(() => { generation++ })
|
||||
<section aria-label="沿用原整车排车"><h3>沿用原整车排车 <InlineHelp title="排车说明"><p>在原排车单维护车辆、司机和货物;一张排车单就是一趟车。</p></InlineHelp></h3>
|
||||
<p v-if="!readable">查看原排车单及批次关联需要相应列表权限。</p><p v-if="loading">正在核对原排车单与批次…</p>
|
||||
<LoadErrorState v-if="error" title="原排车单读取失败" :message="error.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState>
|
||||
<table v-if="!loading && !error && readable" class="density-table"><thead><tr><th>操作</th><th>排车单号</th><th>车牌号</th><th>箱数</th><th>阶段</th></tr></thead><tbody><tr v-for="row in rows" :key="row.dispatchNo"><td><el-button v-if="canEdit(row)" :disabled="addingBusy" @click="emit('external', path(row.dispatchNo))">继续维护本车</el-button><el-button v-if="canAdd(row)" :disabled="addingBusy" @click="add(row)">为本车添加同仓货物</el-button><el-button @click="emit('external', path(row.dispatchNo))">查看原排车单</el-button></td><td>{{ row.dispatchNo }}</td><td>{{ row.vehicleInfo.vehicle?.plate || '—' }}</td><td>{{ row.counts.packageCount }}</td><td>{{ stageLabel(row.stage) }}</td></tr></tbody></table>
|
||||
<table v-if="!loading && !error && readable" class="density-table"><thead><tr><th>操作</th><th>排车单号</th><th>阶段</th><th>车牌号</th><th>箱数</th></tr></thead><tbody><tr v-for="row in rows" :key="row.dispatchNo"><td><el-button v-if="canEdit(row)" :disabled="addingBusy" @click="emit('external', path(row.dispatchNo))">继续维护本车</el-button><el-button v-if="canAdd(row)" :disabled="addingBusy" @click="add(row)">为本车添加同仓货物</el-button><el-button @click="emit('external', path(row.dispatchNo))">查看原排车单</el-button></td><td>{{ row.dispatchNo }}</td><td>{{ stageLabel(row.stage) }}</td><td>{{ row.vehicleInfo.vehicle?.plate || '—' }}</td><td>{{ row.counts.packageCount }}</td></tr></tbody></table>
|
||||
<WorkSharedTripForm v-if="addingNo" :key="addingNo" :dispatch-no="addingNo" :can-write="canWrite" @busy="addingBusy = $event" @updated="updated" @external="value => emit('external', value)" />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -48,7 +48,7 @@ onMounted(load)
|
||||
<template v-if="detail && !error"><p>{{ detail.summary.containerNo || '柜号待核实' }} · {{ detail.summary.overseasWarehouseName || '接仓仓库待核实' }}</p><p v-if="detail.summary.blockingReason">{{ detail.summary.blockingReason }}</p><table class="density-table"><thead><tr><th>操作</th><th>入库单号</th><th>入仓码 / 订单号</th></tr></thead><tbody><tr v-for="row in detail.orders" :key="`${row.customerId}:${row.orderId}`"><td><el-button :disabled="saving || !!pending || !row.planNo || !row.customerId || !row.planValid || !access.can('platform:overseas-warehouse:inbound-order:list')" @click="emit('external', `/overseas-warehouse/inbound-orders/${encodeURIComponent(row.planNo!)}?customerId=${encodeURIComponent(row.customerId)}`)">查看原接仓记录</el-button></td><td>{{ row.planNo || '尚未生成有效计划' }}</td><td>{{ row.inboundCode || row.orderNo || '—' }}</td></tr></tbody></table><p v-if="!detail.orders.length">本柜尚无可读取的入仓来源。</p></template>
|
||||
<el-button v-if="writable && !preview && !pending" :loading="previewLoading" @click="loadPreview">核对本柜入库计划预览</el-button>
|
||||
<LoadErrorState v-if="previewError" title="入库计划预览失败" :message="previewError.message" :loading="previewLoading" @retry="loadPreview"><template #details><ApiErrorMeta :details="previewError" /></template></LoadErrorState>
|
||||
<div v-if="preview && !previewError"><h4>核对本柜计划与托盘</h4><p>{{ preview.containerNo || '柜号待核实' }} · {{ preview.overseasWarehouseName || '接仓仓库待核实' }} · {{ preview.pendingOrderCount }} 票待生成 · {{ preview.blockedOrderCount }} 票待处理</p><p>头程已有 {{ preview.sourcePalletCount }} 托;海外补充 {{ preview.supplementPalletCount }} 托;已保存计划 {{ preview.totalPlannedPalletCount }} 托。</p><table class="density-table"><thead><tr><th>客户</th><th>原票</th><th>目的地</th><th>箱数</th><th>目标托数</th><th>计划状态</th></tr></thead><tbody><tr v-for="row in preview.groups.flatMap(group => group.orders)" :key="`${row.customerId}:${row.orderId}`"><td>{{ row.customerName || '客户名称待核实' }}</td><td>{{ row.inboundCode || row.businessOrderNo || '—' }}</td><td>{{ row.destinationWarehouseName || row.fbaWarehouseCode || (row.destinationType === 'PRIVATE_ADDRESS' ? '客户地址' : '目的地待核实') }}</td><td>{{ row.packageCount }}</td><td>{{ row.targetPalletCount }}</td><td>{{ row.existingPlanNo || ({ READY: '可生成', RESOURCE_PENDING: '资源待确认', PLANNED: '已有计划', BLOCKED: '来源条件未就绪' }[row.readiness]) }}</td></tr></tbody></table><p v-if="!preview.ready">本柜尚不具备生成条件,请在原尾程订单核对阻塞来源。</p><el-button type="primary" :loading="saving" :disabled="!writable || (!pending && (!preview.ready || (!preview.resourceRetry && preview.pendingOrderCount <= 0)))" @click="createPlans">{{ preview.resourceRetry ? '重试原资源确认' : '确认生成本柜计划' }}</el-button></div>
|
||||
<div v-if="preview && !previewError"><h4>核对本柜计划与托盘</h4><p>{{ preview.containerNo || '柜号待核实' }} · {{ preview.overseasWarehouseName || '接仓仓库待核实' }} · {{ preview.pendingOrderCount }} 票待生成 · {{ preview.blockedOrderCount }} 票待处理</p><p>头程已有 {{ preview.sourcePalletCount }} 托;海外补充 {{ preview.supplementPalletCount }} 托;已保存计划 {{ preview.totalPlannedPalletCount }} 托。</p><table class="density-table"><thead><tr><th>客户</th><th>原票</th><th>计划状态</th><th>目的地</th><th>箱数</th><th>目标托数</th></tr></thead><tbody><tr v-for="row in preview.groups.flatMap(group => group.orders)" :key="`${row.customerId}:${row.orderId}`"><td>{{ row.customerName || '客户名称待核实' }}</td><td>{{ row.inboundCode || row.businessOrderNo || '—' }}</td><td>{{ row.existingPlanNo || ({ READY: '可生成', RESOURCE_PENDING: '资源待确认', PLANNED: '已有计划', BLOCKED: '来源条件未就绪' }[row.readiness]) }}</td><td>{{ row.destinationWarehouseName || row.fbaWarehouseCode || (row.destinationType === 'PRIVATE_ADDRESS' ? '客户地址' : '目的地待核实') }}</td><td>{{ row.packageCount }}</td><td>{{ row.targetPalletCount }}</td></tr></tbody></table><p v-if="!preview.ready">本柜尚不具备生成条件,请在原尾程订单核对阻塞来源。</p><el-button type="primary" :loading="saving" :disabled="!writable || (!pending && (!preview.ready || (!preview.resourceRetry && preview.pendingOrderCount <= 0)))" @click="createPlans">{{ preview.resourceRetry ? '重试原资源确认' : '确认生成本柜计划' }}</el-button></div>
|
||||
<LoadErrorState v-if="saveError" title="入库计划尚未确认" :message="saveError.message" :show-retry="false"><template #details><ApiErrorMeta :details="saveError" /></template></LoadErrorState>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -248,7 +248,7 @@ describe('ContainerCustomsPanel', () => {
|
||||
global: { plugins: [ElementPlus] },
|
||||
})
|
||||
await flushPromises()
|
||||
expect(wrapper.findAll('th').map(header => header.text())).toEqual(['操作', '客户退税资料', '预录单', '真实报关完税文件', '关联入仓码', '报关行状态', '客户', '预录单情况'])
|
||||
expect(wrapper.findAll('th').map(header => header.text())).toEqual(['操作', '关联入仓码', '报关行状态', '客户退税资料', '预录单', '真实报关完税文件', '客户', '预录单情况'])
|
||||
expect(wrapper.text()).not.toMatch(/版本|退税资料 v|回执 v|完税 v/)
|
||||
expect(wrapper.text()).toContain('预录单历史:2026/8/29 10:50:00 · 报关员')
|
||||
expect(wrapper.text()).toContain('确认 · 2026/8/29 11:15:00 · 客户一')
|
||||
|
||||
@@ -321,13 +321,13 @@ onMounted(load)
|
||||
</div>
|
||||
<div v-if="customs.documents.length" class="compact-table-wrap">
|
||||
<h3>已冻结退税资料、预录单与完税文件</h3>
|
||||
<div class="workbench-evidence"><table class="compact-table document-table density-table"><thead><tr><th>操作</th><th>客户退税资料</th><th>预录单</th><th>真实报关完税文件</th><th>关联入仓码</th><th>报关行状态</th><th>客户</th><th>预录单情况</th></tr></thead><tbody>
|
||||
<div class="workbench-evidence"><table class="compact-table document-table density-table"><thead><tr><th>操作</th><th>关联入仓码</th><th>报关行状态</th><th>客户退税资料</th><th>预录单</th><th>真实报关完税文件</th><th>客户</th><th>预录单情况</th></tr></thead><tbody>
|
||||
<tr v-for="document in customs.documents" :key="document.documentId">
|
||||
<td><div v-if="canManage && customs.status === 'DECLARING'" class="document-actions density-actions"><el-upload :disabled="!brokerSubmission || brokerSubmitted" :auto-upload="false" :limit="1" :show-file-list="false" :on-change="(upload: UploadFile) => selectReceipt(document.documentId, upload)"><el-button class="density-action" size="small" :disabled="!brokerSubmission || brokerSubmitted">{{ !brokerSubmission ? '首次发送后上传预录单' : document.receipt ? '覆盖预录单' : '上传预录单' }}</el-button></el-upload><el-button data-tone="primary" class="density-action" size="small" type="primary" :disabled="!brokerSubmission || brokerSubmitted || !receiptFiles[document.documentId]" :loading="actionLoading" @click="uploadReceipt(document)">提交</el-button><el-upload :disabled="!brokerSubmitted" :auto-upload="false" :limit="1" :show-file-list="false" :on-change="(upload: UploadFile) => selectFinal(document.documentId, upload)"><el-button class="density-action" size="small" :disabled="!brokerSubmitted">{{ document.finalDocument ? '覆盖完税文件' : '上传完税文件' }}</el-button></el-upload><el-button data-tone="primary" class="density-action" size="small" type="primary" :disabled="!brokerSubmitted || !finalFiles[document.documentId]" :loading="actionLoading" @click="uploadFinal(document)">提交</el-button></div><span v-else class="readonly-label">只读</span></td>
|
||||
<td>{{ document.inboundCodes.join('、') || '—' }}</td>
|
||||
<td>{{ brokerSubmitted ? '已发送报关行执行' : brokerSubmission ? '已首次发送,等待预录单确认' : '待首次发送报关行' }}</td>
|
||||
<td><div class="file-cell"><span :title="document.source.fileName">{{ document.source.fileName }}</span><el-button class="density-action" type="primary" :loading="downloadingKey === `source:${document.documentId}`" :aria-label="`下载客户退税资料 ${document.source.fileName}`" @click="downloadFile(document, 'source')">下载</el-button></div></td>
|
||||
<td><div v-if="document.receipt" class="file-cell"><span :title="document.receipt.fileName">{{ document.receipt.fileName }}</span><el-button class="density-action" type="primary" :loading="downloadingKey === `receipt:${document.documentId}`" :aria-label="`下载预录单 ${document.receipt.fileName}`" @click="downloadFile(document, 'receipt')">下载</el-button></div><span v-else>未上传</span></td>
|
||||
<td><div v-if="document.finalDocument" class="file-cell"><span :title="document.finalDocument.fileName">{{ document.finalDocument.fileName }}</span><el-button class="density-action" type="primary" :loading="downloadingKey === `final:${document.documentId}`" :aria-label="`下载报关完税文件 ${document.finalDocument.fileName}`" @click="downloadFile(document, 'final')">下载</el-button></div><span v-else>未上传</span></td>
|
||||
<td>{{ document.inboundCodes.join('、') || '—' }}</td><td>{{ brokerSubmitted ? '已发送报关行执行' : brokerSubmission ? '已首次发送,等待预录单确认' : '待首次发送报关行' }}</td><td>{{ documentCustomers(document) }}</td><td><small v-for="history in document.confirmationHistory" :key="history.id" class="confirmation-history-note">{{ confirmationActionLabel(history.action) }} · {{ formatDateTime(history.createdAt) }} · {{ history.operatorName || '—' }}<span v-if="history.reason"> · {{ history.reason }}</span></small><small v-for="history in document.receiptHistory" :key="history.version" class="receipt-history-note">预录单历史:{{ formatDateTime(history.createdAt) }} · {{ history.operatorName || '—' }}</small>{{ document.receipt ? (document.receiptStatus === 'CONFIRMED' ? '客户已确认预录单' : document.receiptStatus === 'REJECTED' ? '客户已打回预录单' : '待客户确认预录单') : '待上传预录单' }}</td>
|
||||
<td><div v-if="document.receipt" class="file-cell"><span :title="document.receipt.fileName">{{ document.receipt.fileName }}</span><el-button class="density-action" type="primary" :loading="downloadingKey === `receipt:${document.documentId}`" :aria-label="`下载预录单 ${document.receipt.fileName}`" @click="downloadFile(document, 'receipt')">下载</el-button></div><span v-else>未上传</span></td><td><div v-if="document.finalDocument" class="file-cell"><span :title="document.finalDocument.fileName">{{ document.finalDocument.fileName }}</span><el-button class="density-action" type="primary" :loading="downloadingKey === `final:${document.documentId}`" :aria-label="`下载报关完税文件 ${document.finalDocument.fileName}`" @click="downloadFile(document, 'final')">下载</el-button></div><span v-else>未上传</span></td><td>{{ documentCustomers(document) }}</td><td><small v-for="history in document.confirmationHistory" :key="history.id" class="confirmation-history-note">{{ confirmationActionLabel(history.action) }} · {{ formatDateTime(history.createdAt) }} · {{ history.operatorName || '—' }}<span v-if="history.reason"> · {{ history.reason }}</span></small><small v-for="history in document.receiptHistory" :key="history.version" class="receipt-history-note">预录单历史:{{ formatDateTime(history.createdAt) }} · {{ history.operatorName || '—' }}</small>{{ document.receipt ? (document.receiptStatus === 'CONFIRMED' ? '客户已确认预录单' : document.receiptStatus === 'REJECTED' ? '客户已打回预录单' : '待客户确认预录单') : '待上传预录单' }}</td>
|
||||
</tr>
|
||||
</tbody></table></div>
|
||||
</div>
|
||||
|
||||
@@ -176,12 +176,12 @@ onMounted(load)
|
||||
<el-table-column label="操作" fixed="left" width="240"><template #default="{ row }"><div class="row-actions density-actions"><el-button data-tone="primary" class="density-action" v-if="canEdit && row.sourceStatus === 'ACTIVE' && ['DRAFT', 'REJECTED'].includes(row.reviewStatus)" type="primary" @click="submit(row as OverseasContainerFeeSource)">提交</el-button><el-button data-tone="warning" class="density-action" v-if="canReview && row.sourceStatus === 'ACTIVE' && row.reviewStatus === 'PENDING'" type="success" @click="review(row as OverseasContainerFeeSource, 'APPROVE')">通过</el-button><el-button data-tone="danger" class="density-action" v-if="canReview && row.sourceStatus === 'ACTIVE' && row.reviewStatus === 'PENDING'" type="danger" @click="review(row as OverseasContainerFeeSource, 'REJECT')">驳回</el-button><el-button class="density-action" v-if="canEdit && row.sourceStatus === 'ACTIVE' && row.reviewStatus === 'APPROVED' && !voidPending(row as OverseasContainerFeeSource)" type="primary" @click="openAdjust(row as OverseasContainerFeeSource)">调整</el-button><el-button data-tone="danger" class="density-action" v-if="canReview && row.sourceStatus === 'ACTIVE' && row.reviewStatus === 'APPROVED' && !voidPending(row as OverseasContainerFeeSource)" type="danger" @click="voidFee(row as OverseasContainerFeeSource)">作废</el-button><span v-if="row.sourceStatus !== 'ACTIVE' || (!canEdit && !canReview)">—</span></div></template></el-table-column>
|
||||
<el-table-column prop="customerName" label="客户" min-width="160" show-overflow-tooltip><template #default="{ row }">{{ row.customerName || '—' }}</template></el-table-column>
|
||||
<el-table-column prop="feeItemCode" label="费用项目" min-width="140" show-overflow-tooltip><template #default="{ row }">{{ feeItemLabel(row.feeItemCode) }}</template></el-table-column>
|
||||
<el-table-column prop="reviewStatus" label="审核状态" width="110"><template #default="{ row }"><el-tag>{{ reviewLabels[row.reviewStatus] || row.reviewStatus }}</el-tag></template></el-table-column>
|
||||
<el-table-column prop="direction" label="方向" width="100"><template #default="{ row }">{{ row.direction === 'RECEIVABLE' ? '应收' : '应付' }}</template></el-table-column>
|
||||
<el-table-column prop="supplierName" label="供应商" min-width="150" show-overflow-tooltip><template #default="{ row }">{{ row.supplierName || '—' }}</template></el-table-column>
|
||||
<el-table-column prop="amount" label="金额" min-width="130" align="right"><template #default="{ row }">¥{{ row.amount.toFixed(2) }}</template></el-table-column>
|
||||
<el-table-column label="付款凭证" min-width="180"><template #default="{ row }"><div v-if="row.paymentVoucherFileIds.length" class="voucher-list"><el-button v-for="(fileId, index) in (row.paymentVoucherFileIds as string[])" :key="fileId" link type="primary" @click="previewVoucher(row.customerId, fileId)">{{ voucherNames[fileId] || `付款凭证 ${index + 1}` }}</el-button></div><span v-else>—</span></template></el-table-column>
|
||||
<el-table-column prop="businessDate" label="业务日期" width="120" />
|
||||
<el-table-column prop="reviewStatus" label="审核状态" width="110"><template #default="{ row }"><el-tag>{{ reviewLabels[row.reviewStatus] || row.reviewStatus }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="作废确认" min-width="130"><template #default="{ row }">{{ voidLabels[row.voidStatus] || '—' }}</template></el-table-column>
|
||||
<el-table-column prop="reviewReason" label="审核说明" min-width="180" show-overflow-tooltip><template #default="{ row }">{{ row.reviewReason || '—' }}</template></el-table-column>
|
||||
</el-table>
|
||||
|
||||
@@ -58,8 +58,10 @@ describe('ContainerOverviewPanel', () => {
|
||||
it('renders two file rows and one inline PU code row without a nested heading', async () => {
|
||||
const wrapper = mount(ContainerOverviewPanel, { props: { overview, canManageDocuments: true, canManageCustoms: true, canManageException: true }, global: { plugins: [ElementPlus] } })
|
||||
const headers = wrapper.findAll('.document-table thead th').map((cell) => cell.text())
|
||||
expect(headers).toEqual(['操作', '资料类型', '资料内容', '更新人', '更新时间', '状态'])
|
||||
expect(headers).toEqual(['操作', '资料类型', '状态', '资料内容', '更新人', '更新时间'])
|
||||
expect(wrapper.findAll('.document-table tbody tr')).toHaveLength(3)
|
||||
expect(wrapper.findAll('.document-table tbody tr').every(row => row.findAll('td')[2]!.find('.el-tag').exists())).toBe(true)
|
||||
expect(wrapper.find('.pickup-code-row').findAll('td')[3]!.find('input[aria-label="PU码"]').exists()).toBe(true)
|
||||
expect(wrapper.findAll('.document-item')).toHaveLength(0)
|
||||
expect(wrapper.find('.document-table-wrap').exists()).toBe(true)
|
||||
expect(wrapper.text()).not.toContain('业务资料')
|
||||
|
||||
@@ -130,21 +130,21 @@ onBeforeUnmount(() => { photoGeneration += 1; revokePhotoUrls() })
|
||||
<section v-if="visibleSections.has('documents')" class="overview-section overview-section--documents" aria-label="报关清关资料">
|
||||
<div class="document-table-wrap">
|
||||
<div class="workbench-evidence"><table class="document-table density-table">
|
||||
<thead><tr><th class="document-table__actions">操作</th><th>资料类型</th><th>资料内容</th><th>更新人</th><th>更新时间</th><th>状态</th></tr></thead>
|
||||
<thead><tr><th class="document-table__actions">操作</th><th>资料类型</th><th>状态</th><th>资料内容</th><th>更新人</th><th>更新时间</th></tr></thead>
|
||||
<tbody><tr v-for="item in documentTypes" :key="item.type">
|
||||
<td class="document-table__actions"><div class="document-actions density-actions"><el-button class="density-action" v-if="documentsByType.get(item.type) && canDownloadDocument(item.type)" size="small" :loading="documentBusy" @click="emit('downloadDocument', documentsByType.get(item.type)!)">下载</el-button><el-button class="density-action" v-if="canUploadDocument(item.type) && !documentsByType.get(item.type)?.completed" size="small" @click="emit('uploadDocument', item.type)">{{ documentsByType.get(item.type) ? '更新附件' : '上传附件' }}</el-button><span v-if="!hasDocumentAction(item.type)" class="no-action">—</span></div></td>
|
||||
<td><strong>{{ item.label }}</strong></td>
|
||||
<td><el-tag size="small" :type="documentStatusType(item.type)">{{ documentStatusLabel(item.type) }}</el-tag></td>
|
||||
<td><span class="document-file-name" :title="documentsByType.get(item.type)?.fileName || undefined">{{ documentsByType.get(item.type)?.fileName || '—' }}</span><small v-if="item.type === 'DELIVERY_ORDER' && deliveryOrderBlockedReason" class="document-blocked-reason">{{ deliveryOrderBlockedReason }}</small></td>
|
||||
<td>{{ documentsByType.get(item.type)?.uploadedByName || '—' }}</td>
|
||||
<td>{{ documentsByType.get(item.type) ? formatTime(documentsByType.get(item.type)?.uploadedAt) : '—' }}</td>
|
||||
<td><el-tag size="small" :type="documentStatusType(item.type)">{{ documentStatusLabel(item.type) }}</el-tag></td>
|
||||
</tr><tr class="pickup-code-row">
|
||||
<td class="document-table__actions"><el-button data-tone="primary" class="density-action" v-if="canManageDocuments" size="small" type="primary" :loading="pickupCodeBusy" :disabled="!pickupCodeValid || pickupCodeInput === (overview.container.pickupCode || '')" @click="emit('savePickupCode', pickupCodeInput)">保存</el-button><span v-else class="no-action">—</span></td>
|
||||
<td><strong>PU 资料</strong></td>
|
||||
<td><el-tag size="small" :type="overview.container.pickupCode ? 'success' : 'info'">{{ overview.container.pickupCode ? '已填写' : '未填写' }}</el-tag></td>
|
||||
<td><el-input v-model="pickupCodeInput" :disabled="!canManageDocuments" maxlength="64" inputmode="numeric" placeholder="请输入数字 PU 码" aria-label="PU码" @input="pickupCodeInput = pickupCodeInput.replace(/\D/g, '')" /></td>
|
||||
<td>{{ overview.container.pickupCodeUpdatedByName || '—' }}</td>
|
||||
<td>{{ formatTime(overview.container.pickupCodeUpdatedAt) }}</td>
|
||||
<td><el-tag size="small" :type="overview.container.pickupCode ? 'success' : 'info'">{{ overview.container.pickupCode ? '已填写' : '未填写' }}</el-tag></td>
|
||||
</tr></tbody>
|
||||
</table></div>
|
||||
</div>
|
||||
|
||||
@@ -849,8 +849,8 @@ describe('ContainersView', () => {
|
||||
describe('ContainersView trucking supplier contract', () => {
|
||||
it('shows the frozen plan, exception and document aggregates without deriving them client-side', () => {
|
||||
const source = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/ContainersView.vue'), 'utf8')
|
||||
const fields = ['containerNo', 'planNo', 'planStatus', 'exceptionActive', 'cabinetNo', 'destinationClearanceDocumentStatus', 'deliveryOrderDocumentStatus', 'pickupCode']
|
||||
let previous = source.indexOf("key: 'actions'")
|
||||
const fields = ['containerNo', 'planStatus', 'destinationClearanceDocumentStatus', 'deliveryOrderDocumentStatus', 'exceptionActive', 'planNo', 'cabinetNo', 'pickupCode']
|
||||
let previous = source.indexOf('const allColumns')
|
||||
for (const field of fields) {
|
||||
const current = source.indexOf(`key: '${field}'`, previous + 1)
|
||||
expect(current, field).toBeGreaterThan(previous)
|
||||
|
||||
@@ -109,9 +109,9 @@ let emailSequence = 0
|
||||
const emailColumns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 90, fixed: 'left', slotName: 'emailActions' },
|
||||
{ key: 'kind', label: '类型', width: 100, formatter: (row: TerminalDrayageEmail) => row.kind === 'CANCEL_PICKUP' ? '取消提柜' : '请求提柜' },
|
||||
{ key: 'status', label: '发送状态', width: 100, formatter: (row: TerminalDrayageEmail) => ({ PENDING: '待发送', SENDING: '发送中', SENT: '已发送', FAILED: '失败待核对' })[row.status], afterColumn: 'kind' },
|
||||
{ key: 'fleetName', label: '车队', minWidth: 130 },
|
||||
{ key: 'recipient', label: '收件邮箱', minWidth: 210 },
|
||||
{ key: 'status', label: '发送状态', width: 100, formatter: (row: TerminalDrayageEmail) => ({ PENDING: '待发送', SENDING: '发送中', SENT: '已发送', FAILED: '失败待核对' })[row.status] },
|
||||
{ key: 'content', label: '邮件内容', minWidth: 270, slotName: 'emailContent' },
|
||||
{ key: 'failureReason', label: '失败说明', minWidth: 200 },
|
||||
{ key: 'createdAt', label: '创建时间', width: 165, formatter: (row: TerminalDrayageEmail) => formatTime(row.createdAt) },
|
||||
@@ -239,12 +239,12 @@ const allColumns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 240, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'emptyReturnWarning', label: '报空预警', width: 120, slotName: 'emptyReturnWarning' },
|
||||
{ key: 'containerNo', label: '集装箱号', minWidth: 150, slotName: 'containerNo' },
|
||||
{ key: 'planStatus', label: '状态', width: 110, slotName: 'status', afterColumn: 'containerNo' },
|
||||
{ key: 'destinationClearanceDocumentStatus', label: '清关资料', width: 110, formatter: (row: ContainerBookingResource) => documentStatusText(row.destinationClearanceDocumentStatus), afterColumn: 'containerNo' },
|
||||
{ key: 'deliveryOrderDocumentStatus', label: 'DO资料', width: 100, formatter: (row: ContainerBookingResource) => documentStatusText(row.deliveryOrderDocumentStatus), afterColumn: 'containerNo' },
|
||||
{ key: 'exceptionActive', label: '异常', width: 90, formatter: (row: ContainerBookingResource) => row.exceptionActive ? '异常' : '正常', afterColumn: 'containerNo' },
|
||||
{ key: 'planNo', prop: 'planNo', label: '装柜清单号', minWidth: 165, formatter: (row: ContainerBookingResource) => row.planNo || '—' },
|
||||
{ key: 'planStatus', label: '状态', width: 110, slotName: 'status' },
|
||||
{ key: 'exceptionActive', label: '异常', width: 90, formatter: (row: ContainerBookingResource) => row.exceptionActive ? '异常' : '正常' },
|
||||
{ key: 'cabinetNo', label: '柜次号', minWidth: 180, slotName: 'cabinet' },
|
||||
{ key: 'destinationClearanceDocumentStatus', label: '清关资料', width: 110, formatter: (row: ContainerBookingResource) => documentStatusText(row.destinationClearanceDocumentStatus) },
|
||||
{ key: 'deliveryOrderDocumentStatus', label: 'DO资料', width: 100, formatter: (row: ContainerBookingResource) => documentStatusText(row.deliveryOrderDocumentStatus) },
|
||||
{ key: 'pickupCode', label: 'PU码', minWidth: 140, formatter: (row: ContainerBookingResource) => row.pickupCode || '—' },
|
||||
{ key: 'oceanBooking', label: '真实订舱', minWidth: 170, slotName: 'oceanBooking' },
|
||||
{ key: 'warehouseName', prop: 'warehouseName', label: '装柜仓库', minWidth: 160 },
|
||||
|
||||
@@ -240,8 +240,8 @@ function preferredContainerId() {
|
||||
const candidateColumns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 90, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'inboundCode', label: '入仓码', minWidth: 180, fixed: 'left', slotName: 'candidateInbounds' },
|
||||
{ key: 'orderStatusSnapshot', prop: 'orderStatusSnapshot', label: '订单状态', width: 100, slotName: 'orderStatusSnapshot', afterColumn: 'inboundCode' },
|
||||
{ key: 'customerName', prop: 'customerName', label: '客户', minWidth: 120, slotName: 'customerName' },
|
||||
{ key: 'orderStatusSnapshot', prop: 'orderStatusSnapshot', label: '订单状态', width: 100, slotName: 'orderStatusSnapshot' },
|
||||
{ key: 'availablePackageCount', prop: 'availablePackageCount', label: '箱数', width: 80 },
|
||||
{ key: 'totalGrossWeightKg', label: '重量', width: 100, slotName: 'candidateTotalWeight' },
|
||||
{ key: 'totalVolumeCbm', label: '体积', width: 100, slotName: 'candidateTotalVolume' },
|
||||
|
||||
@@ -157,12 +157,12 @@ const itemColumns: DataTableColumn[] = [
|
||||
const candidateColumns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 110, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'inboundCode', label: '入仓码', minWidth: 220, fixed: 'left', slotName: 'candidateInbounds' },
|
||||
{ key: 'cartonIdentityStatus', label: '箱号归属', width: 110, slotName: 'cartonIdentityStatus', afterColumn: 'inboundCode' },
|
||||
{ key: 'palletNo', prop: 'palletNo', label: '托盘号', minWidth: 150, showOverflowTooltip: true },
|
||||
{ key: 'coLoadGroup', label: '装柜规则', minWidth: 170, slotName: 'coLoadGroup' },
|
||||
{ key: 'customerName', prop: 'customerName', label: '客户', minWidth: 150, slotName: 'customerName' },
|
||||
{ key: 'goodsSummary', prop: 'goodsSummary', label: '货物摘要', minWidth: 180, slotName: 'goodsSummary' },
|
||||
{ key: 'availablePackageCount', prop: 'availablePackageCount', label: '箱数', width: 90 },
|
||||
{ key: 'cartonIdentityStatus', label: '箱号归属', width: 110, slotName: 'cartonIdentityStatus' },
|
||||
{ key: 'averageUnitVolumeCbm', label: '单位体积', width: 120, slotName: 'averageUnitVolume' },
|
||||
{ key: 'unitGrossWeightKg', label: '单位毛重', width: 120, slotName: 'unitGrossWeight' },
|
||||
]
|
||||
|
||||
@@ -89,6 +89,18 @@ describe('CustomerBillingRulesPanel', () => {
|
||||
expect(document.body.textContent).toContain('标准中箱')
|
||||
})
|
||||
|
||||
it('shows weighted volume and volume weight with the coefficient applied to all three dimensions', async () => {
|
||||
vi.mocked(listCustomerChargeRules).mockResolvedValue([{ ...chargeRule, defaultRule: true, lengthFactor: 1.25 }])
|
||||
const wrapper = mount(CustomerBillingRulesPanel, { attachTo: document.body, props: { customerId: '8', customerName: '柏悦物流' }, global: { plugins: [ElementPlus] } })
|
||||
await flushPromises()
|
||||
|
||||
const chargeTable = wrapper.findAll('.el-table')[1]!
|
||||
const largeRow = chargeTable.findAll('.el-table__body tbody tr')[3]!
|
||||
const values = largeRow.findAll('td').map(cell => cell.text())
|
||||
expect(values[4]).toBe('0.293')
|
||||
expect(values[6]).toBe('48.93')
|
||||
})
|
||||
|
||||
it('hides permission codes in the no-access state', async () => {
|
||||
const auth = useAuthStore()
|
||||
auth.permissions = []
|
||||
|
||||
@@ -399,15 +399,15 @@ onBeforeUnmount(stopInitializationPolling)
|
||||
<div v-else-if="initializationReady" v-loading="loading" class="loading-state" />
|
||||
|
||||
<section v-if="initializationReady && canChargeView" class="rule-card charge-rule-card">
|
||||
<header class="rule-card-heading"><div><div class="rule-title-row"><h2>箱规按权重系数计算<InlineHelp title="默认规则说明"><p>勾选后客户下单数据才会按该规则调整;未勾选时默认使用实测原尺寸(不调整)。</p></InlineHelp></h2><el-checkbox :model-value="defaultCalculationMode === 'WEIGHT'" :disabled="!canEdit || defaultModeSaving" @change="selectDefaultCalculationMode('WEIGHT', Boolean($event))">设为默认规则</el-checkbox></div></div></header>
|
||||
<header class="rule-card-heading"><div><div class="rule-title-row"><h2>箱规按权重系数计算<InlineHelp title="默认规则说明"><p>勾选后才默认按该规则调整;两个默认规则均未勾选时使用实测原尺寸,仓库修正尺寸替换对应实测边。</p><p>系数分别作用于长、宽、高,加权体积 = 原体积 × 系数³。</p></InlineHelp></h2><el-checkbox :model-value="defaultCalculationMode === 'WEIGHT'" :disabled="!canEdit || defaultModeSaving" @change="selectDefaultCalculationMode('WEIGHT', Boolean($event))">设为默认规则</el-checkbox></div></div></header>
|
||||
<div class="workbench-table"><el-table v-loading="chargeLoading" :data="chargeDisplayRows" border stripe empty-text="暂无系数配置">
|
||||
<el-table-column prop="boxTypeName" label="箱规类型" min-width="110" />
|
||||
<el-table-column label="内径尺寸(长×宽×高 cm)" min-width="240"><template #default="{ row }">{{ row.dimension ? formatDimension(row.dimension.innerLengthCm, row.dimension.innerWidthCm, row.dimension.innerHeightCm) : '—' }}</template></el-table-column>
|
||||
<el-table-column label="权重系数" min-width="110"><template #default="{ row }"><strong>{{ row.coefficient }}</strong></template></el-table-column>
|
||||
<el-table-column label="体积(CBM)" min-width="120"><template #default="{ row }">{{ row.dimension ? Number(row.dimension.volumeCbm).toFixed(3) : '—' }}</template></el-table-column>
|
||||
<el-table-column label="加权体积(CBM)" min-width="145"><template #default="{ row }">{{ row.dimension ? (Number(row.dimension.volumeCbm) * row.coefficient).toFixed(3) : '—' }}</template></el-table-column>
|
||||
<el-table-column label="加权体积(CBM)" min-width="145"><template #default="{ row }">{{ row.dimension ? (Number(row.dimension.volumeCbm) * row.coefficient ** 3).toFixed(3) : '—' }}</template></el-table-column>
|
||||
<el-table-column label="重量(kg)" min-width="105"><template #default="{ row }">{{ row.weight ? row.weight.toFixed(2) : '—' }}</template></el-table-column>
|
||||
<el-table-column label="体积重(kg)" min-width="115"><template #default="{ row }">{{ row.dimension ? (Number(row.dimension.volumeCbm) * row.coefficient * 167).toFixed(2) : '—' }}</template></el-table-column>
|
||||
<el-table-column label="体积重(kg)" min-width="115"><template #default="{ row }">{{ row.dimension ? (Number(row.dimension.volumeCbm) * row.coefficient ** 3 * 167).toFixed(2) : '—' }}</template></el-table-column>
|
||||
<el-table-column prop="remark" label="备注" min-width="180" align="left" header-align="left" class-name="charge-remark-column" label-class-name="charge-remark-column"><template #default="{ row }">{{ row.remark || '—' }}</template></el-table-column>
|
||||
</el-table></div>
|
||||
</section>
|
||||
|
||||
@@ -289,14 +289,14 @@ onBeforeUnmount(() => {
|
||||
<section class="detail-panel user-panel">
|
||||
<div class="panel-title"><div><h2>企业用户<InlineHelp title="账号信息说明"><p>登录手机号属于账号信息;请在成员/账号管理中单独维护,修改会影响登录</p></InlineHelp></h2></div><UsersRound :size="19" /></div>
|
||||
<div v-if="enterpriseMembers.length" class="user-table-wrap workbench-evidence">
|
||||
<table class="density-table"><thead><tr><th>账号</th><th>成员类型</th><th>登录手机号</th><th>邮箱</th><th>角色</th><th>状态</th></tr></thead>
|
||||
<table class="density-table"><thead><tr><th>账号</th><th>状态</th><th>成员类型</th><th>登录手机号</th><th>邮箱</th><th>角色</th></tr></thead>
|
||||
<tbody><tr v-for="member in enterpriseMembers" :key="String(member.membershipId)">
|
||||
<td><div class="user-identity"><span>{{ (member.displayName || member.username || '?').slice(0, 1) }}</span><div><strong>{{ displayText(member.username) }}</strong><small>{{ displayText(member.displayName) }}</small></div></div></td>
|
||||
<td><el-tag :type="member.status === 'ACTIVE' ? 'success' : 'info'" effect="light" size="small">{{ member.status === 'ACTIVE' ? '启用' : '停用' }}</el-tag></td>
|
||||
<td>{{ member.memberType === 'OWNER' ? '企业管理员' : '企业员工' }}</td>
|
||||
<td>{{ displayText(member.phone) }}</td>
|
||||
<td>{{ displayText(member.email) }}</td>
|
||||
<td><div v-if="member.roleNames?.length" class="role-list"><el-tag v-for="role in member.roleNames" :key="role" size="small" effect="plain">{{ role }}</el-tag></div><span v-else>—</span></td>
|
||||
<td><el-tag :type="member.status === 'ACTIVE' ? 'success' : 'info'" effect="light" size="small">{{ member.status === 'ACTIVE' ? '启用' : '停用' }}</el-tag></td>
|
||||
</tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -214,6 +214,7 @@ const unifiedSocialCreditCodePattern = /^[0-9A-Z]{18}$/
|
||||
const customerColumns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 330, slotName: 'actions' },
|
||||
{ key: 'tenantCode', prop: 'tenantCode', label: '客户编号', width: 220, className: 'customer-code-column' },
|
||||
{ key: 'status', label: '状态', width: 110, slotName: 'status', afterColumn: 'tenantCode' },
|
||||
{ key: 'customerNickname', prop: 'customerNickname', label: '客户昵称', width: 120, formatter: (row: PlatformTenantResource) => displayText(row.customerNickname) },
|
||||
{ key: 'company', label: '公司名称', width: 250, slotName: 'company' },
|
||||
{ key: 'unifiedSocialCreditCode', prop: 'unifiedSocialCreditCode', label: '统一社会信用代码', width: 220, formatter: (row: PlatformTenantResource) => displayText(row.unifiedSocialCreditCode) },
|
||||
@@ -221,7 +222,6 @@ const customerColumns: DataTableColumn[] = [
|
||||
{ key: 'contactPhone', prop: 'contactPhone', label: '公司联系电话', minWidth: 150, formatter: (row: PlatformTenantResource) => displayText(row.contactPhone) },
|
||||
{ key: 'salesOwnerName', prop: 'salesOwnerName', label: '销售人员', minWidth: 120, formatter: (row: PlatformTenantResource) => displayText(row.salesOwnerName) },
|
||||
{ key: 'paymentTermDays', prop: 'paymentTermDays', label: '账期(天)', width: 105, formatter: (row: PlatformTenantResource) => row.paymentTermDays ?? 0 },
|
||||
{ key: 'status', label: '状态', width: 110, slotName: 'status' },
|
||||
{ key: 'orderCount', label: '运单数', width: 100, slotName: 'orderCount' },
|
||||
{ key: 'createdAt', prop: 'createdAt', label: '入驻时间', minWidth: 165, formatter: (row: PlatformTenantResource) => formatDate(row.createdAt) },
|
||||
]
|
||||
|
||||
@@ -27,7 +27,7 @@ const filters = ref<QueryModel>({ customerId: '', status: '' }); const fields =
|
||||
{ key: 'customerId', type: 'select', label: '客户', placeholder: '全部客户', width: 220, options: customers.value.filter((customer) => customer.status === 'ACTIVE').map((customer) => ({ label: customer.tenantName, value: String(customer.id) })), props: { filterable: true, loading: customersLoading.value } },
|
||||
{ key: 'status', type: 'select', label: '状态', width: 160, options: customsCaseStatusOptions.map((option) => ({ ...option })) },
|
||||
])
|
||||
const columns = [{ key: 'actions', label: '操作', fixed: 'left', width: 175, slotName: 'actions', hideable: false, movable: false }, { prop: 'customsNo', label: '报关编号', minWidth: 145 }, { key: 'orderLink', label: '订单关联', minWidth: 120, formatter: (row: CustomsCaseResource) => associationLabel(row.orderId) }, { key: 'inboundLink', label: '入库单关联', minWidth: 120, formatter: (row: CustomsCaseResource) => associationLabel(row.inboundId) }, { prop: 'checklistCode', label: '资料清单', minWidth: 125 }, { prop: 'checklistVersionNo', label: '版本', width: 90 }, { prop: 'status', label: '状态', width: 110, slotName: 'status' }, { prop: 'updatedAt', label: '更新时间', minWidth: 160 }]
|
||||
const columns = [{ key: 'actions', label: '操作', fixed: 'left', width: 175, slotName: 'actions', hideable: false, movable: false }, { prop: 'customsNo', label: '报关编号', minWidth: 145 }, { prop: 'status', label: '状态', width: 110, slotName: 'status', afterColumn: 'customsNo' }, { key: 'orderLink', label: '订单关联', minWidth: 120, formatter: (row: CustomsCaseResource) => associationLabel(row.orderId) }, { key: 'inboundLink', label: '入库单关联', minWidth: 120, formatter: (row: CustomsCaseResource) => associationLabel(row.inboundId) }, { prop: 'checklistCode', label: '资料清单', minWidth: 125 }, { prop: 'checklistVersionNo', label: '版本', width: 90 }, { prop: 'updatedAt', label: '更新时间', minWidth: 160 }]
|
||||
function associationLabel(value: string | null | undefined) { return value ? '已关联' : '未关联' }
|
||||
async function loadCustomers() { customersLoading.value = true; try { const result = await listPlatformTenants({ page: 1, size: 200, status: 'ACTIVE' }); customers.value = result.records.filter((customer) => customer.status === 'ACTIVE') } catch { customers.value = [] } finally { customersLoading.value = false } }
|
||||
async function loadCustomsAgents() { customsAgentsLoading.value = true; customsAgentsError.value = ''; try { const result = await listSuppliers({ page: 1, size: 200, category: 'CUSTOMS', status: 'ACTIVE' }); customsAgents.value = result.records.filter((supplier) => supplier.category === 'CUSTOMS' && supplier.status === 'ACTIVE') } catch (cause) { customsAgents.value = []; customsAgentsError.value = getApiErrorDetails(cause, '报关代理加载失败').message } finally { customsAgentsLoading.value = false } }
|
||||
|
||||
@@ -44,9 +44,9 @@ const queryFields = computed<QueryField[]>(() => {
|
||||
return base
|
||||
})
|
||||
const columns = computed<DataTableColumn[]>(() => activeTab.value === 'inventory' ? [
|
||||
{ key: 'actions', label: '操作', fixed: 'left', width: 112, slotName: 'actions', hideable: false, movable: false }, { prop: 'containerNo', label: '集装箱号', minWidth: 150 }, { prop: 'warehouseName', label: '国内仓', minWidth: 150 }, { prop: 'cargoName', label: '货物名称', minWidth: 150 }, { prop: 'inboundBoxes', label: '入库箱数', width: 105, align: 'right' }, { prop: 'outboundBoxes', label: '已出库箱数', width: 115, align: 'right' }, { prop: 'availableBoxes', label: '当前库存', width: 105, align: 'right' }, { prop: 'inboundVolumeCbm', label: '累计入库体积(m³)', width: 155, align: 'right' }, { prop: 'inventoryStatus', label: '状态', width: 110, slotName: 'status' },
|
||||
{ key: 'actions', label: '操作', fixed: 'left', width: 112, slotName: 'actions', hideable: false, movable: false }, { prop: 'containerNo', label: '集装箱号', minWidth: 150 }, { prop: 'inventoryStatus', label: '状态', width: 110, slotName: 'status', afterColumn: 'containerNo' }, { prop: 'warehouseName', label: '国内仓', minWidth: 150 }, { prop: 'cargoName', label: '货物名称', minWidth: 150 }, { prop: 'inboundBoxes', label: '入库箱数', width: 105, align: 'right' }, { prop: 'outboundBoxes', label: '已出库箱数', width: 115, align: 'right' }, { prop: 'availableBoxes', label: '当前库存', width: 105, align: 'right' }, { prop: 'inboundVolumeCbm', label: '累计入库体积(m³)', width: 155, align: 'right' },
|
||||
] : [
|
||||
{ key: 'actions', label: '操作', fixed: 'left', width: 112, slotName: 'actions', hideable: false, movable: false }, { prop: 'orderNo', label: activeTab.value === 'inbound' ? '入库单号' : '出库单号', minWidth: 155 }, { prop: 'warehouseName', label: '国内仓', minWidth: 150 }, { prop: 'containerNo', label: '集装箱号', minWidth: 150 }, { prop: 'cargoName', label: '货物名称', minWidth: 150 }, { prop: 'boxCount', label: activeTab.value === 'inbound' ? '箱子数量' : '出库箱数', width: 105, align: 'right' }, ...(activeTab.value === 'inbound' ? [{ prop: 'volumeCbm', label: '体积(m³)', width: 105, align: 'right' }] : []), { prop: 'operationTime', label: activeTab.value === 'inbound' ? '入库时间' : '出库时间', minWidth: 165, formatter: (row: ContainerOrder) => displayTime(row.operationTime) }, { prop: 'status', label: '状态', width: 105, slotName: 'status' },
|
||||
{ key: 'actions', label: '操作', fixed: 'left', width: 112, slotName: 'actions', hideable: false, movable: false }, { prop: 'orderNo', label: activeTab.value === 'inbound' ? '入库单号' : '出库单号', minWidth: 155 }, { prop: 'status', label: '状态', width: 105, slotName: 'status', afterColumn: 'orderNo' }, { prop: 'warehouseName', label: '国内仓', minWidth: 150 }, { prop: 'containerNo', label: '集装箱号', minWidth: 150 }, { prop: 'cargoName', label: '货物名称', minWidth: 150 }, { prop: 'boxCount', label: activeTab.value === 'inbound' ? '箱子数量' : '出库箱数', width: 105, align: 'right' }, ...(activeTab.value === 'inbound' ? [{ prop: 'volumeCbm', label: '体积(m³)', width: 105, align: 'right' }] : []), { prop: 'operationTime', label: activeTab.value === 'inbound' ? '入库时间' : '出库时间', minWidth: 165, formatter: (row: ContainerOrder) => displayTime(row.operationTime) },
|
||||
])
|
||||
function displayTime(value: string) { return value ? value.replace('T', ' ').slice(0, 16) : '—' }
|
||||
function statusLabel(row: ContainerOrder | InventoryRecord) { if ('inventoryStatus' in row) return ({ IN_STOCK: '在库', PARTIALLY_OUTBOUND: '部分出库', OUTBOUND: '已出库' } as const)[row.inventoryStatus]; return row.status === 'PENDING' ? (activeTab.value === 'inbound' ? '待入库' : '待出库') : (activeTab.value === 'inbound' ? '已入库' : '已出库') }
|
||||
|
||||
@@ -85,8 +85,8 @@ const queryFields: QueryField[] = [
|
||||
const columns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 210, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'taskName', prop: 'taskName', label: '任务名称', minWidth: 180, showOverflowTooltip: true },
|
||||
{ key: 'status', label: '状态', width: 110, slotName: 'status', afterColumn: 'taskName' },
|
||||
{ key: 'type', label: '任务类型', width: 120, slotName: 'type' },
|
||||
{ key: 'status', label: '状态', width: 110, slotName: 'status' },
|
||||
{ key: 'progress', label: '数据量', width: 120, slotName: 'progress' },
|
||||
{ key: 'format', prop: 'format', label: '格式', width: 80 },
|
||||
{ key: 'requester', label: '申请人', minWidth: 130, slotName: 'requester' },
|
||||
|
||||
@@ -85,8 +85,8 @@ const columns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 210, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'scope', label: '报价范围', minWidth: 160, slotName: 'scope' },
|
||||
{ key: 'version', label: '价格表版本', width: 110, slotName: 'version' },
|
||||
{ key: 'status', label: '状态', width: 100, slotName: 'status', afterColumn: 'version' },
|
||||
{ key: 'originalFileName', prop: 'originalFileName', label: '原始文件', minWidth: 250, showOverflowTooltip: true, defaultVisible: false },
|
||||
{ key: 'status', label: '状态', width: 100, slotName: 'status' },
|
||||
{ key: 'contents', label: '解析结果', minWidth: 170, slotName: 'contents' },
|
||||
{ key: 'issues', label: '校验结果', width: 140, slotName: 'issues' },
|
||||
{ key: 'createdAt', label: '上传时间', minWidth: 165, formatter: (row: PlatformPriceBookVersion) => formatDateTime(row.createdAt) },
|
||||
|
||||
@@ -119,6 +119,7 @@ const queryFields = computed<QueryField[]>(() => [
|
||||
const columns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 250, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'feeTypeName', label: '费用项目', minWidth: 210, slotName: 'feeItem' },
|
||||
{ key: 'status', label: '状态', width: 100, slotName: 'status', afterColumn: 'feeTypeName' },
|
||||
{ key: 'category', prop: 'category', label: '费用分类', minWidth: 150 },
|
||||
{ key: 'defaultPrice', label: '默认价格(CNY)', width: 150, formatter: (row: PlatformFeeItemResource) => row.defaultPrice == null ? '—' : Number(row.defaultPrice).toFixed(2) },
|
||||
{ key: 'tradeScope', label: '贸易范围', width: 110, slotName: 'tradeScope' },
|
||||
@@ -131,7 +132,6 @@ const columns: DataTableColumn[] = [
|
||||
{ key: 'createdAt', label: '创建日期', minWidth: 155, formatter: (row: PlatformFeeItemResource) => formatTime(row.createdAt) },
|
||||
{ key: 'updatedByName', label: '更新人', minWidth: 120, formatter: (row: PlatformFeeItemResource) => row.updatedByName || '—' },
|
||||
{ key: 'updatedAt', label: '更新日期', minWidth: 155, formatter: (row: PlatformFeeItemResource) => formatTime(row.updatedAt) },
|
||||
{ key: 'status', label: '状态', width: 100, slotName: 'status' },
|
||||
]
|
||||
|
||||
function optionalText(value: string | null) {
|
||||
|
||||
@@ -25,12 +25,12 @@ let requestSerial = 0
|
||||
const columns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 92, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'billNo', prop: 'billNo', label: '对账单号', minWidth: 180 },
|
||||
{ key: 'status', label: '状态', width: 110, slotName: 'status', afterColumn: 'billNo' },
|
||||
{ key: 'billDate', prop: 'billDate', label: '账单日期', width: 120 },
|
||||
{ key: 'dueDate', prop: 'dueDate', label: '到期日', width: 120 },
|
||||
{ key: 'totalAmount', label: '应收(CNY)', width: 130, align: 'right', slotName: 'totalAmount' },
|
||||
{ key: 'receivedAmount', label: '已收(CNY)', width: 130, align: 'right', slotName: 'receivedAmount' },
|
||||
{ key: 'outstandingAmount', label: '未收(CNY)', width: 130, align: 'right', slotName: 'outstandingAmount' },
|
||||
{ key: 'status', label: '状态', width: 110, slotName: 'status' },
|
||||
]
|
||||
|
||||
async function load() {
|
||||
|
||||
@@ -97,8 +97,8 @@ describe('FinanceCustomerAccountsView delivery', () => {
|
||||
expect(page).not.toContain('text-overflow: ellipsis')
|
||||
})
|
||||
|
||||
it('keeps account detail and term editing readable across the 1920 workspace', () => {
|
||||
expect(page).toContain('width: min(100%, 1920px)')
|
||||
it('keeps account detail and term editing readable across the full workspace width', () => {
|
||||
expect(page).not.toContain('width: min(100%, 1920px)')
|
||||
expect(page).toContain('@media (min-width: 1920px)')
|
||||
expect(page).toContain('finance-density-detail')
|
||||
expect(page).toContain('../financeDensity.css')
|
||||
|
||||
@@ -98,6 +98,7 @@ const columns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 290, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'customerName', label: '客户', minWidth: 180, slotName: 'customerName' },
|
||||
{ key: 'customerCode', label: '客户编号', width: 150, slotName: 'customerCode' },
|
||||
{ key: 'status', label: '客户状态', width: 110, slotName: 'status', afterColumn: 'customerCode' },
|
||||
{ key: 'contactName', label: '联系人', width: 130, slotName: 'contactName' },
|
||||
{ key: 'contactPhone', label: '联系电话', width: 150, slotName: 'contactPhone' },
|
||||
{ key: 'actualBalance', label: '实际余额(CNY)', align: 'right', width: 155, slotName: 'actualBalance' },
|
||||
@@ -107,7 +108,6 @@ const columns: DataTableColumn[] = [
|
||||
{ key: 'creditExpiresOn', label: '授信有效期', width: 140, slotName: 'creditExpiresOn' },
|
||||
{ key: 'creditUsed', label: '已用授信(CNY)', align: 'right', width: 150, slotName: 'creditUsed' },
|
||||
{ key: 'pendingRechargeCount', label: '待审核充值', align: 'right', width: 120, slotName: 'pendingRechargeCount' },
|
||||
{ key: 'status', label: '客户状态', width: 110, slotName: 'status' },
|
||||
{ key: 'lastChangedAt', label: '最近变动', width: 170, slotName: 'lastChangedAt' },
|
||||
]
|
||||
const ledgerQueryFields: QueryField[] = [
|
||||
@@ -133,10 +133,10 @@ const ledgerColumns: DataTableColumn[] = [
|
||||
]
|
||||
const rechargeColumns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 90, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'requestNo', label: '申请单号', minWidth: 190 }, { key: 'createdAt', label: '提交时间', width: 170, slotName: 'createdAt' },
|
||||
{ key: 'requestNo', label: '申请单号', minWidth: 190 },
|
||||
{ key: 'status', label: '状态', width: 110, slotName: 'status', afterColumn: 'requestNo' }, { key: 'createdAt', label: '提交时间', width: 170, slotName: 'createdAt' },
|
||||
{ key: 'amount', label: '充值金额(CNY)', width: 150, align: 'right', slotName: 'amount' },
|
||||
{ key: 'voucherFileName', label: '付款凭证', minWidth: 170, slotName: 'voucherFileName' }, { key: 'submittedByName', label: '提交人', width: 130 },
|
||||
{ key: 'status', label: '状态', width: 110, slotName: 'status' }, { key: 'reviewedByName', label: '审核人', width: 130 },
|
||||
{ key: 'voucherFileName', label: '付款凭证', minWidth: 170, slotName: 'voucherFileName' }, { key: 'submittedByName', label: '提交人', width: 130 }, { key: 'reviewedByName', label: '审核人', width: 130 },
|
||||
{ key: 'reviewRemark', label: '审核备注', minWidth: 180 }, { key: 'reviewedAt', label: '审核时间', width: 170, slotName: 'reviewedAt' },
|
||||
]
|
||||
const sortedRechargeRequests = computed(() => [...rechargeRequests.value].sort((left, right) => Number(right.status === 'PENDING') - Number(left.status === 'PENDING')))
|
||||
@@ -560,7 +560,7 @@ onBeforeUnmount(clearVoucherPreview)
|
||||
</DataTable></div>
|
||||
</FormSection>
|
||||
<FormSection v-if="!editing" class="ledger-section density-form-section" title="账户流水" :bordered="false">
|
||||
<div class="detail-table-scroll workbench-table is-page-surface"><DataTable v-model:page="ledgerPage" v-model:page-size="ledgerPageSize" class="ledger-table" appearance="integrated" :data="ledger" :columns="ledgerColumns" :loading="ledgerLoading" :total="ledgerTotal" row-key="ledgerId" empty-text="暂无账户流水" @update:page="loadLedger()" @update:page-size="ledgerPage = 1; loadLedger()"><template #filters><QueryToolbar v-model="ledgerFilters" class="ledger-query workbench-query" :fields="ledgerQueryFields" ariaLabel="账户流水筛选" preference-key="finance-account-ledger-query" @search="ledgerPage = 1; void loadLedger($event)" /></template><template #actions="{ row }"><el-button v-if="canRefund(row as FinanceLedgerEntry)" link type="primary" @click="openRefund(row as FinanceLedgerEntry)">退款</el-button><span v-else>—</span></template><template #pagination-left>共 {{ ledgerTotal }} 条</template><template #ledgerId="{ row }"><span class="ledger-identifier" :title="displayLedgerId(row.businessNo || row.ledgerId)">{{ displayLedgerId(row.businessNo || row.ledgerId) }}</span></template><template #occurredAt="{ row }">{{ row.occurredDate || formatDateTime(row.createdAt || row.occurredAt) }}</template><template #businessType="{ row }">{{ walletBusinessType(row.changeType || row.businessType) }}</template><template #documentNo="{ row }"><span v-if="row.changeType === 'REFUND'">原消费:</span><span class="ledger-document" :title="displayDocumentNo(row.referenceNo || row.sourceNo || row.documentNo)">{{ displayDocumentNo(row.referenceNo || row.sourceNo || row.documentNo) }}</span></template><template #amount="{ row }">{{ money(row.amount) }}</template><template #ledgerPaymentMethod="{ row }">{{ row.paymentMethod ? paymentMethodLabel(row.paymentMethod) : '—' }}</template><template #actualBalanceAfter="{ row }">{{ money(row.actualBalanceAfter) }}</template><template #frozenBalanceAfter="{ row }">{{ money(row.frozenBalanceAfter) }}</template><template #availableBalanceAfter="{ row }">{{ money(row.availableBalanceAfter) }}</template><template #creditUsedAfter="{ row }">{{ money(row.creditUsedAfter) }}</template><template #operatorName="{ row }">{{ displayOperator(row.operatorName) }}</template></DataTable></div>
|
||||
<div class="detail-table-scroll workbench-table is-page-surface"><DataTable v-model:page="ledgerPage" v-model:page-size="ledgerPageSize" class="ledger-table" appearance="integrated" :data="ledger" :columns="ledgerColumns" :loading="ledgerLoading" :total="ledgerTotal" row-key="ledgerId" empty-text="暂无账户流水" @update:page="loadLedger()" @update:page-size="ledgerPage = 1; loadLedger()"><template #filters><QueryToolbar v-model="ledgerFilters" class="ledger-query workbench-query" :fields="ledgerQueryFields" ariaLabel="账户流水筛选" preference-key="finance-account-ledger-query" @search="ledgerPage = 1; void loadLedger($event)" /></template><template #actions="{ row }"><div class="density-actions row-actions admin-text-actions"><el-button v-if="canRefund(row as FinanceLedgerEntry)" class="density-action" link type="primary" @click="openRefund(row as FinanceLedgerEntry)">退款</el-button><span v-else>—</span></div></template><template #pagination-left>共 {{ ledgerTotal }} 条</template><template #ledgerId="{ row }"><span class="ledger-identifier" :title="displayLedgerId(row.businessNo || row.ledgerId)">{{ displayLedgerId(row.businessNo || row.ledgerId) }}</span></template><template #occurredAt="{ row }">{{ row.occurredDate || formatDateTime(row.createdAt || row.occurredAt) }}</template><template #businessType="{ row }">{{ walletBusinessType(row.changeType || row.businessType) }}</template><template #documentNo="{ row }"><span v-if="row.changeType === 'REFUND'">原消费:</span><span class="ledger-document" :title="displayDocumentNo(row.referenceNo || row.sourceNo || row.documentNo)">{{ displayDocumentNo(row.referenceNo || row.sourceNo || row.documentNo) }}</span></template><template #amount="{ row }">{{ money(row.amount) }}</template><template #ledgerPaymentMethod="{ row }">{{ row.paymentMethod ? paymentMethodLabel(row.paymentMethod) : '—' }}</template><template #actualBalanceAfter="{ row }">{{ money(row.actualBalanceAfter) }}</template><template #frozenBalanceAfter="{ row }">{{ money(row.frozenBalanceAfter) }}</template><template #availableBalanceAfter="{ row }">{{ money(row.availableBalanceAfter) }}</template><template #creditUsedAfter="{ row }">{{ money(row.creditUsedAfter) }}</template><template #operatorName="{ row }">{{ displayOperator(row.operatorName) }}</template></DataTable></div>
|
||||
</FormSection>
|
||||
<FormSection v-else class="term-section density-form-section" title="账期设置" :bordered="false"><div class="term-editor"><span class="term-label">账期天数</span><div class="term-control"><el-input-number v-model="creditDays" :min="0" :max="3650" :step="1" placeholder="请输入账期天数" /><span class="term-unit">天</span></div></div></FormSection>
|
||||
</template>
|
||||
@@ -646,7 +646,7 @@ onBeforeUnmount(clearVoucherPreview)
|
||||
}
|
||||
|
||||
.finance-account-detail__surface {
|
||||
width: min(100%, 1920px);
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -281,9 +281,9 @@ describe('FinanceCustomerArapView G2 operations', () => {
|
||||
expect(wrapper.find('.arap-information').text()).not.toContain('T00:00:00')
|
||||
})
|
||||
|
||||
it('keeps the fee-source detail readable across the 1920 workspace', () => {
|
||||
it('keeps the fee-source detail readable across the full workspace width', () => {
|
||||
const page = readFileSync(resolve(process.cwd(), 'src/modules/finance/pages/FinanceCustomerArapView.vue'), 'utf8')
|
||||
expect(page).toContain('width: min(100%, 1920px)')
|
||||
expect(page).not.toContain('width: min(100%, 1920px)')
|
||||
expect(page).toContain('<el-descriptions border :column="3" class="arap-detail density-descriptions">')
|
||||
expect(page).not.toContain('arap-detail-grid')
|
||||
expect(page).toContain('finance-density-detail')
|
||||
|
||||
@@ -149,11 +149,11 @@ const settlementStatusSpan = computed(() => {
|
||||
})
|
||||
|
||||
const columns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 260, fixed: 'left', slotName: 'actions' }, { key: 'selection', type: 'selection', width: 48, fixed: 'left', selectable: isSelectableReceivable }, { key: 'direction', label: '收付方向', width: 100, slotName: 'direction' },
|
||||
{ key: 'customerName', label: '客户', minWidth: 180, slotName: 'customerName' }, { key: 'businessReference', label: '关联业务', minWidth: 220, slotName: 'businessReference' }, { key: 'sourceType', label: '来源类型', width: 120, slotName: 'sourceType' },
|
||||
{ key: 'feeName', label: '费用项目', minWidth: 150 }, { key: 'amount', label: '金额(CNY)', align: 'right', width: 130 }, { key: 'supplierName', label: '供应商', minWidth: 170, slotName: 'supplierName' },
|
||||
{ key: 'status', label: '来源状态', width: 130, slotName: 'sourceStatus' },
|
||||
{ key: 'reviewStatus', label: '审核状态', width: 120, slotName: 'status' }, { key: 'statementGenerated', label: '已生成对账单', width: 140, slotName: 'generated' }, { key: 'businessDate', label: '业务发生日', width: 170, slotName: 'businessDate' },
|
||||
{ key: 'actions', label: '操作', width: 260, fixed: 'left', slotName: 'actions' }, { key: 'selection', type: 'selection', width: 48, fixed: 'left', selectable: isSelectableReceivable }, { key: 'businessReference', label: '关联业务', minWidth: 220, slotName: 'businessReference' },
|
||||
{ key: 'status', label: '来源状态', width: 130, slotName: 'sourceStatus', afterColumn: 'businessReference' },
|
||||
{ key: 'reviewStatus', label: '审核状态', width: 120, slotName: 'status', afterColumn: 'businessReference' }, { key: 'statementGenerated', label: '已生成对账单', width: 140, slotName: 'generated', afterColumn: 'businessReference' }, { key: 'direction', label: '收付方向', width: 100, slotName: 'direction' },
|
||||
{ key: 'customerName', label: '客户', minWidth: 180, slotName: 'customerName' }, { key: 'sourceType', label: '来源类型', width: 120, slotName: 'sourceType' },
|
||||
{ key: 'feeName', label: '费用项目', minWidth: 150 }, { key: 'amount', label: '金额(CNY)', align: 'right', width: 130 }, { key: 'supplierName', label: '供应商', minWidth: 170, slotName: 'supplierName' }, { key: 'businessDate', label: '业务发生日', width: 170, slotName: 'businessDate' },
|
||||
]
|
||||
function customerName(customerId: string, snapshot?: string) {
|
||||
const masterName = customers.value.find((item) => String(item.id) === String(customerId))?.tenantName?.trim()
|
||||
@@ -509,7 +509,7 @@ watch(() => [route.name, String(route.params.feeSourceId || '')], ([name, feeSou
|
||||
}
|
||||
|
||||
.finance-arap-detail__surface {
|
||||
width: min(100%, 1920px);
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -302,7 +302,7 @@ describe('FinanceCustomerStatementsView', () => {
|
||||
{ label: '已结清', value: 'PAID' },
|
||||
{ label: '已作废', value: 'VOIDED' },
|
||||
])
|
||||
expect(statusColumn).toEqual({ key: 'status', label: '对账单状态', width: 120, slotName: 'status' })
|
||||
expect(statusColumn).toEqual({ key: 'status', label: '对账单状态', width: 120, slotName: 'status', afterColumn: 'billNo' })
|
||||
})
|
||||
|
||||
it('creates a formal statement export task with only non-empty filters', async () => {
|
||||
|
||||
@@ -128,9 +128,10 @@ function resetFilters() {
|
||||
|
||||
const columns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 280, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'status', label: '对账单状态', width: 120, slotName: 'status' },
|
||||
{ key: 'customerConfirmationStatus', label: '客户确认', width: 130, slotName: 'customerConfirmationStatus' },
|
||||
{ key: 'billNo', label: '对账单号', minWidth: 180 },
|
||||
{ key: 'status', label: '对账单状态', width: 120, slotName: 'status', afterColumn: 'billNo' },
|
||||
{ key: 'customerConfirmationStatus', label: '客户确认', width: 130, slotName: 'customerConfirmationStatus', afterColumn: 'billNo' },
|
||||
{ key: 'settled', label: '是否结清', width: 100, slotName: 'settled', afterColumn: 'billNo' },
|
||||
{ key: 'customerName', label: '客户', minWidth: 180, slotName: 'customerName' },
|
||||
{ key: 'periodStart', label: '结算起始日', width: 130 },
|
||||
{ key: 'periodEnd', label: '结算结束日', width: 130 },
|
||||
@@ -144,7 +145,6 @@ const columns: DataTableColumn[] = [
|
||||
{ key: 'receivedAmount', label: '已收金额(CNY)', align: 'right', width: 145 },
|
||||
{ key: 'outstandingAmount', label: '未收金额(CNY)', align: 'right', width: 145 },
|
||||
{ key: 'lastPaymentAt', label: '最后收款时间', minWidth: 170, slotName: 'lastPaymentAt' },
|
||||
{ key: 'settled', label: '是否结清', width: 100, slotName: 'settled' },
|
||||
{ key: 'remark', label: '备注', minWidth: 180 },
|
||||
{ key: 'createdByName', label: '创建人', width: 130, slotName: 'createdByName' },
|
||||
{ key: 'createdAt', label: '创建时间', minWidth: 170, slotName: 'createdAt' },
|
||||
@@ -765,7 +765,7 @@ watch(detailRouteId, (statementId) => {
|
||||
.statement-create-table td:not([colspan]) { width: 50%; }
|
||||
.statement-dialog-facts th { width: var(--admin-label-width); }
|
||||
.statement-adjust-editor { display: grid; gap: var(--admin-space-5); }
|
||||
.statement-detail-page { min-width: 0; width: min(100%, 1920px); margin: 0 auto; overflow: hidden; }
|
||||
.statement-detail-page { min-width: 0; width: 100%; margin: 0 auto; overflow: hidden; }
|
||||
.statement-summary-section { padding: var(--admin-space-4) var(--admin-space-6); border-bottom: 1px solid var(--workbench-border); }
|
||||
.statement-outstanding { color: var(--el-color-warning-dark-2); }
|
||||
.statement-detail-table { min-width: 0; }
|
||||
|
||||
@@ -139,6 +139,8 @@ const columns = computed<DataTableColumn[]>(() => props.kind === 'receivable'
|
||||
? [
|
||||
{ key: 'actions', label: '操作', width: 250, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'settlementNo', label: '结算单号', minWidth: 180 },
|
||||
{ key: 'paymentStatus', label: '收款状态', width: 120, slotName: 'paymentStatus', afterColumn: 'settlementNo' },
|
||||
{ key: 'status', label: '结算状态', width: 120, slotName: 'settlementStatus', afterColumn: 'settlementNo' },
|
||||
{ key: 'customerId', label: '客户', minWidth: 180, slotName: 'customer' },
|
||||
{ key: 'feeItemSummary', label: '费用类型', minWidth: 150, slotName: 'feeItemSummary' },
|
||||
{ key: 'currency', label: '币种', width: 80 },
|
||||
@@ -149,15 +151,15 @@ const columns = computed<DataTableColumn[]>(() => props.kind === 'receivable'
|
||||
{ key: 'businessPeriod', label: '结算周期', width: 120, slotName: 'businessPeriod' },
|
||||
{ key: 'generatedDate', label: '生成日期', width: 130, slotName: 'generatedDate' },
|
||||
{ key: 'dueDate', label: '到期日', width: 130, slotName: 'dueDate' },
|
||||
{ key: 'paymentStatus', label: '收款状态', width: 120, slotName: 'paymentStatus' },
|
||||
{ key: 'createdByName', label: '制单人', minWidth: 130, slotName: 'createdByName' },
|
||||
{ key: 'createdAt', label: '创建时间', width: 170, slotName: 'createdAt' },
|
||||
{ key: 'lastPaymentAt', label: '最后收款时间', width: 170, slotName: 'lastPaymentAt' },
|
||||
{ key: 'status', label: '结算状态', width: 120, slotName: 'settlementStatus' },
|
||||
]
|
||||
: [
|
||||
{ key: 'actions', label: '操作', width: 250, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'settlementNo', label: '结算单号', minWidth: 180 },
|
||||
{ key: 'paymentStatus', label: '付款状态', width: 120, slotName: 'paymentStatus', afterColumn: 'settlementNo' },
|
||||
{ key: 'status', label: '结算状态', width: 120, slotName: 'settlementStatus', afterColumn: 'settlementNo' },
|
||||
{ key: 'supplierName', label: '供应商', minWidth: 180, slotName: 'supplier' },
|
||||
{ key: 'customerId', label: '客户', minWidth: 180, slotName: 'customer' },
|
||||
{ key: 'feeItemSummary', label: '费用类型', minWidth: 150, slotName: 'feeItemSummary' },
|
||||
@@ -168,11 +170,9 @@ const columns = computed<DataTableColumn[]>(() => props.kind === 'receivable'
|
||||
{ key: 'businessPeriod', label: '结算周期', width: 120, slotName: 'businessPeriod' },
|
||||
{ key: 'generatedDate', label: '生成日期', width: 130, slotName: 'generatedDate' },
|
||||
{ key: 'dueDate', label: '到期日', width: 130, slotName: 'dueDate' },
|
||||
{ key: 'paymentStatus', label: '付款状态', width: 120, slotName: 'paymentStatus' },
|
||||
{ key: 'createdByName', label: '制单人', minWidth: 130, slotName: 'createdByName' },
|
||||
{ key: 'createdAt', label: '创建时间', width: 170, slotName: 'createdAt' },
|
||||
{ key: 'lastPaymentAt', label: '最后付款时间', width: 170, slotName: 'lastPaymentAt' },
|
||||
{ key: 'status', label: '结算状态', width: 120, slotName: 'settlementStatus' },
|
||||
])
|
||||
|
||||
function money(value: number) { return `¥${Number(value || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}` }
|
||||
@@ -738,7 +738,7 @@ onMounted(() => {
|
||||
.settlement-list-actions { display: flex; min-width: 0; align-items: center; flex-wrap: wrap; }
|
||||
.settlement-list-actions { flex: 0 0 auto; justify-content: flex-end; gap: var(--admin-space-3); }
|
||||
.settlement-list-actions :deep(.el-button + .el-button) { margin-left: 0; }
|
||||
.settlement-detail-page { display: flex; width: min(100%, 1920px); margin: 0 auto; min-width: 0; gap: 0; overflow: hidden; padding: 0; }
|
||||
.settlement-detail-page { display: flex; width: 100%; margin: 0 auto; min-width: 0; gap: 0; overflow: hidden; padding: 0; }
|
||||
.settlement-detail-actions { display: flex; align-items: center; gap: var(--admin-space-4); min-width: 0; flex-wrap: wrap; }
|
||||
.settlement-detail-state { margin: var(--admin-space-5) var(--admin-space-6); }
|
||||
.settlement-detail-content { min-width: 0; padding: 0 var(--admin-space-6) var(--admin-space-5); overflow: hidden; }
|
||||
|
||||
@@ -5,6 +5,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import { ApiResponseError } from '@/shared/api/http'
|
||||
import OperationErrorToast from '@/shared/components/OperationErrorToast.vue'
|
||||
import { preferenceStorageKey } from '@/shared/utils/uiLayoutPreference'
|
||||
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
|
||||
import { listSuppliers, type Supplier } from '@/modules/suppliers'
|
||||
import * as api from '../api/finance'
|
||||
@@ -110,6 +111,7 @@ function errorDetails(wrapper: VueWrapper) {
|
||||
}
|
||||
enableAutoUnmount(afterEach)
|
||||
beforeEach(() => {
|
||||
localStorage.clear()
|
||||
vi.resetAllMocks()
|
||||
can.mockReturnValue(true)
|
||||
vi.stubGlobal('ResizeObserver', class { observe() {} unobserve() {} disconnect() {} })
|
||||
@@ -129,6 +131,19 @@ beforeEach(() => {
|
||||
afterEach(() => { vi.restoreAllMocks(); vi.unstubAllGlobals(); document.body.innerHTML = '' })
|
||||
|
||||
describe('formal settlement native dialogs', () => {
|
||||
it.each(['receivable', 'payable'] as const)('keeps %s statuses after the settlement number with saved old column order', async (kind) => {
|
||||
localStorage.setItem(preferenceStorageKey('table', 'anonymous', `finance-settlement-${kind}-table`), JSON.stringify({
|
||||
version: 1,
|
||||
order: ['actions', 'settlementNo', 'customerId', 'feeItemSummary', 'currency', 'paymentStatus', 'status'],
|
||||
hidden: ['currency'],
|
||||
widths: { settlementNo: 240 },
|
||||
}))
|
||||
const wrapper = await mountPage(kind)
|
||||
const headers = wrapper.findAll('.el-table__header-wrapper th .cell').map(header => header.text())
|
||||
expect(headers.slice(0, 4)).toEqual(['操作', '结算单号', kind === 'receivable' ? '收款状态' : '付款状态', '结算状态'])
|
||||
expect(headers).not.toContain('币种')
|
||||
})
|
||||
|
||||
it('creates receivables from issued statements with original long IDs and CNY', async () => {
|
||||
const { dialog } = await openCreate('receivable')
|
||||
expect(dialog.props('width')).toContain('600px')
|
||||
|
||||
@@ -19,5 +19,6 @@ describe('Finance settlement route 1920px layout', () => {
|
||||
const shared = readFileSync(resolve(process.cwd(), 'src/modules/finance/pages/FinanceFormalSettlementPage.vue'), 'utf8')
|
||||
expect(shared).toContain(':column="4" class="settlement-detail-info density-descriptions"')
|
||||
expect(shared).toContain('finance-density-detail')
|
||||
expect(shared).not.toContain('width: min(100%, 1920px)')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -156,9 +156,9 @@ const formRules = computed<FormRules>(() => ({
|
||||
const userColumns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 170, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'username', prop: 'username', label: '用户名', minWidth: 145 },
|
||||
{ key: 'status', label: '状态', width: 100, slotName: 'status', afterColumn: 'username' },
|
||||
{ key: 'displayName', prop: 'displayName', label: '姓名', minWidth: 130 },
|
||||
{ key: 'roles', label: '角色', minWidth: 180, slotName: 'roles' },
|
||||
{ key: 'status', label: '状态', width: 100, slotName: 'status' },
|
||||
{ key: 'lastLoginAt', label: '最后登录', minWidth: 170, slotName: 'lastLoginAt' },
|
||||
]
|
||||
|
||||
|
||||
@@ -92,12 +92,12 @@ const parentOptions = computed(() => permissions.value.filter((item) => (
|
||||
const permissionColumns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 280, fixed: 'left', slotName: 'actions', className: 'permission-actions-column' },
|
||||
{ key: 'name', label: '权限名称', minWidth: 260, slotName: 'name' },
|
||||
{ key: 'status', label: '状态', width: 88, slotName: 'status', afterColumn: 'name' },
|
||||
{ key: 'menuCount', label: '菜单数', width: 90, align: 'right', formatter: (row: PermissionTreeNode) => permissionCounts(row).menuCount },
|
||||
{ key: 'actionCount', label: '操作数', width: 90, align: 'right', formatter: (row: PermissionTreeNode) => permissionCounts(row).actionCount },
|
||||
{ key: 'type', prop: 'type', label: '类型', width: 80 },
|
||||
{ key: 'endpoint', label: '路由 / 接口', minWidth: 260, slotName: 'endpoint' },
|
||||
{ key: 'sort', prop: 'sort', label: '排序', width: 72, align: 'right' },
|
||||
{ key: 'status', label: '状态', width: 88, slotName: 'status' },
|
||||
]
|
||||
|
||||
async function loadPermissions() {
|
||||
|
||||
@@ -85,11 +85,11 @@ const roleColumns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 250, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'role', label: '角色名称', minWidth: 180, slotName: 'role' },
|
||||
{ key: 'roleCode', prop: 'code', label: '角色编码', minWidth: 180, showOverflowTooltip: true },
|
||||
{ key: 'status', label: '状态', width: 88, slotName: 'status', afterColumn: 'roleCode' },
|
||||
{ key: 'description', prop: 'description', label: '说明', minWidth: 240, showOverflowTooltip: true },
|
||||
{ key: 'permissionSummary', label: '授权范围', minWidth: 170, slotName: 'permissionSummary' },
|
||||
{ key: 'userCount', prop: 'userCount', label: '用户数', width: 86, align: 'right' },
|
||||
{ key: 'roleType', label: '类型', width: 95, slotName: 'roleType' },
|
||||
{ key: 'status', label: '状态', width: 88, slotName: 'status' },
|
||||
]
|
||||
const availablePermissions = computed(() => serverPermissions.value)
|
||||
const permissionTree = computed(() => buildNavigationPermissionTree(availablePermissions.value, adminNavigation))
|
||||
|
||||
@@ -102,8 +102,8 @@ const columns = computed<DataTableColumn[]>(() => [
|
||||
selectable: (row: InboundOrderSummary) => row.measureReviewStatus === 'PENDING' && !batchReviewLoading.value,
|
||||
} satisfies DataTableColumn] : []),
|
||||
{ key: 'inboundCode', label: '入仓码', minWidth: 140, slotName: 'inboundCode' },
|
||||
{ key: 'processStatus', label: '流程状态', minWidth: 132, slotName: 'processStatus' },
|
||||
{ key: 'measureReviewStatus', label: '测量审核状态', minWidth: 120, slotName: 'measureReviewStatus' },
|
||||
{ key: 'processStatus', label: '流程状态', minWidth: 132, slotName: 'processStatus', afterColumn: 'inboundCode' },
|
||||
{ key: 'measureReviewStatus', label: '测量审核状态', minWidth: 120, slotName: 'measureReviewStatus', afterColumn: 'inboundCode' },
|
||||
{ key: 'expectedArrivalAt', label: '预计到达时间', minWidth: 140, slotName: 'expectedArrivalAt' },
|
||||
{ key: 'customerName', label: '客户', minWidth: 140, slotName: 'customerName' },
|
||||
{ key: 'warehouseName', label: '仓库', minWidth: 120, slotName: 'warehouseName' },
|
||||
|
||||
@@ -404,7 +404,7 @@ describe('InboundMeasurementPanel', () => {
|
||||
|
||||
const payload = vi.mocked(saveInboundMeasurementDraft).mock.calls[0]?.[2]
|
||||
expect(payload?.items[0]).toMatchObject({
|
||||
itemNo: '001', outerLengthCm: 55, outerWidthCm: null, outerHeightCm: null,
|
||||
itemNo: '001', outerLengthCm: 55, outerWidthCm: null, outerHeightCm: null, coefficientMode: 'ACTUAL',
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -492,7 +492,7 @@ async function saveDraft() {
|
||||
intelligentImageFileId: item.intelligentImageFileId, overLimitWarning: item.overLimitWarning,
|
||||
outerLengthCm: item.outerLengthCm, outerWidthCm: item.outerWidthCm, outerHeightCm: item.outerHeightCm,
|
||||
chargeRuleId: item.coefficientMode === 'MULTIPLY' ? item.chargeRuleId : null,
|
||||
coefficientMode: item.coefficientMode === 'ACTUAL' ? null : item.coefficientMode, remark: item.remark,
|
||||
coefficientMode: item.coefficientMode, remark: item.remark,
|
||||
})),
|
||||
})
|
||||
version = saved.version
|
||||
|
||||
@@ -117,17 +117,17 @@ const queryFields = computed(() => showAdvancedFilters.value
|
||||
const columns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 228, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'inboundCode', label: '入仓码', minWidth: 166, slotName: 'inbound' },
|
||||
{ key: 'processStatus', label: '入库流程状态', minWidth: 132, slotName: 'processStatus' },
|
||||
{ key: 'orderStatus', label: '订单状态', minWidth: 120, slotName: 'orderStatus' },
|
||||
{ key: 'measureReviewStatus', label: '测量数据审核状态', minWidth: 142, slotName: 'measureReviewStatus' },
|
||||
{ key: 'processStatus', label: '入库流程状态', minWidth: 132, slotName: 'processStatus', afterColumn: 'inboundCode' },
|
||||
{ key: 'orderStatus', label: '订单状态', minWidth: 120, slotName: 'orderStatus', afterColumn: 'inboundCode' },
|
||||
{ key: 'measureReviewStatus', label: '测量数据审核状态', minWidth: 142, slotName: 'measureReviewStatus', afterColumn: 'inboundCode' },
|
||||
{ key: 'feeStatus', label: '费用状态', minWidth: 112, slotName: 'feeStatus', afterColumn: 'inboundCode' },
|
||||
{ key: 'feeReviewStatus', label: '费用审核状态', minWidth: 132, slotName: 'feeReviewStatus', afterColumn: 'inboundCode' },
|
||||
{ key: 'loadingStatus', label: '配载状态', minWidth: 110, slotName: 'loadingStatus', defaultVisible: false, afterColumn: 'inboundCode' },
|
||||
{ key: 'holdFlag', label: '是否扣货', width: 96, slotName: 'holdFlag', defaultVisible: false, afterColumn: 'inboundCode' },
|
||||
{ key: 'expectedArrivalAt', label: '预计到达时间', minWidth: 164, slotName: 'expectedArrivalAt' },
|
||||
{ key: 'feeStatus', label: '费用状态', minWidth: 112, slotName: 'feeStatus' },
|
||||
{ key: 'feeReviewStatus', label: '费用审核状态', minWidth: 132, slotName: 'feeReviewStatus' },
|
||||
{ key: 'customerName', label: '客户', minWidth: 142, slotName: 'customerName' },
|
||||
{ key: 'warehouseName', label: '仓库', minWidth: 140, slotName: 'warehouseName' },
|
||||
{ key: 'deliveryWarehouseName', label: '交货仓', minWidth: 140, slotName: 'deliveryWarehouseName' },
|
||||
{ key: 'loadingStatus', label: '配载状态', minWidth: 110, slotName: 'loadingStatus', defaultVisible: false },
|
||||
{ key: 'holdFlag', label: '是否扣货', width: 96, slotName: 'holdFlag', defaultVisible: false },
|
||||
{ key: 'currentHoldReason', label: '扣货理由', minWidth: 150, slotName: 'currentHoldReason', defaultVisible: false },
|
||||
{ key: 'containerNo', label: '集装箱号', minWidth: 132, slotName: 'containerNo', defaultVisible: false },
|
||||
{ key: 'cabinetNo', label: '柜号', minWidth: 118, slotName: 'cabinetNo', defaultVisible: false },
|
||||
|
||||
@@ -96,6 +96,8 @@ const selectedFeeItem = computed(() => feeItems.value.find((item) => item.id ===
|
||||
const columns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', fixed: 'left', width: 270, slotName: 'actions', hideable: false, movable: false },
|
||||
{ key: 'feeItemName', prop: 'feeItemName', label: '费用项目', minWidth: 150 },
|
||||
{ key: 'reviewStatus', label: '审核状态', width: 110, slotName: 'reviewStatus', afterColumn: 'feeItemName' },
|
||||
{ key: 'sourceStatus', label: '来源状态', width: 100, slotName: 'sourceStatus', afterColumn: 'feeItemName' },
|
||||
{ key: 'direction', label: '方向', width: 90, slotName: 'direction' },
|
||||
{ key: 'settlementMode', label: '结算方式', width: 120, slotName: 'settlementMode' },
|
||||
{ key: 'paidSummary', label: '已付摘要', minWidth: 290, slotName: 'paidSummary' },
|
||||
@@ -103,8 +105,6 @@ const columns: DataTableColumn[] = [
|
||||
{ key: 'businessReference', label: '费用归属', minWidth: 220, slotName: 'businessReference' },
|
||||
{ key: 'amount', label: '金额(CNY)', width: 120, align: 'right', slotName: 'amount' },
|
||||
{ key: 'businessDate', prop: 'businessDate', label: '业务日期', width: 120 },
|
||||
{ key: 'reviewStatus', label: '审核状态', width: 110, slotName: 'reviewStatus' },
|
||||
{ key: 'sourceStatus', label: '来源状态', width: 100, slotName: 'sourceStatus' },
|
||||
{ key: 'remark', prop: 'remark', label: '备注', minWidth: 180, showOverflowTooltip: true },
|
||||
]
|
||||
const allocationColumns: DataTableColumn[] = [
|
||||
|
||||
@@ -60,13 +60,13 @@ const fields = computed<QueryField[]>(() => [
|
||||
const columns = computed<DataTableColumn[]>(() => isPreload.value ? [
|
||||
{ key: 'actions', label: '操作', fixed: 'left', width: 88, slotName: 'actions', hideable: false, movable: false },
|
||||
{ key: 'preloadNo', prop: 'preloadNo', label: '预装单号', minWidth: 120 },
|
||||
{ key: 'status', label: '状态', width: 108, slotName: 'status', afterColumn: 'preloadNo' },
|
||||
{ key: 'containerNo', prop: 'containerNo', label: '集装箱号', minWidth: 120 },
|
||||
{ key: 'cabinetNo', prop: 'cabinetNo', label: '柜次号', minWidth: 140 },
|
||||
// { key: 'customers', label: '客户', minWidth: 190, slotName: 'customers' },
|
||||
{ key: 'warehouseName', prop: 'warehouseName', label: '仓库', minWidth: 80 },
|
||||
{ key: 'vesselVoyage', label: '船名 / 航次', minWidth: 175, slotName: 'vesselVoyage' },
|
||||
{ key: 'containerType', prop: 'containerType', label: '箱型', width: 80 },
|
||||
{ key: 'status', label: '状态', width: 108, slotName: 'status' },
|
||||
{ key: 'totalPackageCount', prop: 'totalPackageCount', label: '总箱数', width: 90, align: 'right' },
|
||||
{ key: 'skuCount', prop: 'skuCount', label: 'SKU 数', width: 90, align: 'right' },
|
||||
{ key: 'totalVolumeCbm', label: '总体积(m³)', width: 90, align: 'right', slotName: 'volume' },
|
||||
@@ -75,10 +75,10 @@ const columns = computed<DataTableColumn[]>(() => isPreload.value ? [
|
||||
] : [
|
||||
{ key: 'actions', label: '操作', fixed: 'left', width: 150, slotName: 'actions', hideable: false, movable: false },
|
||||
{ key: 'loadingListNo', prop: 'loadingListNo', label: '装柜清单号', minWidth: 175 },
|
||||
{ key: 'status', label: '状态', width: 110, slotName: 'status', afterColumn: 'loadingListNo' },
|
||||
{ key: 'containerNo', prop: 'containerNo', label: '集装箱号', minWidth: 145 },
|
||||
{ key: 'cabinetNo', prop: 'cabinetNo', label: '柜次号', minWidth: 150 },
|
||||
{ key: 'warehouseName', prop: 'warehouseName', label: '仓库', minWidth: 140 },
|
||||
{ key: 'status', label: '状态', width: 110, slotName: 'status' },
|
||||
{ key: 'totalPackageCount', prop: 'totalPackageCount', label: '总箱数', width: 90, align: 'right' },
|
||||
{ key: 'totalGrossWeightKg', label: '总毛重(kg)', width: 125, align: 'right', slotName: 'grossWeight' },
|
||||
{ key: 'totalNetWeightKg', label: '总净重(kg)', width: 125, align: 'right', slotName: 'netWeight' },
|
||||
|
||||
@@ -48,7 +48,7 @@ watch([planId, warehouseId], () => void loadDetail())
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.confirmed-loading-detail-page { display: grid; min-width: 0; max-width: 1280px; gap: 16px; }
|
||||
@media (min-width: 1920px) { .confirmed-loading-detail-page { width: 100%; max-width: none; gap: 12px; } }
|
||||
.confirmed-loading-detail-page { display: grid; min-width: 0; width: 100%; gap: 16px; }
|
||||
@media (min-width: 1920px) { .confirmed-loading-detail-page { gap: 12px; } }
|
||||
@media print { .admin-page.admin-workbench { padding: 0; } }
|
||||
</style>
|
||||
|
||||
@@ -157,12 +157,12 @@ const partnerColumns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 250, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'partner', label: '单位名称', minWidth: 210, slotName: 'partner' },
|
||||
{ key: 'partnerCode', prop: 'partnerCode', label: '单位编码', minWidth: 130, defaultVisible: false },
|
||||
{ key: 'status', label: '状态', width: 90, slotName: 'status', afterColumn: 'partnerCode' },
|
||||
{ key: 'type', label: '类型', minWidth: 115, slotName: 'type' },
|
||||
{ key: 'contact', prop: 'contactName', label: '联系人', minWidth: 130 },
|
||||
{ key: 'contactPhone', prop: 'contactPhone', label: '电话', minWidth: 145 },
|
||||
{ key: 'contactEmail', prop: 'contactEmail', label: '邮箱', minWidth: 190, defaultVisible: false, formatter: (row: PlatformPartnerResource) => row.contactEmail || '—' },
|
||||
{ key: 'address', label: '地址', minWidth: 240, slotName: 'address', defaultVisible: false },
|
||||
{ key: 'status', label: '状态', width: 90, slotName: 'status' },
|
||||
{ key: 'updatedAt', prop: 'updatedAt', label: '更新时间', minWidth: 140, defaultVisible: false, formatter: (row: PlatformPartnerResource) => formatUpdatedAt(row.updatedAt) },
|
||||
]
|
||||
|
||||
|
||||
@@ -124,9 +124,9 @@ const expressCompanyColumns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 170, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'company', label: '公司名称', minWidth: 210, slotName: 'company' },
|
||||
{ key: 'companyCode', prop: 'companyCode', label: '编码', minWidth: 120 },
|
||||
{ key: 'status', label: '状态', width: 100, slotName: 'status', afterColumn: 'companyCode' },
|
||||
{ key: 'website', label: '官方网站', minWidth: 210, slotName: 'website' },
|
||||
{ key: 'servicePhone', prop: 'servicePhone', label: '客服电话', minWidth: 130, formatter: (row: PlatformExpressCompanyResource) => row.servicePhone || '—' },
|
||||
{ key: 'status', label: '状态', width: 100, slotName: 'status' },
|
||||
{ key: 'updatedAt', prop: 'updatedAt', label: '更新时间', minWidth: 150, defaultVisible: false, formatter: (row: PlatformExpressCompanyResource) => formatUpdatedAt(row.updatedAt) },
|
||||
]
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import MessageComposerDialog from '../components/MessageComposerDialog.vue'; imp
|
||||
import { notificationAppLabel, notificationCategoryLabel, notificationRecipientStatusLabel } from '../presentation'
|
||||
const access=useAccessPreviewStore(),page=ref(1),pageSize=ref(20),loading=ref(false),actionId=ref(''),error=ref<ApiErrorDetails|null>(null),composerOpen=ref(false),detailOpen=ref(false),detailLoading=ref(false),detailError=ref<ApiErrorDetails|null>(null),detail=ref<NotificationTaskDetail|null>(null),templates=ref<NotificationTemplate[]>([]),result=ref({records:[] as NotificationTask[],total:0,pages:0}),filters=ref<QueryModel>({category:'',status:'',keyword:''})
|
||||
const queryFields:QueryField[]=[{key:'category',type:'input',label:'业务分类',placeholder:'业务分类'},{key:'status',type:'select',label:'任务状态',placeholder:'全部状态',options:['CREATED','SCHEDULED','DISPATCHING','RETRYING','SENT','DEAD','CANCELLED'].map(value=>({label:statusLabel(value as NotificationTaskStatus),value}))},{key:'keyword',type:'input',label:'任务号或标题',placeholder:'任务号/消息标题'}]
|
||||
const columns:DataTableColumn[]=[{key:'actions',label:'操作',width:240,fixed:'left',slotName:'actions'},{key:'taskNo',prop:'taskNo',label:'任务号',minWidth:190},{key:'title',prop:'title',label:'标题',minWidth:220},{key:'category',label:'业务分类',width:130,slotName:'category'},{key:'recipientCount',prop:'recipientCount',label:'接收人数',width:100},{key:'scheduledAt',label:'定时发送',width:170,slotName:'scheduledAt'},{key:'sentAt',label:'发送时间',width:170,slotName:'sentAt'},{key:'status',label:'状态',width:110,slotName:'status'}]
|
||||
const columns:DataTableColumn[]=[{key:'actions',label:'操作',width:240,fixed:'left',slotName:'actions'},{key:'taskNo',prop:'taskNo',label:'任务号',minWidth:190},{key:'status',label:'状态',width:110,slotName:'status', afterColumn: 'taskNo' },{key:'title',prop:'title',label:'标题',minWidth:220},{key:'category',label:'业务分类',width:130,slotName:'category'},{key:'recipientCount',prop:'recipientCount',label:'接收人数',width:100},{key:'scheduledAt',label:'定时发送',width:170,slotName:'scheduledAt'},{key:'sentAt',label:'发送时间',width:170,slotName:'sentAt'}]
|
||||
const detailRow = ref<NotificationTask | null>(null)
|
||||
let detailRequest = 0
|
||||
watch(detailOpen, value => { if (!value) { detailRequest++; detailRow.value = null } })
|
||||
@@ -43,8 +43,8 @@ watch([page,pageSize],load);onMounted(()=>{void load();void loadTemplates()})
|
||||
<h3 class="density-dialog-section__title">收件人投递明细</h3>
|
||||
<div class="workbench-table"><el-table :data="detail.recipients" row-key="id" empty-text="暂无收件人明细" size="small">
|
||||
<el-table-column prop="displayName" label="收件人" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column label="应用端" width="110"><template #default="{row}">{{notificationAppLabel(row.appCode)}}</template></el-table-column>
|
||||
<el-table-column label="投递状态" width="120"><template #default="{row}">{{notificationRecipientStatusLabel(row.status)}}</template></el-table-column>
|
||||
<el-table-column label="应用端" width="110"><template #default="{row}">{{notificationAppLabel(row.appCode)}}</template></el-table-column>
|
||||
<el-table-column label="送达时间" width="180"><template #default="{row}">{{formatDate(row.deliveredAt)}}</template></el-table-column>
|
||||
<el-table-column prop="failureReason" label="失败原因" min-width="220"><template #default="{row}"><span class="message-body">{{row.failureReason||'—'}}</span></template></el-table-column>
|
||||
</el-table></div>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { onMounted, ref, watch } from 'vue'; import { Plus } from '@lucide/vue';
|
||||
import AdminPage from '@/shared/components/AdminPage.vue'; import DataTable,{type DataTableColumn} from '@/shared/components/DataTable.vue'; import ListPageLayout from '@/shared/components/ListPageLayout.vue'; import QueryToolbar,{type QueryField,type QueryModel} from '@/shared/components/QueryToolbar.vue'; import LoadErrorState from '@/shared/components/LoadErrorState.vue'; import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'; import TemplateEditorDialog from '../components/TemplateEditorDialog.vue'; import { changeTemplateStatus, listTemplates } from '../api/notificationCenter'; import type { NotificationTemplate, TemplateStatus } from '../types'; import { getApiErrorDetails,getApiErrorMessage,type ApiErrorDetails } from '@/shared/api/http'; import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
const access=useAccessPreviewStore(),page=ref(1),pageSize=ref(20),loading=ref(false),actionId=ref(''),error=ref<ApiErrorDetails|null>(null),editorOpen=ref(false),editing=ref<NotificationTemplate|null>(null),result=ref({records:[] as NotificationTemplate[],total:0,pages:0}),filters=ref<QueryModel>({code:'',category:'',title:'',status:''})
|
||||
const queryFields:QueryField[]=[{key:'code',type:'input',label:'模板CODE',placeholder:'模板CODE'},{key:'category',type:'input',label:'业务分类',placeholder:'业务分类'},{key:'title',type:'input',label:'名称或标题',placeholder:'模板名称/标题'},{key:'status',type:'select',label:'状态',placeholder:'全部状态',options:[{label:'草稿',value:'DRAFT'},{label:'已激活',value:'ACTIVE'},{label:'已禁用',value:'DISABLED'}]}]
|
||||
const columns:DataTableColumn[]=[{key:'actions',label:'操作',width:190,fixed:'left',slotName:'actions'},{key:'templateCode',prop:'templateCode',label:'模板CODE',minWidth:190},{key:'templateName',prop:'templateName',label:'模板名称',minWidth:170},{key:'category',prop:'category',label:'业务分类',width:130},{key:'titleTemplate',prop:'titleTemplate',label:'消息标题模板',minWidth:220},{key:'bodyTemplate',prop:'bodyTemplate',label:'正文模板',minWidth:320,showOverflowTooltip:true},{key:'status',label:'状态',width:100,slotName:'status'},{key:'version',prop:'version',label:'版本',width:80}]
|
||||
const columns:DataTableColumn[]=[{key:'actions',label:'操作',width:190,fixed:'left',slotName:'actions'},{key:'templateCode',prop:'templateCode',label:'模板CODE',minWidth:190},{key:'status',label:'状态',width:100,slotName:'status', afterColumn: 'templateCode' },{key:'templateName',prop:'templateName',label:'模板名称',minWidth:170},{key:'category',prop:'category',label:'业务分类',width:130},{key:'titleTemplate',prop:'titleTemplate',label:'消息标题模板',minWidth:220},{key:'bodyTemplate',prop:'bodyTemplate',label:'正文模板',minWidth:320,showOverflowTooltip:true},{key:'version',prop:'version',label:'版本',width:80}]
|
||||
function statusLabel(v:TemplateStatus){return {DRAFT:'草稿',ACTIVE:'已激活',DISABLED:'已禁用'}[v]}
|
||||
async function load(){loading.value=true;error.value=null;try{const d=await listTemplates({page:page.value,size:pageSize.value,templateCode:String(filters.value.code||'').trim()||undefined,category:String(filters.value.category||'').trim()||undefined,title:String(filters.value.title||'').trim()||undefined,status:(filters.value.status||undefined) as TemplateStatus|undefined});result.value={records:d.records,total:d.total,pages:d.pages}}catch(e){error.value=getApiErrorDetails(e,'模板加载失败')}finally{loading.value=false}}
|
||||
function search(){page.value===1?void load():page.value=1}function create(){editing.value=null;editorOpen.value=true}function edit(row:NotificationTemplate){editing.value=row;editorOpen.value=true}
|
||||
|
||||
@@ -41,10 +41,10 @@ const queryFields: QueryField[] = [
|
||||
const columns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 190, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'title', prop: 'title', label: '消息标题', minWidth: 220 },
|
||||
{ key: 'status', label: '状态', width: 90, slotName: 'status', afterColumn: 'title' },
|
||||
{ key: 'summary', prop: 'summary', label: '内容摘要', minWidth: 300, showOverflowTooltip: true },
|
||||
{ key: 'category', label: '分类', width: 130, slotName: 'category' },
|
||||
{ key: 'sentAt', label: '发送时间', width: 180, slotName: 'sentAt' },
|
||||
{ key: 'status', label: '状态', width: 90, slotName: 'status' },
|
||||
]
|
||||
|
||||
function statusLabel(status: NotificationInboxStatus) { return status === 'UNREAD' ? '未读' : '已读' }
|
||||
|
||||
@@ -43,6 +43,7 @@ const fields = computed<QueryField[]>(() => [
|
||||
const columns = [
|
||||
{ key: 'actions', label: '操作', fixed: 'left', width: 196, slotName: 'actions', hideable: false, movable: false },
|
||||
{ prop: 'bookingNo', label: '订舱号', minWidth: 150 },
|
||||
{ prop: 'status', label: '状态', minWidth: 110, slotName: 'status', afterColumn: 'bookingNo' },
|
||||
{ prop: 'customerName', label: '客户', minWidth: 160, formatter: (row: OceanBookingResource) => row.customerName || '—' },
|
||||
{ prop: 'billOfLadingNo', label: '提单号', minWidth: 155, formatter: (row: OceanBookingResource) => row.billOfLadingNo || '—' },
|
||||
{ prop: 'shipperName', label: '提单发货人', minWidth: 160, formatter: (row: OceanBookingResource) => row.shipperName || '—' },
|
||||
@@ -55,7 +56,6 @@ const columns = [
|
||||
{ prop: 'pod', label: '目的港', minWidth: 125 },
|
||||
{ prop: 'etd', label: '预计开船', minWidth: 155, slotName: 'etd' },
|
||||
{ prop: 'eta', label: '预计到港', minWidth: 155, slotName: 'eta' },
|
||||
{ prop: 'status', label: '状态', minWidth: 110, slotName: 'status' },
|
||||
{ prop: 'updatedAt', label: '更新时间', minWidth: 155, slotName: 'updatedAt' },
|
||||
]
|
||||
function formatDateTime(value?: string | null) { return value ? value.replace('T', ' ').slice(0, 16) : '—' }
|
||||
|
||||
@@ -49,13 +49,13 @@ const fields = computed<QueryField[]>(() => [
|
||||
const columns = [
|
||||
{ key: 'actions', label: '操作', fixed: 'left', width: 240, slotName: 'actions', hideable: false, movable: false },
|
||||
{ prop: 'inboundCode', label: '入仓码', minWidth: 145 },
|
||||
{ prop: 'oceanBookingStatus', label: '订舱', width: 105, slotName: 'booking', afterColumn: 'inboundCode' },
|
||||
{ prop: 'truckAppointmentStatus', label: '拖车', width: 105, slotName: 'truck', afterColumn: 'inboundCode' },
|
||||
{ prop: 'pickTaskStatus', label: '拣货', width: 105, slotName: 'pick', afterColumn: 'inboundCode' },
|
||||
{ prop: 'dispatchStatus', label: '出仓状态', width: 115, slotName: 'status', afterColumn: 'inboundCode' },
|
||||
{ prop: 'customerName', label: '客户', minWidth: 140 },
|
||||
{ prop: 'cabinetNo', label: '柜次号', minWidth: 145, formatter: (row: OutboundDispatchResource) => row.cabinetNo || '—' },
|
||||
{ prop: 'containerNo', label: '集装箱号', minWidth: 145, formatter: (row: OutboundDispatchResource) => row.containerNo || '—' },
|
||||
{ prop: 'oceanBookingStatus', label: '订舱', width: 105, slotName: 'booking' },
|
||||
{ prop: 'truckAppointmentStatus', label: '拖车', width: 105, slotName: 'truck' },
|
||||
{ prop: 'pickTaskStatus', label: '拣货', width: 105, slotName: 'pick' },
|
||||
{ prop: 'dispatchStatus', label: '出仓状态', width: 115, slotName: 'status' },
|
||||
{ key: 'gate', label: '门禁', width: 90, slotName: 'gate' },
|
||||
]
|
||||
|
||||
|
||||
@@ -47,14 +47,14 @@ const collectionLabel = (fee: OverseasFee) => fee.direction === 'PAYABLE' ? '推
|
||||
const columns = computed<DataTableColumn[]>(() => [
|
||||
{ key: 'actions', label: '操作', width: 220, fixed: 'left', slotName: 'actions' },
|
||||
{ prop: 'feeItemName', label: '费用项目', minWidth: 150 },
|
||||
{ prop: 'reviewStatus', label: '审核状态', width: 100, slotName: 'reviewStatus', afterColumn: 'feeItemName' },
|
||||
{ key: 'chargeStatus', label: '费用状态', width: 120, formatter: feeProcessingLabel, afterColumn: 'feeItemName' },
|
||||
...(props.direction === 'PAYABLE' ? [{ prop: 'supplierName', label: '供应商', minWidth: 180 }] : []),
|
||||
{ key: 'amount', label: '金额', width: 150, align: 'right', formatter: (row: OverseasFee) => feeMoney(row.originalAmount, row.originalCurrency) },
|
||||
{ key: 'settlementAmount', label: '结算金额(CNY)', width: 145, align: 'right', formatter: (row: OverseasFee) => feeMoney(row.amount) },
|
||||
{ prop: 'collectionMode', label: '结算方式', width: 110, formatter: collectionLabel },
|
||||
{ prop: 'reviewStatus', label: '审核状态', width: 100, slotName: 'reviewStatus' },
|
||||
{ prop: 'reviewerName', label: '审核人', width: 120, formatter: (row: OverseasFee) => row.reviewerName || row.reviewerId || '—' },
|
||||
{ prop: 'reviewedAt', label: '审核时间', width: 160, formatter: (row: OverseasFee) => feeTime(row.reviewedAt) },
|
||||
{ key: 'chargeStatus', label: '费用状态', width: 120, formatter: feeProcessingLabel },
|
||||
{ prop: 'businessDate', label: '费用日期', width: 110 },
|
||||
])
|
||||
let loadSequence = 0, detailSequence = 0, itemsSequence = 0
|
||||
|
||||
@@ -123,7 +123,7 @@ async function save() {
|
||||
|
||||
<template>
|
||||
<AppDialog append-to-body v-model="open" :title="title" width="680px" class="admin-density-dialog admin-workbench is-dialog" confirm-text="保存" :confirm-disabled="optionsLoading || !access.can(feePermissions.edit)" :confirm-loading="saving" fullscreenable @confirm="save">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-position="top" class="density-form" :disabled="saving">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-position="top" class="density-form density-form--outlined" :disabled="saving">
|
||||
<div class="fee-editor-grid">
|
||||
<el-form-item v-if="!business" label="业务类型" prop="businessType"><el-select v-model="form.businessType" :disabled="!!fee" @change="changeBusinessType"><el-option v-for="type in selectableTypes" :key="type" :value="type" :label="businessLabels[type]" /></el-select></el-form-item>
|
||||
<el-form-item v-if="!business" label="业务订单" prop="businessId"><el-select v-model="form.businessId" filterable remote :remote-method="loadOptions" :loading="optionsLoading" :disabled="!!fee" placeholder="搜索订单号"><el-option v-for="row in options" :key="row.businessId" :value="row.businessId" :label="`${row.businessNo} · ${row.customerName}`" /></el-select></el-form-item>
|
||||
@@ -144,7 +144,7 @@ async function save() {
|
||||
.fee-editor-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 20px; }
|
||||
.fee-editor-grid :deep(.el-select), .fee-editor-grid :deep(.el-date-editor), .fee-editor-grid :deep(.el-input-number) { width: 100%; }
|
||||
.fee-remark { grid-column: 1 / -1; }
|
||||
.fee-existing { color: var(--cc-text-secondary, #606266); font-size: 13px; margin-bottom: 12px; }
|
||||
.fee-existing { color: var(--cc-text-secondary, #606266); font-size: var(--admin-font-body); margin-bottom: var(--admin-space-5); }
|
||||
.fee-existing p { margin: 4px 0; }
|
||||
.fee-existing-error { color: var(--cc-danger, #f56c6c); }
|
||||
</style>
|
||||
|
||||
@@ -93,8 +93,8 @@ async function save() {
|
||||
const receiptColumns: DataTableColumn[] = [{ prop: 'quantity', label: '收货数量', width: 140 }, { key: 'receivedAt', label: '收货时间', minWidth: 170, formatter: (row: ProcurementWorkflowReceipt) => dateTime(row.receivedAt) }, { key: 'files', label: '照片', minWidth: 180, slotName: 'files' }]
|
||||
const outboundColumns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 180, slotName: 'actions', fixed: 'left' }, { prop: 'outboundNo', label: '出库单号', minWidth: 190 },
|
||||
{ key: 'status', label: '状态', width: 120, formatter: (row: ProcurementOutbound) => statusLabel(row.status), afterColumn: 'outboundNo' },
|
||||
{ prop: 'quantity', label: '出库数量', width: 110 }, { prop: 'trackingNo', label: '物流跟踪号', minWidth: 190 },
|
||||
{ key: 'status', label: '状态', width: 120, formatter: (row: ProcurementOutbound) => statusLabel(row.status) },
|
||||
{ key: 'createdAt', label: '创建时间', minWidth: 170, formatter: (row: ProcurementOutbound) => dateTime(row.createdAt) },
|
||||
{ key: 'completedAt', label: '签收时间', minWidth: 170, formatter: (row: ProcurementOutbound) => dateTime(row.completedAt) },
|
||||
{ key: 'files', label: '签收凭证', minWidth: 170, slotName: 'files' },
|
||||
@@ -113,8 +113,8 @@ const fileName = (id: string) => props.detail.files.find(file => file.fileId ===
|
||||
<dl class="density-fields inventory"><div><dt>累计收货</dt><dd>{{ workflow.receivedQuantity }}</dd></div><div><dt>采购库存</dt><dd>{{ workflow.stockQuantity }}</dd></div><div><dt>出库处理中</dt><dd>{{ workflow.reservedQuantity }}</dd></div><div><dt>可出库</dt><dd>{{ workflow.availableQuantity }}</dd></div></dl>
|
||||
<h3>收货记录</h3><DataTable :data="workflow.receipts" :columns="receiptColumns" row-key="id" empty-text="暂无收货记录"><template #files="{ row }"><el-button v-for="id in row.fileIds" :key="id" :disabled="!!downloading" link type="primary" @click="emit('download', id)">{{ fileName(id) }}</el-button></template></DataTable>
|
||||
<h3>出库单</h3><DataTable :data="workflow.outbounds" :columns="outboundColumns" row-key="id" empty-text="暂无出库单"><template #actions="{ row }"><el-button v-if="row.status === 'IN_TRANSIT'" :disabled="!can('ship-customer')" title="开始本批次派送" link type="primary" @click="begin('dispatch', row as ProcurementOutbound)">开始派送</el-button><el-button v-if="row.status === 'DELIVERING'" :disabled="!can('ship-customer')" title="上传图片或文件并确认本批次签收" link type="primary" @click="begin('complete', row as ProcurementOutbound)">上传签收凭证</el-button></template><template #files="{ row }"><el-button v-for="id in row.fileIds" :key="id" :disabled="!!downloading" link type="primary" @click="emit('download', id)">{{ fileName(id) }}</el-button></template></DataTable>
|
||||
<AppDialog v-model="open" :title="title" width="580px" confirm-text="确认" :confirm-loading="saving" :confirm-disabled="uploading || warehouseLoading" @confirm="save">
|
||||
<el-form :model="form" class="density-form" label-position="top" :disabled="saving">
|
||||
<AppDialog v-model="open" class="admin-density-dialog admin-workbench is-dialog" :title="title" width="580px" confirm-text="确认" :confirm-loading="saving" :confirm-disabled="uploading || warehouseLoading" @confirm="save">
|
||||
<el-form :model="form" class="density-form density-form--outlined" label-position="top" :disabled="saving">
|
||||
<template v-if="action === 'review'"><el-form-item label="审核结果"><el-radio-group v-model="form.decision"><el-radio value="APPROVE">通过</el-radio><el-radio value="REJECT">拒绝</el-radio></el-radio-group></el-form-item><el-form-item v-if="form.decision === 'APPROVE'" label="收货仓" required><el-select v-model="form.warehouseId" filterable :loading="warehouseLoading"><el-option v-for="warehouse in warehouses" :key="warehouse.warehouseId" :value="warehouse.warehouseId" :label="warehouse.warehouseName" /></el-select></el-form-item><el-form-item v-else label="拒绝原因" required><el-input v-model="form.reason" type="textarea" :rows="2" maxlength="1000" /></el-form-item></template>
|
||||
<el-form-item v-if="['receipts', 'outbounds'].includes(action)" :label="action === 'receipts' ? '本批收货数量' : '本批出库数量'" required><el-input-number v-model="form.quantity" :min="1" :max="action === 'outbounds' ? workflow.availableQuantity : detail.customerView.order.purchaseQuantity - workflow.receivedQuantity" :precision="0" /></el-form-item>
|
||||
<el-form-item v-if="action === 'outbounds'" label="物流跟踪号" required><el-input v-model="form.trackingNo" maxlength="100" /></el-form-item>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
/* Shared business form layout for purchase and local transport editors. */
|
||||
.admin-density.overseas-business-editor { width: 100%; }
|
||||
.admin-density.overseas-business-editor:not(.overseas-transport-editor) { max-width: 1120px; }
|
||||
.overseas-business-editor .overseas-business-form-layout { gap: 0; }
|
||||
.overseas-business-editor .form-reference { min-width: 0; color: var(--cc-muted); font-size: var(--admin-font-body); overflow-wrap: anywhere; }
|
||||
.overseas-business-editor .form-panel { min-width: 0; }
|
||||
|
||||
@@ -146,7 +146,9 @@ describe('overseas business mounted pages', () => {
|
||||
it('shows unknown pool vehicle type as a dash without inventing a capacity', async () => {
|
||||
vi.mocked(listUsTruckOrders).mockResolvedValue({ records: [{ ...truck, vehicleType: null, fleetArrangement: arrangement }], total: 1, page: 1, size: 20, pages: 1 })
|
||||
const wrapper = mountPage(TransportOrdersView, { domain: 'truck' }); await flushPromises()
|
||||
expect(wrapper.find('.el-table__body-wrapper tbody .el-table__row').findAll('td')[2]!.text()).toBe('—')
|
||||
const vehicleColumn = wrapper.findAll('.el-table__header-wrapper th').findIndex(header => header.text() === '车辆类型')
|
||||
expect(vehicleColumn).toBeGreaterThan(0)
|
||||
expect(wrapper.find('.el-table__body-wrapper tbody .el-table__row').findAll('td')[vehicleColumn]!.text()).toBe('—')
|
||||
expect(wrapper.find('.el-table__body-wrapper').text()).not.toContain('40HQ')
|
||||
})
|
||||
it('allows standalone demand editing without inventing pallet service or transport arrangements', async () => {
|
||||
@@ -214,10 +216,14 @@ describe('overseas business mounted pages', () => {
|
||||
await flushPromises()
|
||||
const cells = wrapper.findAll('.el-table__body-wrapper tbody .el-table__row')[0]!.findAll('td')
|
||||
expect(cells[0]!.classes()).toContain('el-table-fixed-column--left')
|
||||
for (const [index, value] of [record.orderNo, record.customerName, record.purchaseWarehouseName, record.purchaseCategory, record.productName, record.productSpecification].entries()) {
|
||||
expect(cells[index + 1]!.text()).toBe(value)
|
||||
expect(cells[index + 1]!.classes()).not.toContain('data-table__single-line-cell')
|
||||
expect(cells[index + 1]!.find('.cell').classes()).not.toContain('el-tooltip')
|
||||
const headers = wrapper.findAll('.el-table__header-wrapper th').map(header => header.text())
|
||||
expect(headers.slice(0, 3)).toEqual(['操作', '订单编号', '状态'])
|
||||
for (const [label, value] of [['订单编号', record.orderNo], ['客户名称', record.customerName], ['采购仓库', record.purchaseWarehouseName], ['采购品类', record.purchaseCategory], ['商品名称', record.productName], ['商品规格', record.productSpecification]]) {
|
||||
const index = headers.indexOf(label!)
|
||||
expect(index).toBeGreaterThan(0)
|
||||
expect(cells[index]!.text()).toBe(value)
|
||||
expect(cells[index]!.classes()).not.toContain('data-table__single-line-cell')
|
||||
expect(cells[index]!.find('.cell').classes()).not.toContain('el-tooltip')
|
||||
}
|
||||
})
|
||||
it('refreshes the cached procurement list after returning from detail or edit', () => { expect(purchaseListSource).toContain('onActivated'); expect(purchaseListSource).toMatch(/onActivated\(\(\) => \{ if \(activatedOnce\) void load\(\)/) })
|
||||
|
||||
@@ -53,13 +53,13 @@ const fields = computed<QueryField[]>(() => [
|
||||
const columns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 210, fixed: 'left', slotName: 'actions' },
|
||||
{ prop: 'customerName', label: '客户', minWidth: 150 }, { prop: 'businessNo', label: '业务单号', minWidth: 185 },
|
||||
{ prop: 'reviewStatus', label: '审核状态', width: 100, slotName: 'reviewStatus', afterColumn: 'businessNo' },
|
||||
{ key: 'chargeStatus', label: '费用状态', minWidth: 115, formatter: feeProcessingLabel, afterColumn: 'businessNo' },
|
||||
{ prop: 'businessType', label: '业务类型', width: 115, formatter: feeBusinessLabel },
|
||||
{ prop: 'feeItemName', label: '费用项目', minWidth: 145 },
|
||||
{ prop: 'supplierName', label: '供应商', minWidth: 150, defaultVisible: false },
|
||||
{ key: 'amount', label: '结算金额(CNY)', width: 145, align: 'right', formatter: (row: OverseasFee) => feeMoney(row.amount) },
|
||||
{ prop: 'collectionMode', label: '结算方式', width: 115, formatter: (row: OverseasFee) => row.direction === 'PAYABLE' ? '推送结算' : row.collectionMode === 'BILL' ? '账单结算' : '余额/授信' },
|
||||
{ prop: 'reviewStatus', label: '审核状态', width: 100, slotName: 'reviewStatus' },
|
||||
{ key: 'chargeStatus', label: '费用状态', minWidth: 115, formatter: feeProcessingLabel },
|
||||
{ prop: 'businessDate', label: '费用日期', width: 110 },
|
||||
{ prop: 'createdAt', label: '创建时间', minWidth: 165, defaultVisible: false, formatter: (row: OverseasFee) => feeTime(row.createdAt) },
|
||||
]
|
||||
|
||||
@@ -33,6 +33,7 @@ const fields = computed<QueryField[]>(() => [
|
||||
const columns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 120, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'orderNo', prop: 'orderNo', label: '订单编号', minWidth: 220, singleLine: false },
|
||||
{ key: 'status', label: '状态', minWidth: 145, slotName: 'status', afterColumn: 'orderNo' },
|
||||
{ key: 'customerName', prop: 'customerName', label: '客户名称', minWidth: 220, singleLine: false, formatter: (row: OverseasPurchaseSummary) => row.customerName || '—' },
|
||||
{ key: 'purchaseWarehouseName', prop: 'purchaseWarehouseName', label: '采购仓库', minWidth: 160, singleLine: false, formatter: (row: OverseasPurchaseSummary) => row.purchaseWarehouseName || '—' },
|
||||
{ key: 'purchaseCategory', prop: 'purchaseCategory', label: '采购品类', minWidth: 130, singleLine: false },
|
||||
@@ -40,7 +41,6 @@ const columns: DataTableColumn[] = [
|
||||
{ key: 'productSpecification', prop: 'productSpecification', label: '商品规格', minWidth: 180, singleLine: false, formatter: (row: OverseasPurchaseSummary) => row.productSpecification || '—' },
|
||||
{ key: 'budgetUnitPrice', label: '预算单价', width: 120, align: 'right', formatter: (row: OverseasPurchaseSummary) => money(row.budgetUnitPrice, row.currency) },
|
||||
{ key: 'purchaseQuantity', prop: 'purchaseQuantity', label: '采购数量', width: 100, align: 'right' },
|
||||
{ key: 'status', label: '状态', minWidth: 145, slotName: 'status' },
|
||||
{ key: 'createdAt', prop: 'createdAt', label: '创建时间', minWidth: 165, formatter: (row: OverseasPurchaseSummary) => formatDateTime(row.createdAt) },
|
||||
{ key: 'procurementDirection', label: '采购方向', minWidth: 170, formatter: (row: OverseasPurchaseSummary) => directionLabel(row.procurementDirection) },
|
||||
]
|
||||
|
||||
@@ -55,7 +55,7 @@ const fields = computed<QueryField[]>(() => [
|
||||
{ key: 'routeKeyword', type: 'input', label: '运输路线', placeholder: '起运地/目的地', width: 240 },
|
||||
])
|
||||
const truckColumns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 220, fixed: 'left', slotName: 'actions' }, { key: 'orderNo', prop: 'orderNo', label: '订单编号', minWidth: 170 },
|
||||
{ key: 'actions', label: '操作', width: 220, fixed: 'left', slotName: 'actions' }, { key: 'orderNo', prop: 'orderNo', label: '订单编号', minWidth: 170 }, { key: 'status', label: '状态', width: 120, slotName: 'status', afterColumn: 'orderNo' },
|
||||
{ key: 'vehicleType', label: '车辆类型', minWidth: 110, formatter: (row: LocalTransportOrder) => row.vehicleType ? ({ PALLET_15: '15拖', PALLET_30: '30拖' })[row.vehicleType] : '—' },
|
||||
{ key: 'origin', prop: 'origin', label: '起运地', minWidth: 145 }, { key: 'destination', prop: 'destination', label: '目的地', minWidth: 145 },
|
||||
{ key: 'originContactName', prop: 'originContactName', label: '起运地联系人', minWidth: 125 }, { key: 'originContactPhone', prop: 'originContactPhone', label: '起运地联系电话', minWidth: 155 },
|
||||
@@ -63,16 +63,16 @@ const truckColumns: DataTableColumn[] = [
|
||||
{ key: 'packageCount', prop: 'packageCount', label: '件数', width: 80, align: 'right' }, { key: 'totalWeightKg', prop: 'totalWeightKg', label: '总重量(kg)', width: 115, align: 'right' },
|
||||
{ key: 'budgetAmount', label: '预算总价', width: 120, align: 'right', formatter: (row: LocalTransportOrder) => row.budgetAmount == null ? '—' : `${row.budgetAmount} ${row.currency}` },
|
||||
{ key: 'pickupAt', label: '取货时间', width: 165, formatter: (row: LocalTransportOrder) => formatDateTime(row.pickupAt) },
|
||||
{ key: 'deliveryAt', label: '送货时间', width: 165, formatter: (row: LocalTransportOrder) => formatDateTime(row.deliveryAt) }, { key: 'status', label: '状态', width: 120, slotName: 'status' },
|
||||
{ key: 'deliveryAt', label: '送货时间', width: 165, formatter: (row: LocalTransportOrder) => formatDateTime(row.deliveryAt) },
|
||||
{ key: 'createdAt', prop: 'createdAt', label: '创建时间', minWidth: 165, formatter: (row: LocalTransportOrder) => formatDateTime(row.createdAt) },
|
||||
]
|
||||
const looseboardColumns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 180, fixed: 'left', slotName: 'actions' }, { key: 'orderNo', prop: 'orderNo', label: '订单编号', minWidth: 170 },
|
||||
{ key: 'actions', label: '操作', width: 180, fixed: 'left', slotName: 'actions' }, { key: 'orderNo', prop: 'orderNo', label: '订单编号', minWidth: 170 }, { key: 'status', label: '状态', width: 105, slotName: 'status', afterColumn: 'orderNo' },
|
||||
{ key: 'transportType', label: '运输类型', minWidth: 120, formatter: (row: LocalLooseboardOrder) => row.transportType ? transportTypeLabels[row.transportType] : '—' }, { key: 'origin', prop: 'origin', label: '起运地', minWidth: 145 },
|
||||
{ key: 'destination', prop: 'destination', label: '目的地', minWidth: 145 }, { key: 'originContactName', prop: 'originContactName', label: '起运地联系人', minWidth: 125 }, { key: 'originContactPhone', prop: 'originContactPhone', label: '起运地联系电话', minWidth: 155 },
|
||||
{ key: 'destinationContactName', prop: 'destinationContactName', label: '目的地联系人', minWidth: 125 }, { key: 'destinationContactPhone', prop: 'destinationContactPhone', label: '目的地联系电话', minWidth: 155 }, { key: 'packageCount', prop: 'packageCount', label: '件数', width: 80, align: 'right' },
|
||||
{ key: 'totalWeightKg', prop: 'totalWeightKg', label: '总重量(kg)', width: 115, align: 'right' }, { key: 'budgetAmount', label: '预算总价', width: 115, align: 'right', formatter: (row: LocalLooseboardOrder) => row.budgetAmount == null ? '—' : `${row.budgetAmount} ${row.currency}` },
|
||||
{ key: 'productName', prop: 'productName', label: '商品名称', minWidth: 150 }, { key: 'estimatedPalletCount', prop: 'estimatedPalletCount', label: '预计托数', width: 100 }, { key: 'pickupAt', label: '取货时间', width: 165, formatter: (row: LocalLooseboardOrder) => formatDateTime(row.pickupAt) }, { key: 'deliveryAt', label: '送货时间', width: 165, formatter: (row: LocalLooseboardOrder) => formatDateTime(row.deliveryAt) }, { key: 'status', label: '状态', width: 105, slotName: 'status' },
|
||||
{ key: 'productName', prop: 'productName', label: '商品名称', minWidth: 150 }, { key: 'estimatedPalletCount', prop: 'estimatedPalletCount', label: '预计托数', width: 100 }, { key: 'pickupAt', label: '取货时间', width: 165, formatter: (row: LocalLooseboardOrder) => formatDateTime(row.pickupAt) }, { key: 'deliveryAt', label: '送货时间', width: 165, formatter: (row: LocalLooseboardOrder) => formatDateTime(row.deliveryAt) },
|
||||
{ key: 'createdAt', prop: 'createdAt', label: '创建时间', minWidth: 165, formatter: (row: LocalLooseboardOrder) => formatDateTime(row.createdAt) },
|
||||
]
|
||||
const columns = computed(() => {
|
||||
|
||||
@@ -120,23 +120,23 @@ watch([orderNo, customerId], load, { immediate: true })
|
||||
.truck-detail-actions { flex-wrap: wrap; }
|
||||
.truck-detail-section { margin-bottom: var(--admin-space-6); }
|
||||
.truck-section-heading { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; margin-bottom: 14px; }
|
||||
.truck-section-heading h2 { margin: 0; font-size: 16px; color: var(--cc-text); }
|
||||
.truck-flow-caption, .truck-detail-note { color: var(--cc-muted); font-size: 12px; }
|
||||
.truck-section-heading h2 { margin: 0; font-size: var(--admin-font-section); color: var(--cc-text); }
|
||||
.truck-flow-caption, .truck-detail-note { color: var(--cc-muted); font-size: var(--admin-font-caption); }
|
||||
.truck-detail-note { margin: 10px 0 0; }
|
||||
.truck-flow { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 0; margin: 0; padding: 0; list-style: none; }
|
||||
.truck-flow li { position: relative; display: flex; flex-direction: column; align-items: center; gap: 8px; min-width: 0; color: var(--cc-muted); text-align: center; }
|
||||
.truck-flow li::before { content: ''; position: absolute; top: 15px; left: -50%; width: 100%; height: 1px; background: var(--cc-border); }
|
||||
.truck-flow li:first-child::before { display: none; }
|
||||
.truck-flow li.done::before, .truck-flow li.active::before { background: var(--el-color-primary); }
|
||||
.truck-flow-marker { position: relative; z-index: 1; display: grid; place-items: center; width: 30px; height: 30px; border: 1px solid var(--cc-border); border-radius: 50%; background: var(--cc-surface); color: var(--cc-muted); font-size: 12px; }
|
||||
.truck-flow-marker { position: relative; z-index: 1; display: grid; place-items: center; width: 30px; height: 30px; border: 1px solid var(--cc-border); border-radius: 50%; background: var(--cc-surface); color: var(--cc-muted); font-size: var(--admin-font-caption); }
|
||||
.truck-flow li.done .truck-flow-marker, .truck-flow li.active .truck-flow-marker { border-color: var(--el-color-primary); color: var(--el-color-primary); }
|
||||
.truck-flow li.active .truck-flow-marker { background: var(--el-color-primary); color: #fff; box-shadow: 0 0 0 4px color-mix(in srgb, var(--el-color-primary) 14%, transparent); }
|
||||
.truck-flow-label { font-size: 12px; white-space: nowrap; }
|
||||
.truck-flow-label { font-size: var(--admin-font-caption); white-space: nowrap; }
|
||||
.truck-flow li.active .truck-flow-label { color: var(--el-color-primary); font-weight: 600; }
|
||||
.truck-detail-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 1px; margin: 0; border: 1px solid var(--cc-border); border-radius: var(--cc-radius-lg); overflow: hidden; background: var(--cc-border); }
|
||||
.truck-detail-fact { min-width: 0; display: grid; grid-template-columns: 90px minmax(0, 1fr); gap: 10px; padding: 11px 14px; background: var(--cc-surface); }
|
||||
.truck-detail-fact dt { color: var(--cc-muted); font-size: 12px; }
|
||||
.truck-detail-fact dd { min-width: 0; margin: 0; color: var(--cc-text); overflow-wrap: anywhere; font-size: 13px; }
|
||||
.truck-detail-fact dt { color: var(--cc-muted); font-size: var(--admin-font-caption); }
|
||||
.truck-detail-fact dd { min-width: 0; margin: 0; color: var(--cc-text); overflow-wrap: anywhere; font-size: var(--admin-font-body); }
|
||||
.truck-detail-fact:last-child { grid-column: span 2; }
|
||||
@media (max-width: 1100px) { .truck-detail-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
||||
@media (max-width: 760px) { .truck-flow { overflow-x: auto; grid-template-columns: repeat(5, 100px); padding-bottom: 4px; } .truck-detail-grid { grid-template-columns: 1fr; } .truck-detail-fact:last-child { grid-column: auto; } }
|
||||
|
||||
@@ -25,9 +25,9 @@ function actions(row: CapacityInquiry, busy: boolean): RowActionItem[] {
|
||||
}
|
||||
const columns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 320, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'bookingNo', label: '意向 / 预订号', minWidth: 210, singleLine: false }, { key: 'customerName', label: '客户', minWidth: 210, singleLine: false },
|
||||
{ key: 'tripNo', label: '车次号', minWidth: 210 }, { key: 'destinationLabel', label: '卸货站', minWidth: 130 },
|
||||
{ key: 'status', label: '当前进度 / 下一步', minWidth: 270, slotName: 'status', singleLine: false }, { key: 'cargo', label: '货量', minWidth: 270, slotName: 'cargo', singleLine: false },
|
||||
{ key: 'bookingNo', label: '意向 / 预订号', minWidth: 210, singleLine: false },
|
||||
{ key: 'status', label: '当前进度 / 下一步', minWidth: 270, slotName: 'status', singleLine: false, afterColumn: 'bookingNo' }, { key: 'customerName', label: '客户', minWidth: 210, singleLine: false },
|
||||
{ key: 'tripNo', label: '车次号', minWidth: 210 }, { key: 'destinationLabel', label: '卸货站', minWidth: 130 }, { key: 'cargo', label: '货量', minWidth: 270, slotName: 'cargo', singleLine: false },
|
||||
{ key: 'agreedAmount', label: '约定运费(非实收)', minWidth: 165, slotName: 'amount' }, { key: 'businessOrderNo', label: '本土订单号', minWidth: 210 },
|
||||
{ key: 'updatedAt', label: '更新时间', minWidth: 180, slotName: 'time' },
|
||||
]
|
||||
|
||||
@@ -67,9 +67,9 @@ const quotedAmount = computed(() => form.unitPrice == null ? null : form.priceBa
|
||||
const columns = computed<DataTableColumn[]>(() => [
|
||||
{ key: 'actions', label: '操作', slotName: 'actions', width: planned.value ? 320 : 170, fixed: 'left' },
|
||||
{ key: 'bookingNo', prop: 'bookingNo', label: '外接预订号', minWidth: 225 },
|
||||
{ key: 'status', label: '状态', slotName: 'status', width: 130, afterColumn: 'bookingNo' },
|
||||
{ key: 'customerName', prop: 'customerName', label: '委托客户', minWidth: 170 },
|
||||
{ key: 'destinationLabel', prop: 'destinationLabel', label: '卸货站', minWidth: 140 },
|
||||
{ key: 'status', label: '状态', slotName: 'status', width: 130 },
|
||||
{ key: 'cargo', label: '承运货量', slotName: 'cargo', minWidth: 225 },
|
||||
{ key: 'amount', label: '约定运费', slotName: 'amount', minWidth: 160 },
|
||||
{ key: 'documents', label: '来货 / 排车', slotName: 'documents', minWidth: 230 },
|
||||
|
||||
@@ -25,7 +25,7 @@ const canComplete = computed(() => rows.value.length > 0 && rows.value.every(row
|
||||
const columns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 175, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'transferNo', prop: 'transferNo', label: '仓储转运单', minWidth: 220 },
|
||||
{ key: 'status', label: '取货进度', slotName: 'status', width: 110 }, { key: 'pickupAddress', prop: 'pickupAddress', label: '取货地址', minWidth: 270 },
|
||||
{ key: 'status', label: '取货进度', slotName: 'status', width: 110, afterColumn: 'transferNo' }, { key: 'pickupAddress', prop: 'pickupAddress', label: '取货地址', minWidth: 270 },
|
||||
{ key: 'contactName', prop: 'contactName', label: '联系人', width: 110 }, { key: 'contactPhone', prop: 'contactPhone', label: '电话', minWidth: 145 },
|
||||
{ key: 'packageCount', prop: 'packageCount', label: '预报箱数', width: 100 }, { key: 'actualPackages', label: '实提箱数', slotName: 'actual', width: 100 },
|
||||
{ key: 'evidence', label: '取货凭证', slotName: 'evidence', minWidth: 180 }, { key: 'remark', prop: 'remark', label: '取货备注', minWidth: 180 },
|
||||
|
||||
@@ -48,6 +48,8 @@ const columns = computed<DataTableColumn[]>(() => {
|
||||
const result: DataTableColumn[] = [{ key: 'actions', label: '操作', width: costRecord.value ? 170 : 100, fixed: 'left', slotName: 'actions' }]
|
||||
if (props.recordType === 'EXCEPTION') result.push({ key: 'exceptionType', label: '异常类型', minWidth: 135, slotName: 'exceptionType' })
|
||||
result.push({ key: 'date', label: '发生时间', minWidth: 165, slotName: 'date' })
|
||||
result.push({ key: 'status', label: meterRecord.value ? '核验状态' : '处理状态', width: 105, slotName: 'status', afterColumn: 'date' })
|
||||
if (costRecord.value) result.push({ key: 'payment', label: '付款状态', minWidth: 130, slotName: 'payment', afterColumn: 'date' })
|
||||
if (meterRecord.value) result.push(...(props.recordType === 'MAINTENANCE' ? [
|
||||
{ key: 'last', label: '上次保养', minWidth: 120, slotName: 'last' },
|
||||
{ key: 'current', label: '记录时里程', minWidth: 120, slotName: 'current' },
|
||||
@@ -61,9 +63,8 @@ const columns = computed<DataTableColumn[]>(() => {
|
||||
const place = { key: 'place', prop: 'details.incident.place', label: props.recordType === 'REPAIR' ? '维修地点' : '救援地点', minWidth: 190 }
|
||||
result.push(...(props.recordType === 'REPAIR' ? [project, place] : [place, project]), { key: 'cost', label: '费用(申报金额)', minWidth: 160, slotName: 'cost' })
|
||||
} else result.push({ key: 'description', prop: 'details.incident.description', label: '描述', minWidth: 260 })
|
||||
result.push({ key: 'status', label: meterRecord.value ? '核验状态' : '处理状态', width: 105, slotName: 'status' })
|
||||
if (!meterRecord.value) result.push({ key: 'handler', prop: 'details.incident.handler', label: '处理人', minWidth: 120 })
|
||||
if (['REPAIR', 'RESCUE'].includes(props.recordType)) result.push({ key: 'payment', label: '付款状态', minWidth: 130, slotName: 'payment' }, { key: 'description', prop: 'details.incident.description', label: '描述', minWidth: 260 })
|
||||
if (['REPAIR', 'RESCUE'].includes(props.recordType)) result.push({ key: 'description', prop: 'details.incident.description', label: '描述', minWidth: 260 })
|
||||
result.push({ key: 'remark', prop: 'details.remark', label: '备注', minWidth: 150 })
|
||||
return result
|
||||
})
|
||||
|
||||
@@ -19,11 +19,11 @@ const title = computed(() => props.kind === 'deliveries' ? '派送订单明细'
|
||||
let generation = 0
|
||||
const columns = computed<DataTableColumn[]>(() => props.kind === 'deliveries' ? [
|
||||
{ key: 'deliveryNo', prop: 'deliveryNo', label: '派送单号', minWidth: 210 },
|
||||
{ key: 'status', label: '派送状态', width: 115, slotName: 'status', afterColumn: 'deliveryNo' },
|
||||
{ key: 'source', label: 'BOL / 来源排车', minWidth: 215, slotName: 'source' },
|
||||
{ key: 'customerName', label: '客户', minWidth: 160, slotName: 'customer' },
|
||||
{ key: 'destination', label: '目的仓 / 地址', minWidth: 180, slotName: 'destination' },
|
||||
{ key: 'plannedDeliveryAt', label: '预约日期', minWidth: 165, slotName: 'date' },
|
||||
{ key: 'status', label: '派送状态', width: 115, slotName: 'status' },
|
||||
{ key: 'relationship', label: '关联关系', minWidth: 220, slotName: 'relationship' },
|
||||
] : [
|
||||
{ key: 'occurredAt', label: '时间', minWidth: 165, slotName: 'date' },
|
||||
|
||||
@@ -66,10 +66,10 @@ const fields = computed<QueryField[]>(() => tab.value === 'offers' ? [
|
||||
])
|
||||
const columns = computed<DataTableColumn[]>(() => [
|
||||
{ key: 'actions', label: '操作', width: 200, fixed: 'left', slotName: 'actions' }, { key: 'offerNo', label: '发布号 / 车次号', minWidth: 220, slotName: 'identity' },
|
||||
{ key: 'status', label: '发布状态', minWidth: 170, slotName: 'status', singleLine: false, afterColumn: 'offerNo' },
|
||||
{ key: 'warehouseName', label: '起运仓', minWidth: 155 }, { key: 'destinations', label: '目的站与可接余位', minWidth: 310, slotName: 'destinations', singleLine: false },
|
||||
{ key: 'plannedDepartureAt', label: '发车 / 最晚送仓', minWidth: 195, slotName: 'times' },
|
||||
...(manage.value ? [{ key: 'segments', label: '分段计划占用 / 外接预留', minWidth: 320, slotName: 'loads', singleLine: false }] : []),
|
||||
{ key: 'status', label: '发布状态', minWidth: 170, slotName: 'status', singleLine: false }, { key: 'contactName', label: '调度联系信息', minWidth: 180, slotName: 'contact' },
|
||||
...(manage.value ? [{ key: 'segments', label: '分段计划占用 / 外接预留', minWidth: 320, slotName: 'loads', singleLine: false }] : []), { key: 'contactName', label: '调度联系信息', minWidth: 180, slotName: 'contact' },
|
||||
])
|
||||
const canCreate = computed(() => manage.value || access.can('platform:fleet:capacity-offer:list') && access.can('platform:fleet:capacity-offer:inquiry-create'))
|
||||
async function create(row: CapacityOffer) {
|
||||
|
||||
@@ -16,7 +16,8 @@ const filters = ref<QueryModel>({ origin: '', destination: '' }), applied = ref(
|
||||
const fields: QueryField[] = [{ key: 'origin', label: '起运仓', type: 'input', placeholder: '起运仓名称', width: 190 }, { key: 'destination', label: '目的地', type: 'input', placeholder: '目的地名称', width: 190 }]
|
||||
const amount = (value: number | null | undefined) => value == null ? '—' : String(value)
|
||||
const columns: DataTableColumn[] = [
|
||||
{ key: 'dispatchNo', label: '排车单号', minWidth: 200 }, { key: 'warehouseName', label: '起运仓', minWidth: 160 },
|
||||
{ key: 'dispatchNo', label: '排车单号', minWidth: 200 },
|
||||
{ key: 'capacityReason', label: '余位状态', minWidth: 220, formatter: (row: DispatchCapacityDemand) => row.capacityKnown ? '余位已确认' : row.capacityReason || '余位待确认', afterColumn: 'dispatchNo' }, { key: 'warehouseName', label: '起运仓', minWidth: 160 },
|
||||
{ key: 'route', label: '路线', minWidth: 240, formatter: (row: DispatchCapacityDemand) => row.route || '—' },
|
||||
{ key: 'plannedDepartureAt', label: '计划发车', minWidth: 180, formatter: (row: DispatchCapacityDemand) => offerTime(row.plannedDepartureAt) },
|
||||
{ key: 'vehiclePalletSpaces', label: '车辆容量(托位)', width: 145, align: 'right', formatter: (row: DispatchCapacityDemand) => amount(row.vehiclePalletSpaces) },
|
||||
@@ -24,7 +25,6 @@ const columns: DataTableColumn[] = [
|
||||
{ key: 'remainingPalletSpaces', label: '剩余标准托位', width: 140, align: 'right', formatter: (row: DispatchCapacityDemand) => row.capacityKnown ? amount(row.remainingPalletSpaces) : '待确认' },
|
||||
{ key: 'remainingWeightKg', label: '剩余载重(kg)', width: 145, align: 'right', formatter: (row: DispatchCapacityDemand) => row.capacityKnown ? amount(row.remainingWeightKg) : '待确认' },
|
||||
{ key: 'remainingVolumeCbm', label: '剩余容积(m³)', width: 145, align: 'right', formatter: (row: DispatchCapacityDemand) => row.capacityKnown ? amount(row.remainingVolumeCbm) : '待确认' },
|
||||
{ key: 'capacityReason', label: '余位状态', minWidth: 220, formatter: (row: DispatchCapacityDemand) => row.capacityKnown ? '余位已确认' : row.capacityReason || '余位待确认' },
|
||||
]
|
||||
let sequence = 0
|
||||
async function load() {
|
||||
|
||||
@@ -185,7 +185,7 @@ onBeforeUnmount(() => { detailActive = false; resetDetailRequests() })
|
||||
<style scoped>
|
||||
.wide-field { grid-column: span 4; }
|
||||
.driver-document-actions, .driver-qualification { display: flex; align-items: center; flex-wrap: wrap; gap: var(--admin-space-5); }
|
||||
.driver-review { display: grid; gap: var(--admin-space-5); margin: var(--admin-space-6) 0; max-width: 900px; }
|
||||
.driver-review { display: grid; gap: var(--admin-space-5); margin: var(--admin-space-6) 0; }
|
||||
.driver-qualification { margin-top: var(--admin-space-6); }
|
||||
.driver-qualification label { display: flex; align-items: center; gap: var(--admin-space-4); }
|
||||
.driver-note { color: var(--el-color-warning); }
|
||||
|
||||
@@ -41,6 +41,8 @@ const fields: QueryField[] = [
|
||||
const columns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 205, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'driverNo', prop: 'driverNo', label: '司机编号', minWidth: 205 },
|
||||
{ key: 'review', label: '审核状态', width: 100, slotName: 'review', afterColumn: 'driverNo' },
|
||||
{ key: 'active', label: '当前状态', width: 100, slotName: 'active', afterColumn: 'driverNo' },
|
||||
{ key: 'name', prop: 'details.name', label: '姓名', minWidth: 115 },
|
||||
{ key: 'ownership', label: '归属', width: 100, slotName: 'ownership' },
|
||||
{ key: 'phone', prop: 'details.phone', label: '联系电话', minWidth: 140 },
|
||||
@@ -50,9 +52,7 @@ const columns: DataTableColumn[] = [
|
||||
{ key: 'expires', label: '驾照到期', minWidth: 140, slotName: 'expires' },
|
||||
{ key: 'medical', label: '体检到期', width: 125, slotName: 'medical' },
|
||||
{ key: 'otherDocs', label: '培训 / 保险到期', minWidth: 185, slotName: 'otherDocs' },
|
||||
{ key: 'review', label: '审核状态', width: 100, slotName: 'review' },
|
||||
{ key: 'file', label: '驾驶证附件', width: 110, slotName: 'file' },
|
||||
{ key: 'active', label: '当前状态', width: 100, slotName: 'active' },
|
||||
]
|
||||
async function load() {
|
||||
const current = ++request
|
||||
|
||||
@@ -29,8 +29,8 @@ const fields: QueryField[] = [
|
||||
]
|
||||
const columns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 230, fixed: 'left', slotName: 'actions' }, { key: 'month', prop: 'month', label: '汇率月份', minWidth: 120 },
|
||||
{ key: 'usdToCnyAverage', prop: 'usdToCnyAverage', label: '平均汇率', minWidth: 120 }, { key: 'conversion', label: '换算说明', minWidth: 190, slotName: 'conversion' },
|
||||
{ key: 'status', label: '状态', width: 90, slotName: 'status' }, { key: 'updatedBy', prop: 'updatedBy', label: '更新人', minWidth: 120 },
|
||||
{ key: 'status', label: '状态', width: 90, slotName: 'status', afterColumn: 'month' },
|
||||
{ key: 'usdToCnyAverage', prop: 'usdToCnyAverage', label: '平均汇率', minWidth: 120 }, { key: 'conversion', label: '换算说明', minWidth: 190, slotName: 'conversion' }, { key: 'updatedBy', prop: 'updatedBy', label: '更新人', minWidth: 120 },
|
||||
{ key: 'updatedAt', prop: 'updatedAt', label: '更新时间', minWidth: 150 }, { key: 'remark', prop: 'remark', label: '备注', minWidth: 200 },
|
||||
]
|
||||
const form = reactive({ month: '', usdToCnyAverage: 0, remark: '', recordVersion: 0 })
|
||||
|
||||
@@ -21,11 +21,11 @@ const access = useAccessPreviewStore(), rows = ref<FleetQuote[]>([]), total = re
|
||||
const kind = ref<FleetQuoteKind>('PURCHASE'), editor = ref(false), importer = ref(false), selected = ref<FleetQuote | null>(null), readonly = ref(false), error = ref<ApiErrorDetails | null>(null)
|
||||
const filters = ref<QueryModel>({ keyword: '', status: '', effectiveOn: '' }); let applied: FleetQuoteQuery = {}, sequence = 0
|
||||
const fields: QueryField[] = [{ key: 'keyword', label: '关键词', type: 'input', width: 300, placeholder: '发货城市 / 目的仓码 / 往来单位 / 报价号' }, { key: 'status', label: '状态', type: 'select', width: 130, options: [{ value: 'ACTIVE', label: '启用' }, { value: 'INACTIVE', label: '停用' }] }, { key: 'effectiveOn', label: '有效日期', type: 'date', width: 170, placeholder: '有效日期(选填)' }]
|
||||
const columns: DataTableColumn[] = [{ key: 'actions', label: '操作', width: 220, fixed: 'left', slotName: 'actions' }, { key: 'quoteNo', label: '报价号', minWidth: 210 },
|
||||
const columns: DataTableColumn[] = [{ key: 'actions', label: '操作', width: 220, fixed: 'left', slotName: 'actions' }, { key: 'quoteNo', label: '报价号', minWidth: 210 }, { key: 'status', label: '状态', width: 90, slotName: 'status', afterColumn: 'quoteNo' },
|
||||
{ key: 'origin', label: '发货城市', minWidth: 150, slotName: 'origin' }, { key: 'destination', label: '目的仓码', minWidth: 115, slotName: 'destination' }, { key: 'miles', label: '距离(mile)', width: 110, slotName: 'miles' },
|
||||
{ key: 'partyName', label: '车队 / 委托客户', minWidth: 180 }, { key: 'currency', label: '币种', width: 80, slotName: 'currency' },
|
||||
{ key: 'pickup', label: '整车自提 / 单', width: 140, slotName: 'pickup' }, { key: 'full', label: '整车派送 / 单', width: 140, slotName: 'full' }, { key: 'loose', label: '散板派送 / 托', width: 140, slotName: 'loose' },
|
||||
{ key: 'validity', label: '有效期', minWidth: 220, slotName: 'validity' }, { key: 'status', label: '状态', width: 90, slotName: 'status' }, { key: 'updatedAt', label: '更新时间', minWidth: 190 }]
|
||||
{ key: 'validity', label: '有效期', minWidth: 220, slotName: 'validity' }, { key: 'updatedAt', label: '更新时间', minWidth: 190 }]
|
||||
async function load() { const current = ++sequence; loading.value = true; error.value = null; try { const result = await listFleetQuotes({ ...applied, kind: kind.value, page: page.value, size: size.value }); if (current === sequence) { rows.value = result.records; total.value = result.total } } catch (e) { if (current === sequence) { rows.value = []; total.value = 0; error.value = getApiErrorDetails(e, '报价加载失败') } } finally { if (current === sequence) loading.value = false } }
|
||||
function search() { applied = { keyword: String(filters.value.keyword || '') || undefined, status: String(filters.value.status || '') || undefined, effectiveOn: String(filters.value.effectiveOn || '') || undefined }; if (page.value !== 1) page.value = 1; else void load() }
|
||||
function open(row: FleetQuote | null, view = false) { selected.value = row; readonly.value = view; editor.value = true }
|
||||
|
||||
@@ -37,13 +37,13 @@ const summary = computed(() => [
|
||||
const columns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 245, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'appointments', label: '预约号', minWidth: 195, slotName: 'appointments' }, { prop: 'dispatchNo', label: '排车单号', minWidth: 300 },
|
||||
{ key: 'stage', label: '阶段', minWidth: 110, slotName: 'stage', afterColumn: 'dispatchNo' },
|
||||
{ key: 'carrier', label: '承运商', minWidth: 150, slotName: 'carrier' },
|
||||
{ prop: 'vehicle.plate', label: '车牌', minWidth: 130 }, { key: 'driver', label: '司机', minWidth: 150, slotName: 'driver' }, { prop: 'origin.warehouseName', label: '提货仓库', minWidth: 150 },
|
||||
{ key: 'destinations', label: '目的仓 / 预约到达 / 实际到达', minWidth: 285, slotName: 'destinations' },
|
||||
{ prop: 'actualPalletCount', label: '实际托数', minWidth: 105 },
|
||||
{ key: 'money', label: '整车承运金额 / 单价', minWidth: 170, slotName: 'money' },
|
||||
{ key: 'plannedDepartureAt', label: '计划发车时间', minWidth: 180, slotName: 'plannedDeparture' }, { key: 'loadingCompletedAt', label: '实际装车完成时间', minWidth: 180, slotName: 'loaded' },
|
||||
{ key: 'stage', label: '阶段', minWidth: 110, slotName: 'stage' },
|
||||
]
|
||||
function time(value: string | null | undefined) { return value && dayjs(value).isValid() ? dayjs(value).format('YYYY-MM-DD HH:mm') : '—' }
|
||||
function rowActions(row: OutsourcedBusinessRow): RowActionItem[] {
|
||||
|
||||
@@ -35,10 +35,10 @@ function syncFields() { fields.value = [
|
||||
syncFields()
|
||||
const columns: DataTableColumn[] = [
|
||||
{ key:'actions',label:'操作',width:270,fixed:'left',slotName:'actions' }, { key:'name',prop:'name',label:'报价名称',minWidth:190 },
|
||||
{ key:'status',label:'状态',width:76,slotName:'status', afterColumn: 'name' },
|
||||
{ key:'warehouseName',prop:'warehouseName',label:'海外仓',minWidth:170 }, { key:'providerName',prop:'providerName',label:'服务商',minWidth:180 },
|
||||
{ key:'currency',prop:'currency',label:'币种',width:74 }, { key:'effectiveStart',prop:'effectiveStart',label:'生效日期',width:112 },
|
||||
{ key:'effectiveEnd',prop:'effectiveEnd',label:'失效日期',width:112 }, { key:'versionCode',prop:'versionCode',label:'报价版本',width:92 },
|
||||
{ key:'status',label:'状态',width:76,slotName:'status' }, { key:'updatedAt',prop:'updatedAt',label:'更新时间',minWidth:150 }, { key:'remark',prop:'remark',label:'备注',minWidth:180 },
|
||||
{ key:'effectiveEnd',prop:'effectiveEnd',label:'失效日期',width:112 }, { key:'versionCode',prop:'versionCode',label:'报价版本',width:92 }, { key:'updatedAt',prop:'updatedAt',label:'更新时间',minWidth:150 }, { key:'remark',prop:'remark',label:'备注',minWidth:180 },
|
||||
]
|
||||
const form = reactive({ name:'', warehouseId:'', providerName:'', currency:'USD', effectiveStart:'', effectiveEnd:'', versionCode:'V1.0', remark:'', recordVersion:0, warehouseFees:[] as OverseasWarehouseFee[], transitPrices:[] as OverseasTransitPrice[] })
|
||||
function resetForm(){ Object.assign(form,{name:'',warehouseId:'',providerName:'',currency:'USD',effectiveStart:'',effectiveEnd:'',versionCode:'V1.0',remark:'',recordVersion:0,warehouseFees:[{item:'入库卸货',billingUnit:'托',unitPrice:0,minimumCharge:null,remark:null}],transitPrices:[]}) }
|
||||
|
||||
@@ -36,6 +36,7 @@ const summary = computed(() => [
|
||||
const columns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 245, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'appointments', label: '预约号', minWidth: 195, slotName: 'appointments' }, { prop: 'dispatchNo', label: '排车单号', minWidth: 300 },
|
||||
{ key: 'stage', label: '状态', minWidth: 110, slotName: 'stage', afterColumn: 'dispatchNo' },
|
||||
{ prop: 'vehicle.plate', label: '车牌', minWidth: 130 }, { key: 'driver', label: '司机', minWidth: 150, slotName: 'driver' }, { prop: 'origin.warehouseName', label: '起点', minWidth: 150 },
|
||||
{ key: 'destinations', label: '目的地', minWidth: 150, slotName: 'destinations' },
|
||||
|
||||
@@ -47,7 +48,6 @@ const columns: DataTableColumn[] = [
|
||||
{ key: 'loadingCompletedAt', label: '实际装车完成时间', minWidth: 180, slotName: 'loaded' },
|
||||
{ key: 'scheduledArrival', label: '预约到达时间', minWidth: 190, slotName: 'scheduledArrival' },
|
||||
{ key: 'actualArrival', label: '实际到达时间', minWidth: 190, slotName: 'actualArrival' },
|
||||
{ key: 'stage', label: '状态', minWidth: 110, slotName: 'stage' },
|
||||
]
|
||||
function time(value: string | null | undefined) { return value && dayjs(value).isValid() ? dayjs(value).format('YYYY-MM-DD HH:mm') : '—' }
|
||||
// 全列无值时只显示一个占位;多站有值时保留顺序,目的地通过悬停说明。
|
||||
|
||||
@@ -49,11 +49,11 @@ const planned = computed(() => detail.value?.trip.status === 'PLANNED')
|
||||
const editable = computed(() => planned.value && access.can(permission.tripEdit))
|
||||
const time = (value: string | null | undefined) => value ? new Date(value).toLocaleString('zh-CN', { hour12: false }) : '—'
|
||||
const bindingsColumns: DataTableColumn[] = [{ key: 'actions', label: '操作', width: 320, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'dispatchNo', prop: 'dispatchNo', label: '排车单号', minWidth: 210 }, { key: 'packageCount', prop: 'packageCount', label: '计划箱数', width: 120 },
|
||||
{ key: 'review', label: '调度审核', slotName: 'review', minWidth: 210 },
|
||||
{ key: 'dispatchNo', prop: 'dispatchNo', label: '排车单号', minWidth: 210 },
|
||||
{ key: 'review', label: '调度审核', slotName: 'review', minWidth: 210, afterColumn: 'dispatchNo' }, { key: 'packageCount', prop: 'packageCount', label: '计划箱数', width: 120 },
|
||||
{ key: 'boundAt', label: '关联时间', slotName: 'boundAt', minWidth: 180 }]
|
||||
const outcomeColumns: DataTableColumn[] = [{ key: 'deliveryNo', prop: 'deliveryNo', label: '派送单号', minWidth: 220 },
|
||||
{ key: 'destination', prop: 'destination', label: '目的地', minWidth: 180 }, { key: 'status', label: '状态', slotName: 'status', width: 115 },
|
||||
const outcomeColumns: DataTableColumn[] = [{ key: 'deliveryNo', prop: 'deliveryNo', label: '派送单号', minWidth: 220 }, { key: 'status', label: '状态', slotName: 'status', width: 115, afterColumn: 'deliveryNo' },
|
||||
{ key: 'destination', prop: 'destination', label: '目的地', minWidth: 180 },
|
||||
{ key: 'packages', prop: 'packages', label: '原实发箱数', width: 110 },
|
||||
{ key: 'signedPackages', prop: 'signedPackages', label: '实签箱数', width: 100 },
|
||||
{ key: 'returnedPackages', prop: 'returnedPackages', label: '本趟实退箱数', width: 120 },
|
||||
|
||||
@@ -34,7 +34,7 @@ const fields: QueryField[] = [
|
||||
{ key: 'status', label: '状态', placeholder: '全部状态', type: 'select', width: 140, props: { 'aria-label': '状态' }, options: [{ value: 'IN_TRANSIT', label: '在途' }, { value: 'COMPLETED', label: '货物已结案' }] },
|
||||
]
|
||||
const columns: DataTableColumn[] = [{ key: 'actions', label: '操作', width: 140, fixed: 'left', slotName: 'actions' }, { key: 'tripNo', label: '车次号', minWidth: 220 },
|
||||
{ key: 'status', label: '状态', width: 125, slotName: 'status' }, { key: 'vehiclePlate', label: '发车车牌', minWidth: 125 }, { key: 'driverName', label: '发车司机', minWidth: 100 },
|
||||
{ key: 'status', label: '状态', width: 125, slotName: 'status', afterColumn: 'tripNo' }, { key: 'vehiclePlate', label: '发车车牌', minWidth: 125 }, { key: 'driverName', label: '发车司机', minWidth: 100 },
|
||||
{ key: 'warehouseName', label: '起运仓', minWidth: 150 }, { key: 'destinations', label: '目的地', minWidth: 180, slotName: 'destinations' },
|
||||
{ key: 'departedAt', label: '实际发车(UTC)', minWidth: 190 }, { key: 'physicalPallets', label: '实物托盘', width: 100, formatter: (row: TripOperationsRow) => row.physicalPallets ?? '—' },
|
||||
{ key: 'agreed', label: '外接约定金额', minWidth: 170, slotName: 'agreed' },
|
||||
|
||||
@@ -27,8 +27,8 @@ let applied: Omit<TripQuery, 'page' | 'size'> = {}, sequence = 0
|
||||
const fields: QueryField[] = [{ key: 'keyword', type: 'input', label: '关键词', placeholder: '车次号 / 车牌 / 司机', width: 250 },
|
||||
{ key: 'status', type: 'select', label: '状态', placeholder: '全部状态', width: 150, options: Object.entries(tripStatusLabels).map(([value, label]) => ({ value, label })) }]
|
||||
const columns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 85, fixed: 'left', slotName: 'actions' }, { key: 'tripNo', label: '车次号', minWidth: 215 },
|
||||
{ key: 'purpose', label: '任务类型', slotName: 'purpose', width: 110 }, { key: 'status', label: '状态', slotName: 'status', width: 120 }, { key: 'warehouseName', label: '起运仓', minWidth: 160 },
|
||||
{ key: 'actions', label: '操作', width: 85, fixed: 'left', slotName: 'actions' }, { key: 'tripNo', label: '车次号', minWidth: 215 }, { key: 'status', label: '状态', slotName: 'status', width: 120, afterColumn: 'tripNo' },
|
||||
{ key: 'purpose', label: '任务类型', slotName: 'purpose', width: 110 }, { key: 'warehouseName', label: '起运仓', minWidth: 160 },
|
||||
{ key: 'vehiclePlate', label: '车牌', minWidth: 130 }, { key: 'driverName', label: '司机', minWidth: 110 },
|
||||
{ key: 'departure', label: '计划发车', slotName: 'departure', minWidth: 175 }, { key: 'end', label: '司机预计结束', slotName: 'end', minWidth: 175 },
|
||||
{ key: 'route', label: '路线', slotName: 'route', minWidth: 240 }, { key: 'remark', label: '备注', minWidth: 180 },
|
||||
|
||||
@@ -16,7 +16,7 @@ const submitExport = useExportTask()
|
||||
const router=useRouter(),access=useAccessPreviewStore(),rows=ref<VehicleDetail[]>([]),total=ref(0),page=ref(1),pageSize=ref(20),loading=ref(false),saving=ref(false),exporting=ref(false),error=ref<ApiErrorDetails|null>(null),editorOpen=ref(false),editingId=ref(''),statusOpen=ref(false),statusRow=ref<VehicleDetail|null>(null),statusTarget=ref<VehicleStatus>('IN_SERVICE')
|
||||
const ownership=ref<VehicleOwnership>('OWNED'),tabs:[string,VehicleOwnership][]=[['自有车辆','OWNED'],['外派车辆','DISPATCHED']]
|
||||
const filters=ref<QueryModel>({keyword:'',region:'',status:''});const fields:QueryField[]=[{key:'keyword',type:'input',label:'车辆关键词',placeholder:'车牌 / 司机 / 车型',width:220},{key:'region',type:'select',label:'区域',placeholder:'全部区域',width:140,options:[{label:'国内',value:'DOMESTIC'},{label:'海外',value:'OVERSEAS'}]},{key:'status',type:'select',label:'状态',placeholder:'全部状态',width:140,options:Object.entries(vehicleStatusLabels).map(([value,label])=>({label,value}))}]
|
||||
const columns:DataTableColumn[]=[{key:'actions',label:'操作',width:250,fixed:'left',slotName:'actions'},{key:'plateNumber',prop:'plateNumber',label:'车牌',minWidth:130},{key:'model',prop:'model',label:'车型',minWidth:180},{key:'fuelConsumptionPer100Km',prop:'fuelConsumptionPer100Km',label:'油耗(L/100km)',minWidth:130,align:'right'},{key:'mileageKm',prop:'mileageKm',label:'当前公里码(km)',minWidth:140,align:'right'},{key:'driverOrDispatchCompany',prop:'driverOrDispatchCompany',label:'司机 / 外派单位',minWidth:160},{key:'phone',prop:'phone',label:'联系电话',minWidth:145},{key:'maxPalletSpaces',prop:'maxPalletSpaces',label:'最大托位',minWidth:100,align:'right'},{key:'maxLoadKg',prop:'maxLoadKg',label:'最大载重(kg)',minWidth:130,align:'right'},{key:'maxVolumeCbm',prop:'maxVolumeCbm',label:'可用容积(m³)',minWidth:130,align:'right'},{key:'status',label:'状态',width:100,slotName:'status'},{key:'region',label:'区域',width:100,slotName:'region'}]
|
||||
const columns:DataTableColumn[]=[{key:'actions',label:'操作',width:250,fixed:'left',slotName:'actions'},{key:'plateNumber',prop:'plateNumber',label:'车牌',minWidth:130},{key:'status',label:'状态',width:100,slotName:'status', afterColumn: 'plateNumber' },{key:'model',prop:'model',label:'车型',minWidth:180},{key:'fuelConsumptionPer100Km',prop:'fuelConsumptionPer100Km',label:'油耗(L/100km)',minWidth:130,align:'right'},{key:'mileageKm',prop:'mileageKm',label:'当前公里码(km)',minWidth:140,align:'right'},{key:'driverOrDispatchCompany',prop:'driverOrDispatchCompany',label:'司机 / 外派单位',minWidth:160},{key:'phone',prop:'phone',label:'联系电话',minWidth:145},{key:'maxPalletSpaces',prop:'maxPalletSpaces',label:'最大托位',minWidth:100,align:'right'},{key:'maxLoadKg',prop:'maxLoadKg',label:'最大载重(kg)',minWidth:130,align:'right'},{key:'maxVolumeCbm',prop:'maxVolumeCbm',label:'可用容积(m³)',minWidth:130,align:'right'},{key:'region',label:'区域',width:100,slotName:'region'}]
|
||||
const form=reactive({plateNumber:'',model:'',fuelConsumptionPer100Km:null as number|null,mileageKm:0,driverOrDispatchCompany:'',phone:'',region:'OVERSEAS' as VehicleRegion,ownership:'OWNED' as VehicleOwnership,status:'IN_SERVICE' as VehicleStatus,remark:'',recordVersion:0,maxPalletSpaces:null as number|null,maxLoadKg:null as number|null,maxVolumeCbm:null as number|null})
|
||||
function params(){return{page:page.value,size:pageSize.value,keyword:String(filters.value.keyword||'')||undefined,ownership:ownership.value,region:(filters.value.region||undefined) as VehicleRegion|undefined,status:(filters.value.status||undefined) as VehicleStatus|undefined}}
|
||||
let querySequence = 0
|
||||
|
||||
@@ -124,10 +124,10 @@ async function save() {
|
||||
<section v-for="group in visibleGroups" :key="group.category" class="document-group density-dialog-section" data-tone="source" :data-category="group.category">
|
||||
<header><h3 class="density-dialog-section__title">{{ group.label }}</h3><el-tag :type="group.files.length ? 'success' : 'info'">{{ group.files.length ? '已上传' : '待上传' }}</el-tag><span>{{ group.files.length }}/20</span><el-upload v-if="canEdit" :key="uploadKey" :auto-upload="false" :show-file-list="false" :disabled="busy || conflict || group.files.length >= 20" :on-change="(file: UploadFile) => upload(group.category, file)"><el-button class="density-action" data-tone="primary" :disabled="busy || conflict || group.files.length >= 20">上传</el-button></el-upload></header>
|
||||
<p v-if="!group.files.length" class="density-dialog-note">尚未上传文件</p>
|
||||
<table v-else class="density-table document-files"><thead><tr><th class="document-file-actions">操作</th><th>文件名称</th><th class="document-file-time">上传时间</th><th class="document-file-state">保存状态</th></tr></thead><tbody>
|
||||
<table v-else class="density-table document-files"><thead><tr><th class="document-file-actions">操作</th><th>文件名称</th><th class="document-file-state">保存状态</th><th class="document-file-time">上传时间</th></tr></thead><tbody>
|
||||
<tr v-for="file in group.files" :key="file.fileId" class="document-file">
|
||||
<td><div class="density-actions"><el-button link :aria-label="'下载 ' + file.fileName" :disabled="downloading || !isSaved(group.category, file.fileId)" @click="download(group.category, file)">下载</el-button><template v-if="canEdit"><el-upload :key="uploadKey" :auto-upload="false" :show-file-list="false" :disabled="busy || conflict" :on-change="(uploadFile: UploadFile) => upload(group.category, uploadFile, file.fileId)"><el-button class="density-action" data-tone="primary" :disabled="busy || conflict">替换</el-button></el-upload><el-button class="density-action" data-tone="danger" :disabled="busy || conflict" @click="remove(group.category, file.fileId)">删除</el-button></template></div></td>
|
||||
<td><LineClampText :content="file.fileName" /></td><td>{{ file.uploadedAt?.replace('T', ' ') || '—' }}</td><td>{{ isSaved(group.category, file.fileId) ? '已保存' : '待保存' }}</td>
|
||||
<td><LineClampText :content="file.fileName" /></td><td>{{ isSaved(group.category, file.fileId) ? '已保存' : '待保存' }}</td><td>{{ file.uploadedAt?.replace('T', ' ') || '—' }}</td>
|
||||
</tr>
|
||||
</tbody></table>
|
||||
</section>
|
||||
|
||||
@@ -48,6 +48,6 @@ onBeforeUnmount(() => { generation++ })
|
||||
<p v-if="!deliveries.length" class="density-dialog-note">尚未生成派送单费用。</p>
|
||||
<p v-else-if="!allowed" class="density-dialog-note">暂无派送单费用查看权限。</p>
|
||||
<LoadErrorState v-else-if="error" title="费用读取失败" :message="error.message" compact @retry="load"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState>
|
||||
<table v-else class="density-table"><thead><tr><th>客户</th><th>派送单号</th><th>费用项目</th><th>币种</th><th>金额</th><th>费用状态</th></tr></thead><tbody><tr v-for="fee in fees" :key="`${fee.deliveryNo}-${fee.id}`"><td>{{ fee.customerName || '—' }}</td><td>{{ fee.deliveryNo }}</td><td>{{ labels[fee.feeType] || fee.feeType || '—' }}</td><td>{{ fee.currency || '—' }}</td><td>{{ fee.amount ?? '—' }}</td><td>{{ status(fee) }}</td></tr><tr v-if="!loading && !fees.length"><td colspan="6">暂无正式费用记录</td></tr></tbody></table>
|
||||
<table v-else class="density-table"><thead><tr><th>客户</th><th>派送单号</th><th>费用状态</th><th>费用项目</th><th>币种</th><th>金额</th></tr></thead><tbody><tr v-for="fee in fees" :key="`${fee.deliveryNo}-${fee.id}`"><td>{{ fee.customerName || '—' }}</td><td>{{ fee.deliveryNo }}</td><td>{{ status(fee) }}</td><td>{{ labels[fee.feeType] || fee.feeType || '—' }}</td><td>{{ fee.currency || '—' }}</td><td>{{ fee.amount ?? '—' }}</td></tr><tr v-if="!loading && !fees.length"><td colspan="6">暂无正式费用记录</td></tr></tbody></table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -296,10 +296,10 @@ onBeforeUnmount(clearTruckPreviewUrls)
|
||||
.pickup-detail-facts dd { display: flex; flex-wrap: wrap; gap: 4px 10px; }
|
||||
.truck-evidence-panel { display: flex; align-items: center; flex-wrap: wrap; gap: 12px; padding: 12px 14px; border: 1px solid var(--cc-border); border-radius: var(--cc-radius-md); background: var(--cc-surface); }
|
||||
.truck-evidence-panel > h2 { flex: 0 0 100%; margin: 0; }
|
||||
.truck-evidence-panel label { display: inline-flex; align-items: center; gap: 10px; color: var(--cc-text); font-size: 13px; }
|
||||
.truck-evidence-panel label { display: inline-flex; align-items: center; gap: 10px; color: var(--cc-text); font-size: var(--admin-font-body); }
|
||||
.truck-evidence-upload { padding: 8px 14px; border: 1px solid var(--el-color-primary); border-radius: var(--cc-radius-sm); color: var(--el-color-primary) !important; cursor: pointer; }
|
||||
.truck-evidence-upload input[type='file'] { display: none; }
|
||||
.truck-evidence-list { display: flex; flex-wrap: wrap; gap: 12px; color: var(--cc-muted); font-size: 12px; }
|
||||
.truck-evidence-list { display: flex; flex-wrap: wrap; gap: 12px; color: var(--cc-muted); font-size: var(--admin-font-caption); }
|
||||
.truck-evidence-item { width: 132px; margin: 0; }
|
||||
.truck-evidence-item { position: relative; }
|
||||
.truck-evidence-item .el-image { display: block; width: 132px; height: 92px; overflow: hidden; border: 1px solid var(--cc-border); border-radius: var(--cc-radius-sm); background: var(--cc-surface-muted); cursor: pointer; }
|
||||
|
||||
@@ -123,10 +123,10 @@ const gateText = computed(() => {
|
||||
const allocationRows = computed(() => (detail.value?.allocations || []).map(row => ({ ...row, skuCode: row.skuCode && row.skuCode !== 'MIXED' ? row.skuCode : '—', movementStatus: ({ RESERVED: '已分配', SHIPPED: '已发运', RECEIVED: '目标已实收' } as Record<string, string>)[row.status] || '—' })))
|
||||
const columns: DataTableColumn[] = [
|
||||
{ key: 'inboundCode', prop: 'inboundCode', label: '来源入仓码', minWidth: 160 },
|
||||
{ key: 'movementStatus', prop: 'movementStatus', label: '履约状态', width: 120, afterColumn: 'inboundCode' },
|
||||
{ key: 'palletNo', prop: 'palletNo', label: '来源托盘码', minWidth: 180 },
|
||||
{ key: 'skuCode', prop: 'skuCode', label: '实际 SKU', minWidth: 150 },
|
||||
{ key: 'packageCount', prop: 'packageCount', label: '分配箱数', width: 100 },
|
||||
{ key: 'movementStatus', prop: 'movementStatus', label: '履约状态', width: 120 },
|
||||
]
|
||||
async function load() {
|
||||
const current = ++request
|
||||
|
||||
@@ -196,7 +196,7 @@ onBeforeUnmount(() => { generation++ })
|
||||
.receipt-box-heading{display:flex;align-items:center;justify-content:flex-start;flex-wrap:wrap;gap:12px}
|
||||
.receipt-discrepancy-reason{margin:0;white-space:pre-wrap;overflow-wrap:anywhere}
|
||||
.transfer-receipt-panel{display:grid;gap:var(--admin-space-2)}
|
||||
.receipt-aligned-table{width:100%;max-width:1490px;min-width:0}
|
||||
.receipt-aligned-table{width:100%;min-width:0}
|
||||
.receipt-difference{color:var(--el-color-danger);font-weight:600}
|
||||
h3 small{margin-left:8px;color:var(--workbench-muted);font-size:var(--admin-font-caption);font-weight:400}
|
||||
h3{margin:0;font-size:var(--admin-font-section)}
|
||||
|
||||
@@ -22,7 +22,7 @@ const access = useAccessPreviewStore(); const rows = ref<CargoDiscardSummary[]>(
|
||||
/** 数量事实缺失时明确提示,不用零代替未记录。 */
|
||||
function quantity(value: number | null | undefined, unit: string) { return value == null ? '未记录 / 不适用' : `${value} ${unit}` }
|
||||
const fields: QueryField[] = [{ key: 'customerId', type: 'select', label: '客户', placeholder: '请选择客户', options: [] }, { key: 'discardNo', type: 'input', label: '废弃单号', placeholder: '废弃单号', width: 220 }, { key: 'inboundCode', type: 'input', label: '入仓码', placeholder: '请输入入仓码', width: 180 }, { key: 'status', type: 'select', label: '状态', placeholder: '全部状态', options: [{ label: '待审核', value: 'PENDING_REVIEW' }, { label: '已审核', value: 'APPROVED' }, { label: '待客户确认', value: 'PDA_COMPLETED' }, { label: '已完成', value: 'COMPLETED' }, { label: '已驳回', value: 'REJECTED' }, { label: '已取消', value: 'CANCELLED' }] }]
|
||||
const columns: DataTableColumn[] = [{ key: 'actions', label: '操作', width: 140, fixed: 'left', slotName: 'actions' }, { key: 'discardNo', label: '废弃单号', minWidth: 180 }, { key: 'inboundCodes', label: '入仓码', minWidth: 180 }, { key: 'overseasWarehouseName', label: '仓库', minWidth: 140 }, { key: 'requestedPackages', label: '申请箱数', width: 90 }, { key: 'totalFee', label: '废弃费用', width: 120, formatter: (row: CargoDiscardSummary) => `${row.currency || 'USD'} ${row.totalFee ?? 0}` }, { key: 'createdBy', label: '申请人', width: 110 }, { key: 'createdAt', label: '创建时间', minWidth: 170 }, { key: 'status', label: '状态', width: 150, slotName: 'status' }]
|
||||
const columns: DataTableColumn[] = [{ key: 'actions', label: '操作', width: 140, fixed: 'left', slotName: 'actions' }, { key: 'discardNo', label: '废弃单号', minWidth: 180 }, { key: 'status', label: '状态', width: 150, slotName: 'status', afterColumn: 'discardNo' }, { key: 'inboundCodes', label: '入仓码', minWidth: 180 }, { key: 'overseasWarehouseName', label: '仓库', minWidth: 140 }, { key: 'requestedPackages', label: '申请箱数', width: 90 }, { key: 'totalFee', label: '废弃费用', width: 120, formatter: (row: CargoDiscardSummary) => `${row.currency || 'USD'} ${row.totalFee ?? 0}` }, { key: 'createdBy', label: '申请人', width: 110 }, { key: 'createdAt', label: '创建时间', minWidth: 170 }]
|
||||
const statusLabels: Record<string, string> = { PENDING_REVIEW: '待审核', APPROVED: '已审核,待 PDA', PDA_COMPLETED: 'PDA 已完成,待客户确认', COMPLETED: '已完成', REJECTED: '已驳回', CANCELLED: '已取消' }
|
||||
const customerError = ref<ApiErrorDetails | null>(null); const listError = ref<ApiErrorDetails | null>(null)
|
||||
const detailError = ref<ApiErrorDetails | null>(null); const reviewError = ref<ApiErrorDetails | null>(null)
|
||||
|
||||
@@ -25,10 +25,10 @@ const fields: QueryField[] = [
|
||||
]
|
||||
const columns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 180, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'cartonNo', label: '箱号', minWidth: 180 }, { key: 'fbaWarehouse', label: '所属FBA仓', minWidth: 150 },
|
||||
{ key: 'cartonNo', label: '箱号', minWidth: 180 },
|
||||
{ key: 'status', label: '状态', width: 100, afterColumn: 'cartonNo' }, { key: 'fbaWarehouse', label: '所属FBA仓', minWidth: 150 },
|
||||
{ key: 'sku', label: '商品SKU', minWidth: 150 }, { key: 'productName', label: '中文品名', minWidth: 180 },
|
||||
{ key: 'quantity', label: '数量', width: 100, align: 'right' }, { key: 'weight', label: '重量(kg)', width: 110, align: 'right' },
|
||||
{ key: 'status', label: '状态', width: 100 },
|
||||
]
|
||||
const dialogOpen = ref(false)
|
||||
const dialogMode = ref<'create' | 'edit'>('create')
|
||||
|
||||
@@ -44,11 +44,11 @@ const fields: QueryField[] = [
|
||||
const columns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 130, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'deliveryNo', prop: 'deliveryNo', label: '派送单号', minWidth: 180, fixed: 'left' },
|
||||
{ key: 'status', prop: 'status', label: '回访状态', width: 110, slotName: 'status', afterColumn: 'deliveryNo' },
|
||||
{ key: 'customerName', prop: 'customerName', label: '客户', minWidth: 180 },
|
||||
{ key: 'contact', label: '联系人', minWidth: 170, formatter: (row: CustomerVisitRow) => [row.contactName, row.contactPhone].filter(Boolean).join(' ') || '—' },
|
||||
{ key: 'deliveryCompletedAt', prop: 'deliveryCompletedAt', label: '完成时间', minWidth: 170, formatter: (row: CustomerVisitRow) => formatOverseasDateTime(row.deliveryCompletedAt) },
|
||||
{ key: 'destination', prop: 'destination', label: '目的地', minWidth: 140 },
|
||||
{ key: 'status', prop: 'status', label: '回访状态', width: 110, slotName: 'status' },
|
||||
{ key: 'visitedAt', prop: 'visitedAt', label: '回访时间', minWidth: 170, formatter: (row: CustomerVisitRow) => formatOverseasDateTime(row.visitedAt) },
|
||||
{ key: 'visitResult', prop: 'visitResult', label: '回访结果', minWidth: 130, formatter: (row: CustomerVisitRow) => resultLabels[row.visitResult || ''] || '—' },
|
||||
{ key: 'attachments', label: '资料', minWidth: 110, formatter: (row: CustomerVisitRow) => row.attachments.length ? `${row.attachments.length} 份` : '—' },
|
||||
|
||||
@@ -113,8 +113,8 @@ watch([deliveryNo, customerId], () => { void load() }, { immediate: true })
|
||||
<h2 v-if="detail?.fees.length" class="fulfillment-fees-heading">履约费用记录</h2>
|
||||
<el-table v-if="detail?.fees.length" :data="detail.fees" border stripe empty-text="暂无履约费用">
|
||||
<el-table-column type="index" label="序号" width="80" /><el-table-column label="应收/应付" width="120"><template #default="scope">{{ scope.row.feeDirection === 'PAYABLE' ? '应付' : scope.row.feeDirection === 'RECEIVABLE' ? '应收' : show(scope.row.feeDirection) }}</template></el-table-column>
|
||||
<el-table-column label="费用类型" min-width="180"><template #default="scope">{{ feeLabels[scope.row.feeType] || scope.row.feeType }}</template></el-table-column><el-table-column prop="currency" label="币种" width="100" />
|
||||
<el-table-column prop="amount" label="金额" min-width="140" align="right" /><el-table-column label="费用状态" min-width="165"><template #default="scope">{{ feeStatus(scope.row as DeliveryFeeRow) }}</template></el-table-column><el-table-column prop="remark" label="备注" min-width="240" />
|
||||
<el-table-column label="费用类型" min-width="180"><template #default="scope">{{ feeLabels[scope.row.feeType] || scope.row.feeType }}</template></el-table-column><el-table-column label="费用状态" min-width="165"><template #default="scope">{{ feeStatus(scope.row as DeliveryFeeRow) }}</template></el-table-column>
|
||||
<el-table-column prop="currency" label="币种" width="100" /><el-table-column prop="amount" label="金额" min-width="140" align="right" /><el-table-column prop="remark" label="备注" min-width="240" />
|
||||
</el-table>
|
||||
</section>
|
||||
</el-tab-pane>
|
||||
|
||||
@@ -51,6 +51,7 @@ const fields = computed<QueryField[]>(() => [
|
||||
const columns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 300, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'deliveryNo', prop: 'deliveryNo', label: '派送单号', minWidth: 230, fixed: 'left' },
|
||||
{ key: 'status', prop: 'status', label: '订单状态', width: 110, slotName: 'status', afterColumn: 'deliveryNo' },
|
||||
{ key: 'documents', label: '所需资料', minWidth: 380, slotName: 'documents' },
|
||||
{ key: 'bolNo', prop: 'bolNo', label: 'BOL号', minWidth: 170 },
|
||||
{ key: 'customerName', prop: 'customerName', label: '客户', minWidth: 160 },
|
||||
@@ -58,7 +59,6 @@ const columns: DataTableColumn[] = [
|
||||
{ key: 'overseasWarehouseName', prop: 'overseasWarehouseName', label: '仓库', minWidth: 150 },
|
||||
{ key: 'dispatchNo', prop: 'dispatchNo', label: '排车单号', minWidth: 170 },
|
||||
{ key: 'waybillNo', prop: 'waybillNo', label: '运单号', minWidth: 150 },
|
||||
{ key: 'status', prop: 'status', label: '订单状态', width: 110, slotName: 'status' },
|
||||
{ key: 'deliveryMethod', prop: 'deliveryMethod', label: '派送方式', width: 120, formatter: (row: DeliveryOrderRow) => deliveryMethodLabel(row.deliveryMethod) },
|
||||
{ key: 'carrierName', prop: 'carrierName', label: '派送人', minWidth: 150 },
|
||||
{ key: 'vehiclePlate', prop: 'vehiclePlate', label: '车牌', minWidth: 120 },
|
||||
|
||||
@@ -75,18 +75,18 @@ const columns = computed<DataTableColumn[]>(() => [
|
||||
{ key: 'actions', label: '操作', width: rows.value.some(row => row.taskType === 'PICKUP') ? 330 : 220, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'taskType', label: '排车类型', width: 115, slotName: 'taskType' },
|
||||
{ key: 'dispatchNo', prop: 'dispatchNo', label: '排车单号', minWidth: 190 },
|
||||
{ key: 'vehicleStatus', label: '车辆状态', minWidth: 110, slotName: 'vehicleStatus', afterColumn: 'dispatchNo' }, { key: 'stage', label: '作业阶段', minWidth: 110, slotName: 'stage', afterColumn: 'dispatchNo' },
|
||||
{ key: 'createdAt', label: '创建时间', minWidth: 170, slotName: 'createdAt' },
|
||||
{ key: 'vehicle', label: '车牌号', minWidth: 130, slotName: 'vehicle' }, { key: 'driver', label: '司机', minWidth: 100, slotName: 'driver' },
|
||||
{ key: 'driverPhone', label: '联系电话', minWidth: 140, slotName: 'driverPhone' },
|
||||
{ key: 'appointmentNo', label: '预约号', minWidth: 160, slotName: 'appointmentNo' },
|
||||
{ key: 'appointmentAt', label: '确认送达时间', minWidth: 170, slotName: 'appointmentAt' },
|
||||
{ key: 'shipmentNumbers', label: '货件号', minWidth: 180, slotName: 'shipmentNumbers' },
|
||||
{ key: 'vehicleStatus', label: '车辆状态', minWidth: 110, slotName: 'vehicleStatus' },
|
||||
{ key: 'warehouseName', prop: 'warehouseName', label: '起运仓', minWidth: 160 },
|
||||
...(showSections.value && section.value === 'VEHICLE_INFO' ? [{ key: 'returnReason', label: '退回原因', minWidth: 240, slotName: 'returnReason' }] : []),
|
||||
...(showSections.value && section.value !== 'VEHICLE_INFO' ? [{ key: 'review', label: section.value === 'DISPATCH_REVIEW' ? '调度审核' : '理货员审核', minWidth: 210, slotName: 'review' }] : []),
|
||||
...(showSections.value && section.value !== 'VEHICLE_INFO' ? [{ key: 'review', label: section.value === 'DISPATCH_REVIEW' ? '调度审核' : '理货员审核', minWidth: 210, slotName: 'review', afterColumn: 'dispatchNo' }] : []),
|
||||
{ key: 'counts', label: '托数/箱数/已拣', minWidth: 160, slotName: 'counts' }, { key: 'planned', label: '计划发车', minWidth: 165, slotName: 'planned' },
|
||||
{ key: 'departure', label: '实际发车', minWidth: 165, slotName: 'departure' }, { key: 'stage', label: '作业阶段', minWidth: 110, slotName: 'stage' },
|
||||
{ key: 'departure', label: '实际发车', minWidth: 165, slotName: 'departure' },
|
||||
...(stage.value === 'DELIVERING' ? [{ key: 'routeMap', label: '路线', width: 100, slotName: 'routeMap' }] : []),
|
||||
])
|
||||
const preferenceKey = computed(() => `overseas-dispatches-${stage.value}-${section.value}`)
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import ElementPlus from 'element-plus'
|
||||
import { computed } from 'vue'
|
||||
import { flushPromises, mount } from '@vue/test-utils'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createExceptionOrder, listExceptionOrders, resolveExceptionRelation, getExceptionDetail } from '../api/exceptionOrders'
|
||||
import ExceptionOrdersView from './ExceptionOrdersView.vue'
|
||||
import { commonPhase5Stubs } from './phase5TestStubs'
|
||||
import { UI_PREFERENCE_SCOPE, preferenceStorageKey } from '@/shared/utils/uiLayoutPreference'
|
||||
|
||||
const canPermission = vi.hoisted(() => vi.fn((_code: string) => false))
|
||||
vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: canPermission }) }))
|
||||
@@ -13,6 +15,7 @@ function mountView() { return mount(ExceptionOrdersView, { global: { plugins: [E
|
||||
|
||||
describe('ExceptionOrdersView permissions and creation', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear()
|
||||
canPermission.mockReset().mockReturnValue(false)
|
||||
vi.mocked(getExceptionDetail).mockReset().mockResolvedValue({ order: { id: '19', exceptionNo: 'EXC-1', customerId: '100', customerName: '示例客户',
|
||||
exceptionType: 'DAMAGED', status: 'PROCESSING', reason: '包装破损', handlerName: '1000001', version: 1, allowedActions: ['COMPLETE_EXCEPTION'], exceptionAt: '', updatedAt: '' }, returnSources: [], returnReceipts: [] })
|
||||
@@ -21,6 +24,30 @@ describe('ExceptionOrdersView permissions and creation', () => {
|
||||
vi.mocked(createExceptionOrder).mockReset().mockResolvedValue({ id: '19', exceptionNo: 'EXC-1', status: 'PENDING' })
|
||||
})
|
||||
|
||||
it('places the processing status after the exception number when old column settings exist', async () => {
|
||||
localStorage.setItem(preferenceStorageKey('table', 'exception-user', 'overseas-exception-orders'), JSON.stringify({
|
||||
version: 1,
|
||||
order: ['actions', 'exceptionNo', 'related', 'customerName', 'reason', 'status'],
|
||||
hidden: ['reason'],
|
||||
widths: { status: 135 },
|
||||
}))
|
||||
const wrapper = mount(ExceptionOrdersView, { global: {
|
||||
plugins: [ElementPlus],
|
||||
provide: { [UI_PREFERENCE_SCOPE as symbol]: computed(() => 'exception-user') },
|
||||
stubs: {
|
||||
...commonPhase5Stubs,
|
||||
DataTable: false,
|
||||
ElTable: { template: '<div><slot /></div>' },
|
||||
ElTableColumn: { props: ['label', 'width'], template: '<span class="column-title">{{ label }}</span>' },
|
||||
},
|
||||
} })
|
||||
await flushPromises()
|
||||
expect(wrapper.findAll('.column-title').slice(0, 3).map(column => column.text()))
|
||||
.toEqual(['操作', '异常订单号', '处理状态'])
|
||||
expect(wrapper.findAll('.column-title').some(column => column.text() === '异常原因')).toBe(false)
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('hides creation without the independent permission', async () => {
|
||||
const wrapper = mountView()
|
||||
await flushPromises()
|
||||
|
||||
@@ -31,6 +31,7 @@ const fields: QueryField[] = [
|
||||
const columns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 100, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'exceptionNo', prop: 'exceptionNo', label: '异常订单号', minWidth: 180, fixed: 'left' },
|
||||
{ key: 'status', label: '处理状态', width: 100, slotName: 'status', afterColumn: 'exceptionNo' },
|
||||
{ key: 'related', label: '关联订单号', minWidth: 230, slotName: 'related' },
|
||||
{ key: 'exceptionType', label: '异常类型', width: 100, formatter: (row: ExceptionOrderRow) => typeLabels[row.exceptionType] || row.exceptionType },
|
||||
{ key: 'customerName', prop: 'customerName', label: '客户', minWidth: 160 },
|
||||
@@ -39,7 +40,6 @@ const columns: DataTableColumn[] = [
|
||||
{ key: 'skuCode', prop: 'skuCode', label: 'SKU', minWidth: 120 },
|
||||
{ key: 'packageCount', prop: 'packageCount', label: '箱数', width: 80 },
|
||||
{ key: 'exceptionAt', label: '异常时间', minWidth: 170, formatter: (row: ExceptionOrderRow) => formatOverseasDateTime(row.exceptionAt) },
|
||||
{ key: 'status', label: '处理状态', width: 100, slotName: 'status' },
|
||||
{ key: 'handlingAction', prop: 'handlingAction', label: '处理动作', minWidth: 140 },
|
||||
{ key: 'returnLogisticsNo', prop: 'returnLogisticsNo', label: '退回物流单号', minWidth: 160 },
|
||||
{ key: 'handlerName', prop: 'handlerName', label: '处理人', minWidth: 110, slotName: 'handlerName' },
|
||||
|
||||
@@ -50,11 +50,11 @@ const palletColumns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 100, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'inboundCode', prop: 'inboundCode', label: '入仓码', minWidth: 150 },
|
||||
{ key: 'palletNo', prop: 'palletNo', label: '托盘号', minWidth: 150 },
|
||||
{ key: 'putawayStatus', label: '上架状态', width: 110, prop: 'putawayStatus', afterColumn: 'palletNo' },
|
||||
{ key: 'palletOwner', prop: 'palletOwner', label: '托盘所属计划', minWidth: 190 },
|
||||
{ key: 'packageCount', prop: 'packageCount', label: '对应箱数', width: 110, align: 'right' },
|
||||
{ key: 'location', label: '当前库位', minWidth: 150, slotName: 'location' },
|
||||
{ key: 'locationType', label: '库位类型', width: 120, prop: 'locationType' },
|
||||
{ key: 'putawayStatus', label: '上架状态', width: 110, prop: 'putawayStatus' },
|
||||
]
|
||||
const palletRows = computed(() => (detail.value?.allocations || []).map((allocation, index) => {
|
||||
const line = detail.value?.lines.find(item => item.id === allocation.lineId)
|
||||
|
||||
@@ -81,6 +81,10 @@ const columns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 300, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'selection', type: 'selection', width: 48, fixed: 'left', selectable: selectableRow },
|
||||
{ key: 'orderNo', prop: 'orderNo', label: '入库单号', minWidth: 170 },
|
||||
{ key: 'status', prop: 'status', label: '状态', width: 100, afterColumn: 'orderNo' },
|
||||
{ key: 'appointmentStatus', prop: 'appointmentStatus', label: 'FBA预约', width: 110, afterColumn: 'orderNo' },
|
||||
{ key: 'dispatchStatus', prop: 'dispatchStatus', label: '排车状态', width: 110, afterColumn: 'orderNo' },
|
||||
{ key: 'assigned', prop: 'assigned', label: '分配', width: 82, align: 'center', afterColumn: 'orderNo' },
|
||||
{ key: 'inboundCode', prop: 'inboundCode', label: '入仓码', minWidth: 140 },
|
||||
{ key: 'lxCode', prop: 'lxCode', label: 'LX代码', minWidth: 150 },
|
||||
{ key: 'type', prop: 'type', label: '类型', width: 100 },
|
||||
@@ -93,12 +97,8 @@ const columns: DataTableColumn[] = [
|
||||
{ key: 'netWeight', prop: 'netWeight', label: '货物净重(kg)', width: 120, align: 'right' },
|
||||
{ key: 'volume', prop: 'volume', label: '计费体积(m³)', width: 125, align: 'right' },
|
||||
{ key: 'pallets', prop: 'pallets', label: '总托盘数', width: 100, align: 'right' },
|
||||
{ key: 'assigned', prop: 'assigned', label: '分配', width: 82, align: 'center' },
|
||||
{ key: 'mergeAllowed', prop: 'mergeAllowed', label: '合托', width: 96, align: 'center' },
|
||||
{ key: 'urgent', prop: 'urgent', label: '加急', width: 82, align: 'center' },
|
||||
{ key: 'status', prop: 'status', label: '状态', width: 100 },
|
||||
{ key: 'appointmentStatus', prop: 'appointmentStatus', label: 'FBA预约', width: 110 },
|
||||
{ key: 'dispatchStatus', prop: 'dispatchStatus', label: '排车状态', width: 110 },
|
||||
{ key: 'createdAt', prop: 'createdAt', label: '创建时间', minWidth: 170 },
|
||||
]
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ const productFields = ['客户', '海外仓', 'SKU编码', '产品名称', '更
|
||||
const locationRows = computed(() => positions.value.map((item) => ({ id: String(item.id), location: text(item.warehouseLocationCode), status: text(item.status), palletNo: text(item.palletNo), cartons: Number(item.totalPackages || 0), quantity: Number(item.totalPackages || 0), available: Number(item.availablePackages || 0), inboundDate: formatOverseasDateTime(item.updatedAt), ageDays: '—' })))
|
||||
const palletRows = computed(() => positions.value.map((item) => ({ id: String(item.id), palletNo: text(item.palletNo), cartons: Number(item.totalPackages || 0), cartonRange: '—', location: text(item.warehouseLocationCode), quantity: Number(item.totalPackages || 0) })))
|
||||
const ledgerRows = computed(() => ledger.value.map((item) => ({ id: String(item.id), occurredAt: formatOverseasDateTime(item.occurredAt), businessType: inventoryBusinessTypeLabel(item.businessType), businessOrderNo: text(item.businessOrderNo), inboundOrderNo: text(item.inboundOrderNo), outboundOrderNo: text(item.outboundOrderNo), location: text(item.warehouseLocationCode), inboundQuantity: item.deltaPackages > 0 ? item.deltaPackages : 0, outboundQuantity: item.deltaPackages < 0 ? Math.abs(item.deltaPackages) : 0, balance: Number(item.afterPackages || 0), operator: '—', remark: text(item.remark) })))
|
||||
const locationColumns: DataTableColumn[] = [{ key: 'location', prop: 'location', label: '库位', minWidth: 130 }, { key: 'status', prop: 'status', label: '库存状态', width: 120 }, { key: 'palletNo', prop: 'palletNo', label: '托盘号', minWidth: 150 }, { key: 'cartons', prop: 'cartons', label: '箱数', width: 90, align: 'right' }, { key: 'quantity', prop: 'quantity', label: '库存数量', width: 110, align: 'right' }, { key: 'available', prop: 'available', label: '可用数量', width: 110, align: 'right' }, { key: 'inboundDate', prop: 'inboundDate', label: '最近更新', width: 160 }, { key: 'ageDays', prop: 'ageDays', label: '库龄(天)', width: 100, align: 'right' }]
|
||||
const locationColumns: DataTableColumn[] = [{ key: 'location', prop: 'location', label: '库位', minWidth: 130 }, { key: 'palletNo', prop: 'palletNo', label: '托盘号', minWidth: 150 }, { key: 'status', prop: 'status', label: '库存状态', width: 120, afterColumn: 'palletNo' }, { key: 'cartons', prop: 'cartons', label: '箱数', width: 90, align: 'right' }, { key: 'quantity', prop: 'quantity', label: '库存数量', width: 110, align: 'right' }, { key: 'available', prop: 'available', label: '可用数量', width: 110, align: 'right' }, { key: 'inboundDate', prop: 'inboundDate', label: '最近更新', width: 160 }, { key: 'ageDays', prop: 'ageDays', label: '库龄(天)', width: 100, align: 'right' }]
|
||||
const palletColumns: DataTableColumn[] = [{ key: 'palletNo', prop: 'palletNo', label: '托盘号', minWidth: 150 }, { key: 'cartons', prop: 'cartons', label: '关联箱数', width: 110, align: 'right' }, { key: 'cartonRange', prop: 'cartonRange', label: '箱号范围', minWidth: 200 }, { key: 'location', prop: 'location', label: '库位', minWidth: 130 }, { key: 'quantity', prop: 'quantity', label: '库存数量', width: 110, align: 'right' }]
|
||||
const ledgerColumns: DataTableColumn[] = [{ key: 'occurredAt', prop: 'occurredAt', label: '发生时间', minWidth: 160 }, { key: 'businessType', prop: 'businessType', label: '业务类型', width: 120 }, { key: 'businessOrderNo', prop: 'businessOrderNo', label: '业务订单号', minWidth: 170 }, { key: 'inboundOrderNo', prop: 'inboundOrderNo', label: '入库单号', minWidth: 160 }, { key: 'outboundOrderNo', prop: 'outboundOrderNo', label: '出库单号', minWidth: 160 }, { key: 'location', prop: 'location', label: '库位', minWidth: 120 }, { key: 'inboundQuantity', prop: 'inboundQuantity', label: '入库数量', width: 110, align: 'right' }, { key: 'outboundQuantity', prop: 'outboundQuantity', label: '出库数量', width: 110, align: 'right' }, { key: 'balance', prop: 'balance', label: '结存', width: 100, align: 'right' }, { key: 'operator', prop: 'operator', label: '操作人', minWidth: 120 }, { key: 'remark', prop: 'remark', label: '备注', minWidth: 180 }]
|
||||
async function load() { if (!inventoryId.value) { loadError.value = { message: '详情缺少库存记录编号,请从库存列表重新进入' }; return }; loading.value = true; loadError.value = null; try { const inboundOrderNo = String(route.query.inboundOrderNo || ''); const inboundCode = String(route.query.inboundCode || ''); const skuCode = String(route.query.skuCode || ''); const overseasWarehouseId = String(route.query.overseasWarehouseId || ''); const result = await listInventoryBalances({ page: 1, size: 200, inboundOrderNo: inboundOrderNo || undefined, inboundCode: inboundCode || undefined, skuCode: skuCode || undefined, overseasWarehouseId: overseasWarehouseId || undefined }); balance.value = result.records.find((item) => String(item.id) === inventoryId.value) || null; if (!balance.value) throw new Error('Inventory balance not found'); const [positionRows, ledgerPage] = await Promise.all([listInventoryPositions(inventoryId.value), listInventoryLedger({ page: 1, size: 200, inboundOrderNo: balance.value.inboundOrderNo || undefined, inboundCode: balance.value.inboundCode || undefined })]); positions.value = positionRows; ledger.value = ledgerPage.records } catch (error) { loadError.value = getApiErrorDetails(error, '库存详情加载失败,请重试') } finally { loading.value = false } }
|
||||
|
||||
@@ -243,9 +243,9 @@ const cargoColumns = computed<DataTableColumn[]>(() => isReturnOrder.value ? [
|
||||
{ key: 'quantity', prop: 'quantity', label: '件数', width: 100, align: 'right' }, { key: 'weight', prop: 'weight', label: '重量(kg)', width: 120, align: 'right' },
|
||||
{ key: 'volume', prop: 'volume', label: '体积(m³)', width: 120, align: 'right' },
|
||||
])
|
||||
const documentColumns: DataTableColumn[] = [
|
||||
{ key: 'status', prop: 'status', label: '状态', minWidth: 130 }, { key: 'quantity', prop: 'quantity', label: '换标数量(箱/件)', minWidth: 150, formatter: (row: { quantity?: number; quantityUnit?: string }) => `${row.quantity ?? '—'}${row.quantityUnit || '件'}` },
|
||||
const documentColumns: DataTableColumn[] = [ { key: 'quantity', prop: 'quantity', label: '换标数量(箱/件)', minWidth: 150, formatter: (row: { quantity?: number; quantityUnit?: string }) => `${row.quantity ?? '—'}${row.quantityUnit || '件'}` },
|
||||
{ key: 'type', prop: 'type', label: '单据类型', minWidth: 150 }, { key: 'number', prop: 'number', label: '单据编号', minWidth: 200 },
|
||||
{ key: 'status', prop: 'status', label: '状态', minWidth: 130, afterColumn: 'number' },
|
||||
{ key: 'createdAt', prop: 'createdAt', label: '生成时间', minWidth: 170 }, { key: 'quote', prop: 'quote', label: '报价', minWidth: 150 },
|
||||
]
|
||||
const logColumns: DataTableColumn[] = [
|
||||
|
||||
@@ -293,6 +293,11 @@ const columns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 330, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'selection', type: 'selection', width: 48, fixed: 'left' },
|
||||
{ key: 'orderNo', prop: 'orderNo', label: '尾程单号', minWidth: 170, slotName: 'orderNo' },
|
||||
{ key: 'dispatchStatus', prop: 'dispatchStatus', label: '排车状态', width: 110, afterColumn: 'orderNo' },
|
||||
{ key: 'relabelStatus', slotName: 'relabelStatus', label: '换标子状态', minWidth: 180, afterColumn: 'orderNo' },
|
||||
{ key: 'status', prop: 'status', label: '当前状态', width: 110, afterColumn: 'orderNo' },
|
||||
{ key: 'abnormal', prop: 'abnormal', label: '是否异常', width: 100, afterColumn: 'orderNo' },
|
||||
{ key: 'held', prop: 'held', label: '是否暂扣', width: 100, afterColumn: 'orderNo' },
|
||||
{ key: 'businessSource', prop: 'businessSource', label: '业务来源', minWidth: 220 },
|
||||
{ key: 'businessOrderNo', prop: 'businessOrderNo', label: '关联商业单号', minWidth: 230, slotName: 'businessOrderNo' },
|
||||
{ key: 'externalBookingNo', prop: 'externalBookingNo', label: '外接预订号', minWidth: 220 },
|
||||
@@ -308,8 +313,6 @@ const columns: DataTableColumn[] = [
|
||||
{ key: 'transferWarehouse', prop: 'transferWarehouse', label: '转仓', minWidth: 140 },
|
||||
{ key: 'fbaWarehouse', prop: 'fbaWarehouse', label: 'FBA仓库', minWidth: 130 },
|
||||
{ key: 'containerNo', prop: 'containerNo', label: '集装箱号', minWidth: 150 },
|
||||
{ key: 'abnormal', prop: 'abnormal', label: '是否异常', width: 100 },
|
||||
{ key: 'held', prop: 'held', label: '是否暂扣', width: 100 },
|
||||
{ key: 'pallets', prop: 'pallets', label: '托数(退货实收)', width: 130, align: 'right', slotName: 'receiptPallets' },
|
||||
{ key: 'cartons', prop: 'cartons', label: '箱数(退货实收)', width: 130, align: 'right', slotName: 'receiptBoxes' },
|
||||
{ key: 'returnParcels', label: '退货实收包裹', width: 120, align: 'right', slotName: 'returnParcels' },
|
||||
@@ -326,12 +329,9 @@ const columns: DataTableColumn[] = [
|
||||
{ key: 'portAta', prop: 'portAta', label: '实际到港时间', minWidth: 150 },
|
||||
{ key: 'warehouseEta', prop: 'warehouseEta', label: '预计到仓', minWidth: 140 },
|
||||
{ key: 'inboundOrderNo', prop: 'inboundOrderNo', label: '入库单号', minWidth: 150 },
|
||||
{ key: 'dispatchStatus', prop: 'dispatchStatus', label: '排车状态', width: 110 },
|
||||
{ key: 'outboundOrderNo', prop: 'outboundOrderNo', label: '出库单号', minWidth: 150 },
|
||||
{ key: 'dispatchOrderNo', prop: 'dispatchOrderNo', label: '派送单号', minWidth: 150 },
|
||||
{ key: 'relabelOrderNo', slotName: 'relabelOrderNo', label: '换标单号', minWidth: 220 },
|
||||
{ key: 'relabelStatus', slotName: 'relabelStatus', label: '换标子状态', minWidth: 180 },
|
||||
{ key: 'status', prop: 'status', label: '当前状态', width: 110 },
|
||||
{ key: 'createdAt', prop: 'createdAt', label: '创建时间', minWidth: 170 },
|
||||
{ key: 'internalRemark', prop: 'internalRemark', label: '备注(仅自己)', minWidth: 160 },
|
||||
{ key: 'customerRemark', prop: 'customerRemark', label: '备注(客户)', minWidth: 160 },
|
||||
@@ -339,10 +339,10 @@ const columns: DataTableColumn[] = [
|
||||
]
|
||||
const containerDetailColumns: DataTableColumn[] = [
|
||||
{ key: 'orderNo', prop: 'orderNo', label: '尾程单号', minWidth: 170 },
|
||||
{ key: 'orderStatus', prop: 'orderStatus', label: '订单状态', width: 130, slotName: 'orderStatus', afterColumn: 'orderNo' },
|
||||
{ key: 'planStatus', prop: 'planStatus', label: '计划状态', width: 130, slotName: 'planStatus', afterColumn: 'orderNo' },
|
||||
{ key: 'inboundCode', prop: 'inboundCode', label: '入仓码', minWidth: 150 },
|
||||
{ key: 'orderStatus', prop: 'orderStatus', label: '订单状态', width: 130, slotName: 'orderStatus' },
|
||||
{ key: 'planNo', prop: 'planNo', label: '独立入库计划', minWidth: 170 },
|
||||
{ key: 'planStatus', prop: 'planStatus', label: '计划状态', width: 130, slotName: 'planStatus' },
|
||||
{ key: 'plannedArrivalAt', prop: 'plannedArrivalAt', label: '计划到仓时间', minWidth: 170, slotName: 'plannedArrivalAt' },
|
||||
]
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ const queryFields = computed<QueryField[]>(() => [
|
||||
{ key: 'locationType', type: 'select', label: '类型', width: 150, options: [{ label: '普通库位', value: 'NORMAL' }, { label: 'A0共享溢出位', value: 'SHARED_OVERFLOW' }, { label: '虚拟收货位', value: 'VIRTUAL' }] },
|
||||
{ key: 'status', type: 'select', label: '状态', width: 140, options: [{ label: '可用', value: 'ACTIVE' }, { label: '空闲', value: 'IDLE' }, { label: '已占用', value: 'OCCUPIED' }, { label: '已停用', value: 'INACTIVE' }] },
|
||||
])
|
||||
const columns: DataTableColumn[] = [{ key: 'actions', label: '操作', width: 220, fixed: 'left', slotName: 'actions' }, { key: 'locationCode', prop: 'locationCode', label: '库位编码', minWidth: 180 }, { key: 'warehouse', label: '海外仓', minWidth: 190, slotName: 'warehouse' }, { key: 'locationType', label: '类型', width: 130, slotName: 'locationType' }, { key: 'status', label: '状态', width: 110, slotName: 'status' }, { key: 'occupiedPalletCount', prop: 'occupiedPalletCount', label: '托盘数', width: 100, align: 'right' }, { key: 'remark', prop: 'remark', label: '备注', minWidth: 180 }]
|
||||
const columns: DataTableColumn[] = [{ key: 'actions', label: '操作', width: 220, fixed: 'left', slotName: 'actions' }, { key: 'locationCode', prop: 'locationCode', label: '库位编码', minWidth: 180 }, { key: 'status', label: '状态', width: 110, slotName: 'status', afterColumn: 'locationCode' }, { key: 'warehouse', label: '海外仓', minWidth: 190, slotName: 'warehouse' }, { key: 'locationType', label: '类型', width: 130, slotName: 'locationType' }, { key: 'occupiedPalletCount', prop: 'occupiedPalletCount', label: '托盘数', width: 100, align: 'right' }, { key: 'remark', prop: 'remark', label: '备注', minWidth: 180 }]
|
||||
const typeLabels: Record<OverseasLocationType, string> = { VIRTUAL: '虚拟收货位', NORMAL: '普通库位', SHARED_OVERFLOW: 'A0共享溢出位' }; const statusLabels: Record<OverseasLocationStatus, string> = { ACTIVE: '可用', IDLE: '空闲', OCCUPIED: '已占用', INACTIVE: '已停用', DISABLED: '已停用' }
|
||||
const selectedWarehouse = computed(() => warehouses.value.find(item => String(item.id) === String(form.warehouseId)))
|
||||
const warehouseNameById = computed(() => new Map(warehouses.value.map(item => [String(item.id), item.warehouseName])))
|
||||
|
||||
@@ -46,12 +46,12 @@ const editForm = reactive({ skuCode: '', lengthCm: 0, widthCm: 0, heightCm: 0, w
|
||||
const columns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 190, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'sourceCode', label: '上传标识', minWidth: 180, showOverflowTooltip: true, slotName: 'sourceCode' },
|
||||
{ key: 'syncStatus', label: '同步状态', width: 120, slotName: 'syncStatus', afterColumn: 'sourceCode' },
|
||||
{ key: 'kind', label: '类型', width: 110, slotName: 'kind' },
|
||||
{ key: 'inboundCode', prop: 'inboundCode', label: '命中入仓码', minWidth: 150, showOverflowTooltip: true },
|
||||
{ key: 'image', label: '测量图片', width: 110, slotName: 'image' },
|
||||
{ key: 'dimensions', label: '长 × 宽 × 高(cm)', width: 170, slotName: 'dimensions' },
|
||||
{ key: 'weightKg', prop: 'weightKg', label: '重量(kg)', width: 110 },
|
||||
{ key: 'syncStatus', label: '同步状态', width: 120, slotName: 'syncStatus' },
|
||||
{ key: 'match', label: '领星 SKU', minWidth: 150, slotName: 'match' },
|
||||
{ key: 'measuredAt', label: '测量时间', width: 170, slotName: 'measuredAt' },
|
||||
{ key: 'failure', label: '失败原因', minWidth: 220, slotName: 'failure' },
|
||||
@@ -246,8 +246,8 @@ onMounted(() => { void Promise.all([loadConfig(), load()]) })
|
||||
<div class="connection-result__sku">查询 SKU:<strong>{{ testResult.skuCode }}</strong></div>
|
||||
<el-table :data="testResult.warehouses" border>
|
||||
<el-table-column prop="whCode" label="仓库" width="90" />
|
||||
<el-table-column label="结果" width="110"><template #default="{ row }"><el-tag :type="row.matched ? 'success' : 'danger'">{{ row.matched ? '已命中' : '未命中' }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="维护状态" width="110"><template #default="{ row }">{{ row.matched ? (row.maintained ? '已维护' : '待维护') : '—' }}</template></el-table-column>
|
||||
<el-table-column label="结果" width="110"><template #default="{ row }"><el-tag :type="row.matched ? 'success' : 'danger'">{{ row.matched ? '已命中' : '未命中' }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="领星数据" min-width="220"><template #default="{ row }">{{ row.matched ? `${row.length ?? 0} × ${row.width ?? 0} × ${row.height ?? 0} cm / ${row.weight ?? 0} kg` : row.message }}</template></el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
@@ -27,6 +27,7 @@ const warehouseName = ref('')
|
||||
const statusLabels: Record<string, string> = { WAIT_PICK: '待拣货', WAIT_OUTBOUND: '待出库', PICKING: '拣货中', PICKED: '已拣货', OUTBOUNDED: '已出库', CANCELLED: '已取消' }
|
||||
const palletColumns: DataTableColumn[] = [
|
||||
{ key: 'palletNo', label: '托盘码', width: 220 },
|
||||
{ key: 'status', label: '状态', width: 110, afterColumn: 'palletNo' },
|
||||
{ key: 'inboundCode', label: '入仓码', width: 190 },
|
||||
{ key: 'destinationWarehouseCode', label: '目的仓编码', width: 150 },
|
||||
{ key: 'destinationWarehouseName', label: '目的仓名称', minWidth: 180 },
|
||||
@@ -37,7 +38,6 @@ const palletColumns: DataTableColumn[] = [
|
||||
{ key: 'volumeWeightKg', label: '材积重(kg)', width: 130, align: 'right' },
|
||||
{ key: 'volumeCbm', label: '体积(m³)', width: 120, align: 'right' },
|
||||
{ key: 'pickedPackageCount', label: '已拣箱数', width: 110, align: 'right' },
|
||||
{ key: 'status', label: '状态', width: 110 },
|
||||
]
|
||||
function show(value: unknown) { const text = String(value ?? '').trim(); return text || '—' }
|
||||
// A warehouse ID is only a join key; never render it as a warehouse name.
|
||||
|
||||
@@ -103,7 +103,7 @@ const allPageSelected = computed(() => pagedDropshipCandidates.value.length > 0
|
||||
const somePageSelected = computed(() => selectedPageCount.value > 0 && !allPageSelected.value)
|
||||
const selectedDropshipCustomerCount = computed(() => new Set(dropshipCandidates.value.filter(item => selectedDropshipKeys.value.has(dropshipCandidateKey(item))).map(item => item.customerId).filter(Boolean)).size)
|
||||
const oneClickMarks = computed(() => typeof cartonMarks.value === 'string' ? cartonMarks.value.split(/[,\n,]+/).map(value => value.trim()).filter(Boolean) : [])
|
||||
const columns: DataTableColumn[] = [{ key: 'actions', label: '操作', width: 180, fixed: 'left', slotName: 'actions' }, { key: 'outboundNo', prop: 'outboundNo', label: '出库单号', minWidth: 180 }, { key: 'outboundType', prop: 'outboundType', label: '出库类型', width: 150, slotName: 'outboundType' }, { key: 'lxCodes', prop: 'lxCodes', label: 'LX代码', minWidth: 180 }, { key: 'dispatchNo', prop: 'dispatchNo', label: '关联排车单', minWidth: 160 }, { key: 'status', prop: 'status', label: '状态', width: 130, slotName: 'status' }, { key: 'palletCount', prop: 'palletCount', label: '托盘数', width: 100 }, { key: 'packageCount', prop: 'packageCount', label: '箱数', width: 100 }, { key: 'grossWeightKg', prop: 'grossWeightKg', label: '毛重(kg)', width: 120, align: 'right' }, { key: 'volumeWeightKg', prop: 'volumeWeightKg', label: '材积重(kg)', width: 130, align: 'right' }, { key: 'volumeCbm', prop: 'volumeCbm', label: '体积(m³)', width: 120, align: 'right' }, { key: 'createdAt', prop: 'createdAt', label: '创建时间', minWidth: 180 }]
|
||||
const columns: DataTableColumn[] = [{ key: 'actions', label: '操作', width: 180, fixed: 'left', slotName: 'actions' }, { key: 'outboundNo', prop: 'outboundNo', label: '出库单号', minWidth: 180 }, { key: 'status', prop: 'status', label: '状态', width: 130, slotName: 'status', afterColumn: 'outboundNo' }, { key: 'outboundType', prop: 'outboundType', label: '出库类型', width: 150, slotName: 'outboundType' }, { key: 'lxCodes', prop: 'lxCodes', label: 'LX代码', minWidth: 180 }, { key: 'dispatchNo', prop: 'dispatchNo', label: '关联排车单', minWidth: 160 }, { key: 'palletCount', prop: 'palletCount', label: '托盘数', width: 100 }, { key: 'packageCount', prop: 'packageCount', label: '箱数', width: 100 }, { key: 'grossWeightKg', prop: 'grossWeightKg', label: '毛重(kg)', width: 120, align: 'right' }, { key: 'volumeWeightKg', prop: 'volumeWeightKg', label: '材积重(kg)', width: 130, align: 'right' }, { key: 'volumeCbm', prop: 'volumeCbm', label: '体积(m³)', width: 120, align: 'right' }, { key: 'createdAt', prop: 'createdAt', label: '创建时间', minWidth: 180 }]
|
||||
const fields: QueryField[] = [{ key: 'documentNo', type: 'input', label: '出库单号', placeholder: '请输入出库单号', width: 220 }, { key: 'lxCode', type: 'input', label: 'LX代码', placeholder: '请输入领星入库单代码', width: 220 }, { key: 'warehouseId', type: 'input', label: '目的仓', placeholder: '请输入目的仓', width: 220 }, { key: 'status', type: 'select', label: '状态', placeholder: '全部状态', width: 160, options: [{ label: '全部', value: '' }, { label: '待拣货', value: 'WAIT_PICK' }, { label: '拣货中', value: 'PICKING' }, { label: '已拣货', value: 'PICKED' }, { label: '待出库', value: 'WAIT_OUTBOUND' }, { label: '已出库', value: 'OUTBOUNDED' }, { label: '已取消', value: 'CANCELLED' }] }, { key: 'outboundType', type: 'select', label: '出库类型', placeholder: '全部出库类型', width: 170, options: [{ label: '全部', value: '' }, { label: '仓储转运出库', value: 'WAREHOUSE_TRANSFER_OUTBOUND' }, { label: '一件代发出库', value: 'ONE_CLICK_DROPSHIP' }, { label: '退货出库', value: 'RETURN_OUTBOUND' }, { label: '派送出库', value: 'DISPATCH_DELIVERY' }, { label: '废弃出库', value: 'DISCARD_OUTBOUND' }, { label: '贴标出库', value: 'RELABEL_OUTBOUND' }] }]
|
||||
|
||||
async function load(model: QueryModel = filters.value) { Object.assign(filters.value, model); loading.value = true; error.value = null; try { const result = await listOutbounds({ page: page.value, size: pageSize.value, documentNo: String(filters.value.documentNo || '').trim() || undefined, lxCode: String(filters.value.lxCode || '').trim() || undefined, warehouseId: String(filters.value.warehouseId || '').trim() || undefined, status: String(filters.value.status || '').trim() || undefined, outboundType: String(filters.value.outboundType || '').trim() || undefined }); rows.value = result.records.map((row) => ({ ...row, createdAt: formatOverseasDateTime(row.createdAt) })); total.value = result.total } catch (cause) { error.value = getApiErrorDetails(cause, '出库单列表加载失败') } finally { loading.value = false } }
|
||||
|
||||
@@ -45,10 +45,10 @@ const fields = computed<QueryField[]>(() => [
|
||||
])
|
||||
const columns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 180, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'code', prop: 'code', label: '托盘编码', minWidth: 160 }, { key: 'type', prop: 'type', label: '托盘类型', minWidth: 130 },
|
||||
{ key: 'code', prop: 'code', label: '托盘编码', minWidth: 160 },
|
||||
{ key: 'status', prop: 'status', label: '状态', width: 100, afterColumn: 'code' }, { key: 'type', prop: 'type', label: '托盘类型', minWidth: 130 },
|
||||
{ key: 'warehouse', prop: 'warehouse', label: '仓库', minWidth: 180 }, { key: 'volume', prop: 'volume', label: '容积(m³)', width: 110, align: 'right' },
|
||||
{ key: 'purchasePrice', prop: 'purchasePrice', label: '采购价格', width: 120, align: 'right' }, { key: 'fbaWarehouse', prop: 'fbaWarehouse', label: '所属FBA仓', minWidth: 140 },
|
||||
{ key: 'status', prop: 'status', label: '状态', width: 100 }, { key: 'remark', prop: 'remark', label: '备注', minWidth: 180 },
|
||||
{ key: 'purchasePrice', prop: 'purchasePrice', label: '采购价格', width: 120, align: 'right' }, { key: 'fbaWarehouse', prop: 'fbaWarehouse', label: '所属FBA仓', minWidth: 140 }, { key: 'remark', prop: 'remark', label: '备注', minWidth: 180 },
|
||||
]
|
||||
const dialogOpen = ref(false)
|
||||
const dialogMode = ref<'create' | 'edit'>('create')
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user