This commit is contained in:
2026-09-23 17:47:56 +08:00
parent 06e0598e39
commit 5bfe284e22
3 changed files with 67 additions and 41 deletions
+2 -2
View File
@@ -39,7 +39,7 @@
## 模块快速查询
退货预报与签收结果:`src/modules/overseas-warehouse/views/ReturnOrdersView.vue`、`api/returnOrders.ts`;菜单仍名为“退货换标”,正式路由 `return-relabel` 已使用独立页面,不再由 `WarehouseServiceView.vue` 承载。客户退货库存/箱号共用 `views/ReturnInventoryView.vue`、`api/returnInventory.ts`,`return-cartons` 传 `cartonOnly=true`。Backend 为 Lastleg `returnreceipt/`,退货单签收完成不等于换标或退货出库完成;后续换标、出库须按各自业务状态核对。
退货预报与签收结果:`src/modules/overseas-warehouse/views/ReturnOrdersView.vue`、`api/returnOrders.ts`;菜单仍名为“退货换标”。列表路由 `return-relabel`、独立新建路由 `return-relabel/create` 与详情路由均复用该页面,并分别由 `createPage/detailPage` 明确模式;新建页直接填写海外仓、联系人、客户参考号及 SKU 预报明细,取消或提交后返回列表,不再由 `WarehouseServiceView.vue` 或列表内弹框承载。客户退货库存/箱号共用 `views/ReturnInventoryView.vue`、`api/returnInventory.ts`,`return-cartons` 传 `cartonOnly=true`。Backend 为 Lastleg `returnreceipt/`,退货单签收完成不等于换标或退货出库完成;后续换标、出库须按各自业务状态核对。
先从 `src/modules/registry.ts` 确认模块,再按目标模块的 `routes.ts → views/ → api/、store/、contracts.ts 或 public.ts` 查询。页面是否正式开放以 `routes.ts` 的 `release` 为准,不从任务或进度文档推断。
@@ -72,7 +72,7 @@
## UI 与交互
- 修改已有 Client 原型页面前先读 `../../docs/Admin-Client前端原型还原验收规则.md`,按页面和操作形成原型核对矩阵;本节维护 Client 的实现约束,不复制跨前端验收文档全文。
- 视觉还原、布局/交互承载方式变更或真实页面验收时读取 `../../docs/Admin-Client前端原型还原验收规则.md`,只为受影响操作形成原型核对矩阵;纯文案、逻辑或测试修改不自动读取。自动检查与人工视觉验收分开报告,无视觉变化时标记“无需视觉验收”。
- 已向客户讲解或确认过的产品原型是页面视觉与布局实现的直接事实来源。字段顺序、字段分组、字段摆放、列宽、控件宽高、对齐方式、换行方式、区块层级、上下左右边距、内边距、间隙、留白、圆角、边框、阴影、背景色、文字颜色和字号均必须按原型还原,不得只参考大致风格后自行重新排版。
- 原型中的按钮位置、顺序、尺寸、圆角、边框、背景色、文字色、图标、悬停态、激活态、禁用态和危险语义必须直接按原型实现;不得擅自改为文字链接、不同颜色、不同按钮类型、折叠菜单或自创交互效果,也不得增加原型没有的上浮、缩放、渐变线、装饰图标或其他视觉元素。
- 原型已经给出明确图标、字符图标、图片资源或按钮内图标时,直接使用原型中的同一方案和资源,不得以 Lucide、Element Plus 默认图标、手绘图标或个人偏好替换。仅当原型没有定义对应图标时,才按本节的默认图标语义补齐。
@@ -25,14 +25,13 @@ test('supports a standalone dialog and keeps the unpushed billing-weight state v
})
test('shows the published merchant inbound, measurement details and customer confirmation entry', () => {
for (const label of ['入仓码', '平台收货仓', '实际毛重', '实测体积', '托盘与库位', '换算与精度', '最终计费量', '对客单价', '基础费用', '附加费用', '费用合计', '平台备注']) {
assert.match(source, new RegExp(label))
for (const label of ['入仓码', '平台收货仓', '实际毛重', '实测体积', '托盘与库位', '换算与精度', '最终计费量', '单价', '基础费用', '附加费用', '费用合计', '平台备注']) {
assert.ok(source.includes(label), `missing label: ${label}`)
}
assert.match(source, /formatInboundMoney\(props\.data\.latestFee\.totalAmount/)
assert.match(source, /formatConversionPrecision\(/)
assert.match(source, /确认费用/)
assert.match(source, /二次确认费用/)
assert.match(source, /确认后该费用将锁定,平台不能再修改/)
assert.match(source, /确认后当前费用版本将锁定/)
assert.match(source, /customerConfirmationStatus === 'CONFIRMED'/)
assert.match(source, /confirmMerchantInboundFee/)
assert.match(source, /measurementItems/)
@@ -46,6 +45,17 @@ test('shows the published merchant inbound, measurement details and customer con
assert.match(source, /getCalculationRuleLabel\(row\.calculationRuleVersion\)/)
})
test('matches the confirmed billing prototype summary, table, notice and footer', () => {
assert.match(source, /width="min\(1180px, calc\(100vw - 40px\)\)"/)
assert.match(source, /class="fee-dialog-summary" aria-label="计费汇总"/)
for (const label of ['计算重', '单价', '总金额', '箱规长(cm)', '箱规宽(cm)', '箱规高(cm)', '每箱体积', '总体积', '总材积重']) assert.ok(source.includes(label), `missing prototype label: ${label}`)
assert.match(source, /class="fee-confirm-notice"/)
assert.match(source, /请核对计算重、单价和总金额。确认后当前费用版本将锁定。/)
assert.match(source, /确认计费量和费用/)
assert.match(source, /\.fee-dialog-summary \{ display:grid; grid-template-columns:repeat\(3,minmax\(0,1fr\)\)/)
assert.match(source, /\.fee-dialog-table :deep\(\.el-table__body td\.el-table__cell\)/)
})
test('stacks core information for 720 and 360 pixel layouts', () => {
assert.match(source, /@media \(max-width: 720px\)/)
assert.match(source, /\.inbound-overview, \.inbound-measurements, \.fee-calculation-grid \{ grid-template-columns: 1fr;/)
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { ref, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Clock3, PackageCheck, RefreshCw, TriangleAlert } from '@lucide/vue'
import { ElMessage } from 'element-plus'
import { CircleAlert, Clock3, PackageCheck, RefreshCw, TriangleAlert } from '@lucide/vue'
import { confirmMerchantInboundFee } from '@/modules/first-leg/api/inboundBilling'
import type { MerchantInboundBilling } from '@/modules/first-leg/types/inboundBilling'
import {
@@ -55,15 +55,6 @@ async function confirmFee() {
ElMessage.info('当前费用已经确认,无需重复操作')
return
}
try {
await ElMessageBox.confirm(
`请再次确认费用单 ${fee.feeNo},合计 ${formatInboundMoney(fee.totalAmount, fee.currency)}。确认后该费用将锁定,平台不能再修改。`,
'二次确认费用',
{ type: 'warning', confirmButtonText: '确认费用无误', cancelButtonText: '返回核对' },
)
} catch {
return
}
confirming.value = true
try {
await confirmMerchantInboundFee(orderId, fee.feeVersion)
@@ -207,7 +198,7 @@ async function confirmFee() {
</section>
<el-dialog v-model="feeDialogOpen" title="确认测量与计费数据" width="min(980px, 94vw)" append-to-body @closed="emit('closed')">
<el-dialog v-model="feeDialogOpen" class="billing-confirm-dialog" title="确认测量与计费数据" width="min(1180px, calc(100vw - 40px))" append-to-body @closed="emit('closed')">
<div v-if="props.loading" class="inbound-billing-loading" aria-live="polite" aria-busy="true">
<el-skeleton :rows="4" animated />
<span class="sr-only">正在加载计费重</span>
@@ -222,19 +213,12 @@ async function confirmFee() {
<div><strong>{{ props.unavailableMessage }}</strong><span>平台推送计费重后,可在这里核对并确认。</span></div>
</div>
<template v-else>
<dl class="inbound-measurements fee-dialog-summary">
<div><dt>计费总箱数</dt><dd>{{ formatInboundNumber(props.data.measurementSummary.actualPackageCount, 0) }}</dd></div>
<div><dt>计费总毛重</dt><dd>{{ formatInboundMeasure(props.data.measurementSummary.actualGrossWeightKg, 'kg') }}</dd></div>
<div><dt>外箱规总体积</dt><dd>{{ formatInboundMeasure(props.data.measurementSummary.actualVolumeCbm, 'm³') }}</dd></div>
<div><dt>外箱规总材积重</dt><dd>{{ formatInboundMeasure(props.data.measurementSummary.volumeWeightKg, 'kg') }}</dd></div>
<div><dt>计费体积</dt><dd>{{ formatInboundMeasure(props.data.billingBasis.chargeableVolumeCbm, 'm³') }}</dd></div>
<div><dt>计费重量</dt><dd>{{ formatInboundMeasure(props.data.billingBasis.chargeableWeightKg, 'kg') }}</dd></div>
<div><dt>对客单价</dt><dd>{{ formatInboundMoney(props.data.latestFee.unitPrice, props.data.latestFee.currency) }} / {{ props.data.latestFee.chargeUnit }}</dd></div>
<div><dt>基础费用</dt><dd>{{ formatInboundMoney(props.data.latestFee.subtotalAmount, props.data.latestFee.currency) }}</dd></div>
<div><dt>附加费用</dt><dd>{{ formatInboundMoney(props.data.latestFee.additionalAmount, props.data.latestFee.currency) }}</dd></div>
<div><dt>费用合计</dt><dd>{{ formatInboundMoney(props.data.latestFee.totalAmount, props.data.latestFee.currency) }}</dd></div>
<dl class="fee-dialog-summary" aria-label="计费汇总">
<div><dt>计算重</dt><dd>{{ formatInboundMeasure(props.data.billingBasis.chargeableWeightKg, 'kg') }}</dd></div>
<div><dt>单价</dt><dd>{{ formatInboundMoney(props.data.latestFee.unitPrice, props.data.latestFee.currency) }} / {{ props.data.latestFee.chargeUnit.toUpperCase() }}</dd></div>
<div><dt>总金额</dt><dd>{{ formatInboundMoney(props.data.latestFee.totalAmount, props.data.latestFee.currency) }}</dd></div>
</dl>
<el-table v-if="props.data?.measurementItems.length" class="measurement-detail-table" :data="props.data.measurementItems" border size="small">
<el-table class="measurement-detail-table fee-dialog-table" :data="props.data.measurementItems" border empty-text="暂无箱规测量明细">
<el-table-column type="index" label="#" width="54" />
<el-table-column prop="lengthCm" label="箱规长(cm)" min-width="105" />
<el-table-column prop="widthCm" label="箱规宽(cm)" min-width="105" />
@@ -246,15 +230,11 @@ async function confirmFee() {
<el-table-column label="箱规规则" min-width="170"><template #default="{ row }">{{ getCalculationRuleLabel(row.calculationRuleVersion) }}</template></el-table-column>
<el-table-column prop="boxSpecification" label="备注" min-width="130" />
</el-table>
<el-alert
v-if="props.data.latestFee.customerConfirmationStatus === 'CONFIRMED'"
class="fee-confirmed-alert"
:title="`客户已于 ${formatInboundDateTime(props.data.latestFee.customerConfirmedAt)} 确认,费用已锁定,不允许修改。`"
type="success"
:closable="false"
show-icon
/>
<el-alert v-else class="fee-confirmed-alert" title="请核对计费量、对客单价和费用合计。确认后当前费用版本将锁定。" type="warning" :closable="false" show-icon />
<div class="fee-confirm-notice" :class="{ 'is-confirmed': props.data.latestFee.customerConfirmationStatus === 'CONFIRMED' }" role="status">
<CircleAlert :size="21" aria-hidden="true" />
<span v-if="props.data.latestFee.customerConfirmationStatus === 'CONFIRMED'">客户已于 {{ formatInboundDateTime(props.data.latestFee.customerConfirmedAt) }} 确认,费用已锁定,不允许修改。</span>
<span v-else>请核对计算重、单价和总金额。确认后当前费用版本将锁定。</span>
</div>
</template>
<template #footer>
<el-button @click="feeDialogOpen = false">关闭</el-button>
@@ -290,8 +270,25 @@ async function confirmFee() {
.inbound-measurements { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); margin: 10px 0 0; background: #f6f8f8; }
.inbound-measurements > div { min-width: 0; padding: 12px; }
.measurement-detail-table { margin-top: 12px; }
.fee-dialog-summary { margin-top: 0; }
.fee-confirmed-alert { margin-top: 14px; }
.fee-dialog-summary { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:12px; margin:0; }
.fee-dialog-summary > div { position:relative; min-width:0; overflow:hidden; padding:13px 17px; border:1px solid #e4e8f2; border-radius:11px; background:linear-gradient(145deg,#fff,#f8f9fc); box-shadow:0 5px 16px rgba(51,65,85,.04); }
.fee-dialog-summary > div::before { position:absolute; inset:0 auto 0 0; width:4px; background:#a7b4ef; content:''; }
.fee-dialog-summary > div:nth-child(2)::before { background:#7d91eb; }
.fee-dialog-summary > div:nth-child(3) { border-color:#cdd7ff; background:linear-gradient(145deg,#f8faff,#eef2ff); box-shadow:0 8px 22px rgba(82,110,232,.09); }
.fee-dialog-summary > div:nth-child(3)::before { background:#526ee8; }
.fee-dialog-summary dt { margin:0 0 3px; color:#7b8494; font-size:13px; font-weight:500; }
.fee-dialog-summary dd { margin:0; overflow-wrap:anywhere; color:#283246; font-size:20px; font-weight:700; font-variant-numeric:tabular-nums; letter-spacing:0; }
.fee-dialog-summary > div:nth-child(3) dt { color:#6578c5; }
.fee-dialog-summary > div:nth-child(3) dd { color:#405fd8; font-size:22px; }
.fee-dialog-table { margin-top:16px; overflow:hidden; border-radius:10px; box-shadow:0 5px 18px rgba(51,65,85,.04); }
.fee-dialog-table :deep(.el-table__header th.el-table__cell) { height:42px; padding:9px 0; background:linear-gradient(180deg,#f8f9fc,#f3f5f9); color:#697386; font-size:13px; font-weight:650; }
.fee-dialog-table :deep(.el-table__body td.el-table__cell) { height:82px; padding:9px 0; color:#515a6c; font-size:14px; font-variant-numeric:tabular-nums; }
.fee-dialog-table :deep(.el-table__body td:nth-child(7)), .fee-dialog-table :deep(.el-table__body td:nth-child(8)) { color:#34466b; font-weight:650; }
.fee-dialog-table :deep(.cell) { line-height:1.6; }
.fee-confirm-notice { display:flex; align-items:center; gap:10px; margin-top:14px; padding:10px 14px; border:1px solid #f5dfba; border-radius:9px; background:linear-gradient(90deg,#fff9ef,#fffaf2); color:#c6790c; font-size:14px; font-weight:500; }
.fee-confirm-notice > svg { flex:0 0 auto; fill:#eda332; color:#fff; filter:drop-shadow(0 2px 3px rgba(233,154,37,.25)); }
.fee-confirm-notice.is-confirmed { border-color:#b7dfc2; background:#f1faf3; color:#2d8a46; }
.fee-confirm-notice.is-confirmed > svg { fill:#4eae67; }
.pallet-location-list { margin-top: 9px; border-top: 1px solid var(--border); }
.pallet-location-list article { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 11px; padding: 12px 0; border-bottom: 1px solid var(--border); }
.pallet-location-list article > svg { color: var(--primary); }
@@ -315,7 +312,26 @@ async function confirmFee() {
.inbound-public-remark strong { color: var(--navy); font-size: 11px; }
.inbound-public-remark p { margin: 5px 0 0; overflow-wrap: anywhere; color: var(--muted); font-size: 11px; line-height: 1.55; white-space: pre-wrap; }
:global(.billing-confirm-dialog) { max-height:calc(100vh - 40px); overflow:hidden; border-radius:14px; box-shadow:0 28px 90px rgba(17,24,39,.33); }
:global(.billing-confirm-dialog .el-dialog__header) { display:flex; align-items:center; min-height:66px; margin:0; padding:16px 22px 14px; border-bottom:1px solid #edf0f5; }
:global(.billing-confirm-dialog .el-dialog__title) { color:#252b38; font-size:22px; font-weight:700; line-height:1.4; letter-spacing:0; }
:global(.billing-confirm-dialog .el-dialog__headerbtn) { top:14px; right:20px; width:36px; height:36px; border-radius:50%; background:#f5f6f9; }
:global(.billing-confirm-dialog .el-dialog__headerbtn:hover) { background:#e9edfa; }
:global(.billing-confirm-dialog .el-dialog__headerbtn .el-dialog__close) { color:#8b92a0; font-size:22px; }
:global(.billing-confirm-dialog .el-dialog__body) { max-height:calc(100vh - 174px); overflow:auto; padding:18px 22px; }
:global(.billing-confirm-dialog .el-dialog__footer) { padding:14px 22px 16px; border-top:1px solid #edf0f5; }
:global(.billing-confirm-dialog .el-dialog__footer .el-button) { min-height:40px; padding:0 20px; border-radius:8px; font-size:14px; font-weight:650; }
:global(.billing-confirm-dialog .el-dialog__footer .el-button--primary) { min-width:168px; border-color:#526ee8; background:linear-gradient(135deg,#647fec,#526ee8); box-shadow:0 7px 16px rgba(82,110,232,.17); }
@media (max-width: 720px) {
.fee-dialog-summary { grid-template-columns: 1fr; gap: 8px; }
.fee-dialog-summary dd, .fee-dialog-summary > div:nth-child(3) dd { font-size: 18px; }
:global(.billing-confirm-dialog) { width:calc(100vw - 16px) !important; max-height:calc(100vh - 16px); }
:global(.billing-confirm-dialog .el-dialog__header) { padding:14px 16px 11px; }
:global(.billing-confirm-dialog .el-dialog__title) { font-size:19px; }
:global(.billing-confirm-dialog .el-dialog__body) { max-height:calc(100vh - 145px); padding:14px 16px; }
:global(.billing-confirm-dialog .el-dialog__footer) { padding:12px 16px; }
:global(.billing-confirm-dialog .el-dialog__footer .el-button) { min-height:38px; padding:0 14px; font-size:13px; }
.inbound-billing-state { grid-template-columns: auto minmax(0, 1fr); }
.inbound-billing-state .el-button { grid-column: 1 / -1; width: 100%; min-height: 44px; }
.inbound-overview, .inbound-measurements, .fee-calculation-grid { grid-template-columns: 1fr; }