Merge remote-tracking branch 'origin/release/test' into dev
# Conflicts: # AGENTS.md
This commit is contained in:
@@ -46,15 +46,15 @@
|
||||
| 消息中心 | `modules/notifications/` | Backend `iam/.../notification/`;仅顶部消息中心,不进入左侧菜单 |
|
||||
| 订单列表、详情、商品图片与入库费用 | `modules/unified-orders/`、`modules/first-leg-orders/`;平台商品图片由 `modules/unified-orders/api/unifiedOrders.ts` 按客户与 fileId 下载 | `order/`、`firstleg/`、Finance;图片读取归 `file/` 的 `PlatformProductImageController`。Order 状态为 `SUBMITTED` 时统一列表必须保持 `PENDING_REVIEW/APPROVE_ORDER`,不得被已存在的 Firstleg 准备中入库投影覆盖 |
|
||||
| 客户原始申报资料编辑与渠道变更 | `modules/unified-orders/components/OrderDeclarationEditorDialog.vue`、`api/unifiedOrders.ts`、`pages/UnifiedOrdersView.vue` | 使用订单版本和货物 `itemNo` 整体提交,不允许增删货物行;只编辑客户申报快照,不覆盖仓库/PDA 实测、测量审核或计费事实。渠道变化需二次确认并由 Backend `order/` 联查 `firstleg/` 的分柜、同柜、装柜与重新计价门禁;国内收货仓或中转仓变化必须重新下单 |
|
||||
| 入库、测量与计价主数据 | `modules/inbounds/`、`modules/fba-pricing/`、`modules/dimension-config/`、`modules/fee-items/` | `firstleg/`、`order/`、`file/`、`integration-service/` |
|
||||
| 入库、测量、客户箱规策略与计价主数据 | `modules/inbounds/measurement/`、`modules/fba-pricing/`、`modules/dimension-config/`、`modules/customers/components/CustomerBillingRulesPanel.vue`、`modules/fee-items/` | 客户固定四档箱规,默认计算方式为 `ADD/MULTIPLY`;测量行可选实际尺寸、箱规尺寸策略或客户默认尺寸系数,保存后以 Backend `firstleg/` 返回的外箱尺寸、体积重和计费基数为准,不在页面独立重算 |
|
||||
| 财务与柜级公共费用分摊 | `modules/finance/`,继续读该目录 `AGENTS.md`;柜费入口 `modules/loading-lists/components/ContainerFeesPanel.vue`、`api/containerFees.ts` | `finance/`、`firstleg/`、`file/`、`docs/财务管理/`;跨客户公共柜费只按服务端柜内客户/订单事实分摊,金额必须守恒,创建与审核、驳回、调整、作废均整组原子处理 |
|
||||
| 货柜、排柜、预装/装柜清单、真实柜号后补与整柜报关 | `modules/container-loading/`、`modules/loading-lists/`;预装详情 `pages/PreloadPlanDetailView.vue`,货柜详情 `pages/ContainerDetailView.vue`,资料/PDA 证据 `components/ContainerOverviewPanel.vue`,报关 `components/ContainerCustomsPanel.vue`、`api/platformContainerCustoms.ts` | 创建柜次时自动生成唯一预装计划,加入入仓码即同步进入该计划,页面不提供创建计划或推送预装操作。预装详情先调用 `/loading-plans/{planId}/lifecycle`,`COMPLETED` 时替换为正式装柜清单详情并关闭旧工作台标签,不按 A0404 或错误文案猜阶段。订舱关联和预装确认允许真实集装箱号为空;首次补录可在 `EDITING/LOADING` 进行,PDA 最终完成前必须补齐;已有柜号纠正只允许 `EDITING` 且任务未领取、无物理作业事实。整柜报关按 `loadingPlanId` 展示、逐资料完成,全买单柜使用 `complete-all-buy`;后端为 `LoadingListService`、`loading-lists-v1.yaml`、`firstleg/customs/container/`,文件归 `file/` |
|
||||
| 合柜报关 / 同柜不可拆组 | `modules/unified-orders/api/coLoadGroups.ts`、`pages/UnifiedOrdersView.vue`;装柜消费 `modules/container-loading/pages/SmartLoadingWorkbenchView.vue` | 前端只提交订单 ID;Backend `firstleg/loading/coload/` 展开并冻结每票全部可用入仓片段。成员数是入仓片段数,不等于订单票数;只要求同国内仓、同渠道,允许跨客户及不同目的仓港;分配、移除、取消均整组处理 |
|
||||
| 智能装柜按入仓码候选与箱数分配 | `modules/container-loading/pages/SmartLoadingWorkbenchView.vue`、`api/platformContainerLoading.ts` | Backend `firstleg/` 的 `PlatformSmartLoadingController`、`SmartLoadingService`;`GlobalSmartLoadingView.vue` 当前未注册路由,不作为正式入口 |
|
||||
| 三期订舱资料、打印、关联候选与出仓前船期 | `modules/ocean-bookings/` 的 `components/OceanBookingDocumentsPanel.vue`、`OceanBookingPrintDialog.vue`,以及 `modules/container-loading/`、`modules/port-schedules/` | 订舱草稿维护资料,放舱后资料与表单只读;打印只对 `RELEASED` 开放。`platformOceanBookings.ts` 的 `/link-candidates` 只消费服务端分页,`containerCount` 是柜型需求 `quantity` 总和;Backend `firstleg/oceanbooking/`、`file/`、`print/`、`firstleg/portschedule/` |
|
||||
| 订舱级拖车预约 | `modules/trucking/api/platformTrucking.ts`、`components/TruckAppointmentForm.vue`、`pages/TruckAppointmentsView.vue` | 不选择或提交客户、柜次、装柜计划或真实集装箱号;只选择 `RELEASED` 订舱,提单号和仓库由订舱冻结事实带入;订舱须有有效 `BOOKING_CONFIRMATION` 或 `BILL_OF_LADING` 文件,每个订舱最多一条非终态预约。拖车附件为平台作用域且不重复提单文件;Backend `TruckAppointmentService`、`truck-appointments-v1.yaml` |
|
||||
| 全局智能装柜工作台、入仓码候选与箱数分配 | `modules/container-loading/components/SmartLoadingDialog.vue` 内嵌 `pages/GlobalSmartLoadingView.vue`,柜次页 `SmartLoadingWorkbenchView.vue`,以及 `smartLoadingChannelGroups.ts`、`api/platformContainerLoading.ts`;全局页当前不注册独立路由 | Backend `firstleg/` 的 `/inbound-candidates`、`/containers` 和计划分配接口;渠道分组选项与服务端映射必须一致,同柜不可拆组作为单个候选卡片整组分配,前端不得拆成员分页或自行拼组 |
|
||||
| 三期订舱资料、打印、关联候选与出仓前船期 | `modules/ocean-bookings/` 的 `components/OceanBookingDocumentsPanel.vue`、`OceanBookingPrintDialog.vue`,以及 `modules/container-loading/`、`modules/port-schedules/` | 订舱草稿按当前版本维护资料,放舱后资料与表单只读;打印只对 `RELEASED` 开放。`platformOceanBookings.ts` 的 `/link-candidates` 只消费服务端分页,`containerCount` 是柜型需求 `quantity` 总和;Backend `firstleg/oceanbooking/`、`file/`、`print/`、`firstleg/portschedule/` |
|
||||
| 订舱级拖车预约 | `modules/trucking/api/platformTrucking.ts` 的 `/candidates`、`components/TruckAppointmentForm.vue`、`pages/TruckAppointmentCreateView.vue`、`TruckAppointmentsView.vue` | 表单只选择服务端返回的 `RELEASED` 订舱与拖车供应商,不选择或提交客户、柜次、装柜计划或真实集装箱号;提单号和仓库由订舱冻结事实带入。一个订舱最多一张非终态拖车单,重复创建转入现有单继续编辑;PDA 完成不自动推进拖车。附件为平台作用域且不重复提单文件;Backend `TruckAppointmentService`、`truck-appointments-v1.yaml` |
|
||||
| 历史报关协同与国内出仓 | `modules/customs/`、`modules/outbound-dispatch/` | `modules/customs/` 是历史报关案件/清单/代理提交与回执入口;装柜完成后的整柜退税/买单资料从货柜详情进入,不在旧模块复制;Backend `firstleg/outbound/dispatch/`、`firstleg/customs/`、`file/` |
|
||||
| 主数据、下载、审计 | `modules/master-data/`、`modules/downloads/`、`modules/audit/` | `warehouse/`、`file/`、`audit-service/` |
|
||||
| 主数据、下载、退避任务重试、审计 | `modules/master-data/`、`modules/downloads/pages/DownloadCenterView.vue`、`modules/audit/` | File 导出任务返回 `attemptCount/maxAttempts/nextRetryAt`;仅失败或已进入自动退避的等待任务显示重试,等待任务重试用于立即执行当前投递,不创建新任务;联查 `warehouse/`、`file/`、`audit-service/` |
|
||||
| 四期海外接柜、入库与库存 | `modules/overseas-warehouse/` | `gateway-overseas/`、`lastleg-service/`、`warehouse-overseas-service/`;不含出库和派送 |
|
||||
| 四期海外采购、卡车、散板 | `modules/overseas-business/` | `gateway-overseas/`、`lastleg-service/` |
|
||||
| 四期合作仓与通用资料 | 合作仓 `modules/warehouses/`;报价/汇率/车辆 `modules/overseas-master-data/` | 国内 `warehouse/`、海外实际资源与服务 |
|
||||
|
||||
@@ -34,8 +34,8 @@ const config: CustomerDimensionConfig = {
|
||||
rules: boxes.map(([boxType, boxTypeName, innerLengthCm, innerWidthCm, innerHeightCm, remark], index) => ({
|
||||
id: String(100 + index),
|
||||
boxType, boxTypeName, innerLengthCm, innerWidthCm, innerHeightCm, edgeAdditionCm: 2,
|
||||
outerLengthCm: innerLengthCm + 2, outerWidthCm: innerWidthCm + 2, outerHeightCm: innerHeightCm + 2,
|
||||
volumeCbm: Number(((innerLengthCm + 2) * (innerWidthCm + 2) * (innerHeightCm + 2) / 1_000_000).toFixed(3)),
|
||||
outerLengthCm: innerLengthCm + 4, outerWidthCm: innerWidthCm + 4, outerHeightCm: innerHeightCm + 4,
|
||||
volumeCbm: Number(((innerLengthCm + 4) * (innerWidthCm + 4) * (innerHeightCm + 4) / 1_000_000).toFixed(3)),
|
||||
remark, version: 3, createdAt: '2026-08-07T10:00:00', updatedAt: '2026-08-07T10:20:00',
|
||||
})),
|
||||
}
|
||||
@@ -71,11 +71,11 @@ describe('CustomerBillingRulesPanel', () => {
|
||||
await flushPromises()
|
||||
expect(getCustomerDimensionConfig).toHaveBeenCalledWith('8')
|
||||
expect(document.body.textContent).toContain('按箱规尺寸计算')
|
||||
expect(document.body.textContent).toContain('长加 X、宽加 X、高加 X = 外径尺寸')
|
||||
expect(document.body.textContent).toContain('长、宽、高各加 2X = 外径尺寸')
|
||||
expect(document.body.textContent).toContain('小箱')
|
||||
expect(document.body.textContent).toContain('特大箱')
|
||||
expect(document.body.textContent).toContain('32 × 22 × 22')
|
||||
expect(document.body.textContent).toContain('0.015')
|
||||
expect(document.body.textContent).toContain('34 × 24 × 24')
|
||||
expect(document.body.textContent).toContain('0.020')
|
||||
expect(document.body.textContent).toContain('标准中箱')
|
||||
})
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ const loadError = ref<ApiErrorDetails | null>(null)
|
||||
const editorVisible = ref(false)
|
||||
const editorSaving = ref(false)
|
||||
const editorDirty = ref(false)
|
||||
const defaultCalculationMode = ref<'DIMENSION' | 'WEIGHT'>('DIMENSION')
|
||||
const defaultCalculationMode = ref<'ACTUAL' | 'DIMENSION' | 'WEIGHT'>('ACTUAL')
|
||||
const defaultModeSaving = ref(false)
|
||||
const editorForm = ref<FormInstance>()
|
||||
const form = reactive<CustomerDimensionConfigFormModel>({ rules: [] })
|
||||
@@ -71,7 +71,7 @@ const BOX_NAMES: Record<CustomerDimensionBoxType, string> = {
|
||||
const columns: DataTableColumn[] = [
|
||||
{ key: 'boxTypeName', label: '箱规类型', minWidth: 110, slotName: 'boxTypeName' },
|
||||
{ key: 'inner', label: '内径尺寸(长×宽×高 cm)', minWidth: 210, slotName: 'inner' },
|
||||
{ key: 'edgeAdditionCm', label: '尺寸增加值(cm)', width: 150, slotName: 'edgeAdditionCm' },
|
||||
{ key: 'edgeAdditionCm', label: '单边加宽(cm)', width: 150, slotName: 'edgeAdditionCm' },
|
||||
{ key: 'outer', label: '外径尺寸(长×宽×高 cm)', minWidth: 210, slotName: 'outer' },
|
||||
{ key: 'volumeCbm', label: '体积(CBM)', width: 130, slotName: 'volumeCbm' },
|
||||
{ key: 'remark', label: '备注', minWidth: 140, prop: 'remark' },
|
||||
@@ -91,7 +91,7 @@ watch(form, () => {
|
||||
|
||||
watch(() => props.customerId, () => {
|
||||
editorVisible.value = false
|
||||
defaultCalculationMode.value = 'DIMENSION'
|
||||
defaultCalculationMode.value = 'ACTUAL'
|
||||
void loadConfig()
|
||||
})
|
||||
|
||||
@@ -107,7 +107,8 @@ async function loadConfig() {
|
||||
try {
|
||||
config.value = await getCustomerDimensionConfig(props.customerId)
|
||||
config.value.rules = sortRules(config.value.rules)
|
||||
defaultCalculationMode.value = config.value.defaultCalculationMode === 'MULTIPLY' ? 'WEIGHT' : 'DIMENSION'
|
||||
defaultCalculationMode.value = config.value.defaultCalculationMode === 'MULTIPLY' ? 'WEIGHT'
|
||||
: config.value.defaultCalculationMode === 'ADD' ? 'DIMENSION' : 'ACTUAL'
|
||||
} catch (error) {
|
||||
config.value = null
|
||||
loadError.value = getApiErrorDetails(error, '箱规尺寸规则加载失败,请稍后重试')
|
||||
@@ -168,7 +169,7 @@ function validateRules() {
|
||||
return false
|
||||
}
|
||||
if (form.rules.some((rule) => !Number.isFinite(rule.edgeAdditionCm) || rule.edgeAdditionCm < 0.001 || rule.edgeAdditionCm > 100)) {
|
||||
ElMessage.error('尺寸增加值必须在 0.001 至 100cm 之间')
|
||||
ElMessage.error('单边加宽必须在 0.001 至 100cm 之间')
|
||||
return false
|
||||
}
|
||||
if (form.rules.some((rule) => (rule.remark || '').length > 500)) {
|
||||
@@ -207,19 +208,20 @@ function formatDimension(...values: number[]) {
|
||||
return values.map((value) => Number(value.toFixed(2)).toString()).join(' × ')
|
||||
}
|
||||
|
||||
async function selectDefaultCalculationMode(mode: 'DIMENSION' | 'WEIGHT') {
|
||||
if (!canEdit.value || defaultModeSaving.value || mode === defaultCalculationMode.value) return
|
||||
if (mode === 'WEIGHT' && !visibleChargeRules.value.some((rule) => rule.defaultRule)) {
|
||||
async function selectDefaultCalculationMode(mode: 'DIMENSION' | 'WEIGHT', checked = true) {
|
||||
const targetMode = checked ? mode : 'ACTUAL'
|
||||
if (!canEdit.value || defaultModeSaving.value || targetMode === defaultCalculationMode.value) return
|
||||
if (checked && mode === 'WEIGHT' && !visibleChargeRules.value.some((rule) => rule.defaultRule)) {
|
||||
ElMessage.warning('请先保存并启用一套权重系数配置,再将其设为默认规则')
|
||||
return
|
||||
}
|
||||
const previous = defaultCalculationMode.value
|
||||
defaultCalculationMode.value = mode
|
||||
defaultCalculationMode.value = targetMode
|
||||
defaultModeSaving.value = true
|
||||
try {
|
||||
config.value = await updateCustomerDimensionDefaultCalculationMode(
|
||||
props.customerId,
|
||||
mode === 'WEIGHT' ? 'MULTIPLY' : 'ADD',
|
||||
!checked ? 'ACTUAL' : mode === 'WEIGHT' ? 'MULTIPLY' : 'ADD',
|
||||
)
|
||||
config.value.rules = sortRules(config.value.rules)
|
||||
ElMessage.success('默认计算规则已更新')
|
||||
@@ -234,10 +236,11 @@ async function selectDefaultCalculationMode(mode: 'DIMENSION' | 'WEIGHT') {
|
||||
async function saveChargeConfig() {
|
||||
const coefficient = Number(chargeRows[0].coefficient)
|
||||
if (!canChargeEdit.value || chargeRows.some((row) => !(Number(row.coefficient) > 0))) { ElMessage.warning('权重系数必须大于 0'); return }
|
||||
if (chargeRows.some((row) => !(Number(row.weight) > 0))) { ElMessage.warning('每档重量必须大于 0'); return }
|
||||
chargeSaving.value = true
|
||||
try {
|
||||
// A saved version becomes the active coefficient set immediately.
|
||||
const created = await createCustomerChargeRule(props.customerId, { ...chargeForm, ruleCode: `RULE-${Date.now()}`, lengthFactor: coefficient, widthFactor: coefficient, heightFactor: coefficient, defaultRule: false, remark: chargeRows.map((row) => `${row.boxType}:${row.length},${row.width},${row.height};系数:${row.coefficient};${BOX_NAMES[row.boxType]}:${row.remark || '—'}`).join(';') })
|
||||
const created = await createCustomerChargeRule(props.customerId, { ...chargeForm, ruleCode: `RULE-${Date.now()}`, lengthFactor: coefficient, widthFactor: coefficient, heightFactor: coefficient, defaultRule: false, remark: chargeRows.map((row) => `${row.boxType}:${row.length},${row.width},${row.height};系数:${row.coefficient};重量:${row.weight};${BOX_NAMES[row.boxType]}:${row.remark || '—'}`).join(';') })
|
||||
await setCustomerChargeRuleDefault(props.customerId, created.id)
|
||||
await loadChargeRules()
|
||||
editorDirty.value = false
|
||||
@@ -274,6 +277,8 @@ async function loadChargeRules() {
|
||||
if (match) { row.length = Number(match[1]); row.width = Number(match[2]); row.height = Number(match[3]) }
|
||||
const coefficient = remark.match(new RegExp(`${row.boxType}(?::[^;]*)?;系数:(\\d+(?:\\.\\d+)?)`))
|
||||
row.coefficient = coefficient ? Number(coefficient[1]) : fallbackCoefficient
|
||||
const weight = remark.match(new RegExp(`${row.boxType}(?::[^;]*)?;系数:[^;]+;重量:(\\d+(?:\\.\\d+)?)`))
|
||||
if (weight) row.weight = Number(weight[1])
|
||||
const note = remark.match(new RegExp(`${BOX_NAMES[row.boxType]}:([^;]+)`))
|
||||
if (note) row.remark = note[1]
|
||||
})
|
||||
@@ -296,15 +301,15 @@ onMounted(() => { void loadConfig(); void loadChargeRules() })
|
||||
<header class="rule-card-heading">
|
||||
<div class="rule-title-row">
|
||||
<h2>按箱规尺寸计算</h2>
|
||||
<el-checkbox :model-value="defaultCalculationMode === 'DIMENSION'" :disabled="!canEdit || defaultModeSaving" @change="selectDefaultCalculationMode('DIMENSION')">设为默认规则</el-checkbox>
|
||||
<el-checkbox :model-value="defaultCalculationMode === 'DIMENSION'" :disabled="!canEdit || defaultModeSaving" @change="selectDefaultCalculationMode('DIMENSION', Boolean($event))">设为默认规则</el-checkbox>
|
||||
</div>
|
||||
<el-button v-if="canEdit" type="primary" :disabled="loading" @click="openEditor"><Pencil :size="16" />编辑规则</el-button>
|
||||
</header>
|
||||
<div class="formula-band"><span>计算方式:<strong>长加 X、宽加 X、高加 X = 外径尺寸</strong>(X 为尺寸增加值,按实测尺寸匹配最小可容纳箱规)</span><span>体积公式:外径长 × 外径宽 × 外径高 ÷ 1,000,000 = CBM</span><span>超出当前箱规时自动匹配下一档箱规</span></div>
|
||||
<div class="formula-band"><span>计算方式:<strong>长、宽、高各加 2X = 外径尺寸</strong>(X 为单边加宽,按实测尺寸匹配箱规)</span><span>体积公式:外径长 × 外径宽 × 外径高 ÷ 1,000,000 = CBM</span><span>低于最低档使用最低档,超出当前档自动匹配下一档,超出最大档使用最大档</span></div>
|
||||
<DataTable :data="config.rules" :columns="columns" :loading="loading" row-key="boxType" empty-text="当前客户暂无箱规尺寸规则">
|
||||
<template #boxTypeName="{ row }"><strong>{{ row.boxTypeName }}</strong></template>
|
||||
<template #inner="{ row }">{{ formatDimension(row.innerLengthCm, row.innerWidthCm, row.innerHeightCm) }}</template>
|
||||
<template #edgeAdditionCm="{ row }"><el-tag effect="light">+{{ row.edgeAdditionCm }}cm</el-tag></template>
|
||||
<template #edgeAdditionCm="{ row }"><el-tag effect="light">单边 +{{ row.edgeAdditionCm }}cm</el-tag></template>
|
||||
<template #outer="{ row }">{{ formatDimension(row.outerLengthCm, row.outerWidthCm, row.outerHeightCm) }}</template>
|
||||
<template #volumeCbm="{ row }"><strong class="volume-value">{{ Number(row.volumeCbm).toFixed(3) }}</strong></template>
|
||||
<template #empty><div class="rule-state"><Ruler :size="28" /><strong>当前客户暂无箱规尺寸规则</strong></div></template>
|
||||
@@ -313,7 +318,7 @@ onMounted(() => { void loadConfig(); void loadChargeRules() })
|
||||
<div v-else v-loading="loading" class="loading-state" />
|
||||
|
||||
<section v-if="canChargeView" class="rule-card charge-rule-card">
|
||||
<header class="rule-card-heading"><div><div class="rule-title-row"><h2>箱规按权重系数计算</h2><el-checkbox :model-value="defaultCalculationMode === 'WEIGHT'" :disabled="!canEdit || defaultModeSaving" @change="selectDefaultCalculationMode('WEIGHT')">设为默认规则</el-checkbox></div><p class="section-hint">勾选的规则将作为该客户订单“外箱规数据”的默认计算方式,同一时间只能选择一个;权重系数用于调整不同箱型的计费体积:加权体积 = 体积 × 权重系数,体积重 = 加权体积 × 167 kg/CBM。</p></div></header>
|
||||
<header class="rule-card-heading"><div><div class="rule-title-row"><h2>箱规按权重系数计算</h2><el-checkbox :model-value="defaultCalculationMode === 'WEIGHT'" :disabled="!canEdit || defaultModeSaving" @change="selectDefaultCalculationMode('WEIGHT', Boolean($event))">设为默认规则</el-checkbox></div><p class="section-hint">勾选后客户下单数据才会按该规则调整;未勾选时默认使用实测原尺寸(不调整)。</p></div></header>
|
||||
<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>
|
||||
@@ -328,10 +333,10 @@ onMounted(() => { void loadConfig(); void loadChargeRules() })
|
||||
|
||||
<AppDialog v-model="editorVisible" title="编辑箱规尺寸 / 计算规则" width="min(1200px, calc(100vw - 28px))" :confirm-loading="editorSaving" :show-cancel="false" :before-close="beforeEditorClose" @confirm="saveConfig">
|
||||
<div class="editor-content">
|
||||
<p class="editor-formula">计算公式:按长、宽、高匹配最小可容纳箱规;长加 X、宽加 X、高加 X(X 为尺寸增加值),体积(CBM) = 外径长 × 外径宽 × 外径高 ÷ 1,000,000</p>
|
||||
<p class="editor-formula">计算公式:按长、宽、高匹配箱规;长、宽、高各加 2X(X 为单边加宽),体积(CBM) = 外径长 × 外径宽 × 外径高 ÷ 1,000,000</p>
|
||||
<el-form ref="editorForm" :model="form" label-position="top">
|
||||
<div class="editor-table-wrap">
|
||||
<table class="editor-table"><thead><tr><th>箱规类型</th><th>内径长(cm)</th><th>内径宽(cm)</th><th>内径高(cm)</th><th>尺寸增加值(cm)</th><th>备注</th></tr></thead><tbody>
|
||||
<table class="editor-table"><thead><tr><th>箱规类型</th><th>内径长(cm)</th><th>内径宽(cm)</th><th>内径高(cm)</th><th>单边加宽(cm)</th><th>备注</th></tr></thead><tbody>
|
||||
<tr v-for="row in form.rules" :key="row.boxType"><th scope="row">{{ row.boxTypeName }}</th><td><el-input-number v-model="row.innerLengthCm" :min="0.001" :precision="3" :controls="false" /></td><td><el-input-number v-model="row.innerWidthCm" :min="0.001" :precision="3" :controls="false" /></td><td><el-input-number v-model="row.innerHeightCm" :min="0.001" :precision="3" :controls="false" /></td><td><el-input-number v-model="row.edgeAdditionCm" :min="0.001" :max="100" :precision="3" :controls="false" /></td><td><el-input v-model="row.remark" maxlength="500" /></td></tr>
|
||||
</tbody></table>
|
||||
</div>
|
||||
|
||||
@@ -24,7 +24,8 @@ function normalizeRule(rule: RawRule): CustomerDimensionRule {
|
||||
function normalizeConfig(config: RawConfig): CustomerDimensionConfig {
|
||||
return {
|
||||
tenantId: String(config.tenantId),
|
||||
defaultCalculationMode: config.defaultCalculationMode === 'MULTIPLY' ? 'MULTIPLY' : 'ADD',
|
||||
defaultCalculationMode: config.defaultCalculationMode === 'MULTIPLY' ? 'MULTIPLY'
|
||||
: config.defaultCalculationMode === 'ACTUAL' ? 'ACTUAL' : 'ADD',
|
||||
rules: config.rules.map(normalizeRule),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export type CustomerDimensionBoxType = 'SMALL' | 'MEDIUM' | 'LARGE' | 'EXTRA_LARGE'
|
||||
export type CustomerDimensionCalculationMode = 'ADD' | 'MULTIPLY'
|
||||
export type CustomerDimensionCalculationMode = 'ACTUAL' | 'ADD' | 'MULTIPLY'
|
||||
|
||||
export interface CustomerDimensionRule {
|
||||
id: string
|
||||
|
||||
@@ -43,6 +43,8 @@ const snapshot: InboundFeeSnapshot = {
|
||||
rateEffectiveFrom: '2026-08-01T00:00:00', rateEffectiveTo: null,
|
||||
calculatedAt: '2026-08-04T10:00:00', confirmedAt: '2026-08-04T10:01:00', sentAt: '2026-08-04T10:02:00',
|
||||
nextProcessStatus: 'WAIT_SPLIT',
|
||||
customerConfirmationStatus: 'PENDING_CONFIRMATION', customerConfirmedBy: null,
|
||||
customerConfirmedByName: null, customerConfirmedAt: null,
|
||||
}
|
||||
|
||||
enableAutoUnmount(afterEach)
|
||||
|
||||
@@ -8,6 +8,8 @@ export interface InboundFeeSnapshot {
|
||||
feeStatus: InboundFeeStatus
|
||||
inboundId: string
|
||||
inboundCode: string | null
|
||||
measurementId?: string | null
|
||||
measurementSource?: string | null
|
||||
chargeRuleId: string
|
||||
ruleCode: string
|
||||
chargeRuleVersion: number
|
||||
@@ -44,6 +46,10 @@ export interface InboundFeeSnapshot {
|
||||
confirmedAt: string | null
|
||||
sentAt: string | null
|
||||
nextProcessStatus: string | null
|
||||
customerConfirmationStatus: 'PENDING_CONFIRMATION' | 'CONFIRMED'
|
||||
customerConfirmedBy: string | null
|
||||
customerConfirmedByName: string | null
|
||||
customerConfirmedAt: string | null
|
||||
}
|
||||
|
||||
export interface AdjustPublishedInboundFeeRequest {
|
||||
|
||||
@@ -10,7 +10,6 @@ import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
||||
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||
import { inboundProcessStatusLabels, inboundStatusLabel } from '../status'
|
||||
import { createManualCartonScan, downloadInboundMeasurementImage, getInboundMeasurement, saveInboundMeasurementDraft, submitInboundMeasurement, updateConfirmedPackageCount } from './api'
|
||||
import { listCustomerChargeRules, type CustomerChargeRule } from '@/modules/fba-pricing'
|
||||
import type {
|
||||
InboundMeasurementDetail,
|
||||
InboundMeasurementItem,
|
||||
@@ -21,7 +20,6 @@ const props = defineProps<{
|
||||
customerId?: string
|
||||
tenantId?: string
|
||||
inboundId: string
|
||||
openWarehouseSuggestion?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -62,15 +60,10 @@ const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const submitting = ref(false)
|
||||
const manualEntryOpen = ref(false)
|
||||
const warehouseSuggestionOpen = ref(false)
|
||||
const warehouseSuggestionAutoOpened = ref(false)
|
||||
const warehouseSuggestion = reactive({ itemId: '', packageCount: 1, remark: '', coefficientMode: 'ADD' as 'ADD' | 'MULTIPLY' })
|
||||
const manualEntrySubmitting = ref(false)
|
||||
const manualEntryImage = ref<File | null>(null)
|
||||
const manualEntryErrors = ref<string[]>([])
|
||||
const detail = ref<InboundMeasurementDetail | null>(null)
|
||||
const chargeRules = ref<CustomerChargeRule[]>([])
|
||||
const chargeRulesLoading = ref(false)
|
||||
const loadError = ref<ApiErrorDetails | null>(null)
|
||||
const operationError = ref<ApiErrorDetails | null>(null)
|
||||
const conflictDetected = ref(false)
|
||||
@@ -105,7 +98,6 @@ const canView = computed(() => access.can('platform:firstleg:inbound:measure:vie
|
||||
const canEdit = computed(() => access.can('platform:firstleg:inbound:measure:edit'))
|
||||
const canManualEntry = computed(() => access.can('platform:firstleg:inbound:measure:manual-entry'))
|
||||
const canSubmit = computed(() => access.can('platform:firstleg:inbound:measure:submit'))
|
||||
const canWarehouseSuggestion = computed(() => access.can('platform:firstleg:inbound:warehouse-suggestion:edit'))
|
||||
const customerId = computed(() => props.customerId || props.tenantId || '')
|
||||
const hasContext = computed(() => Boolean(customerId.value && props.inboundId))
|
||||
const countEditableProcess = computed(() => ['WAIT_MEASURE', 'MEASURING', 'MEASURE_PENDING', 'MEASURE_REJECTED'].includes(detail.value?.processStatus || ''))
|
||||
@@ -123,9 +115,6 @@ const isDirty = computed(() => Boolean(detail.value) && snapshot.value !== basel
|
||||
const registrationOpen = computed(() => hasContext.value && canView.value && Boolean(detail.value))
|
||||
const requiredPermission = computed(() => canEdit.value ? 'platform:firstleg:inbound:measure:edit' : null)
|
||||
const sourceLabel = computed(() => ({ THIRD_PARTY: '第三方设备', MANUAL: '平台人工', OPENAPI: '第三方设备' }[detail.value?.sourceType || ''] || (detail.value?.sourceType ? '未知来源' : '尚未测量')))
|
||||
const suggestionItem = computed(() => form.items.find((item) => item.itemId === warehouseSuggestion.itemId) || null)
|
||||
const suggestionTotalVolume = computed(() => Number(suggestionItem.value?.serverValues.unitVolumeCbm || 0) * warehouseSuggestion.packageCount)
|
||||
const suggestionGirth = computed(() => suggestionItem.value ? suggestionItem.value.lengthCm + 2 * (suggestionItem.value.widthCm + suggestionItem.value.heightCm) : 0)
|
||||
|
||||
interface MeasurementDeviationWarning {
|
||||
key: string
|
||||
@@ -305,36 +294,6 @@ function editableItem(item: InboundMeasurementItem): EditableMeasurementItem {
|
||||
}
|
||||
}
|
||||
|
||||
function openWarehouseSuggestion() {
|
||||
if (!canWarehouseSuggestion.value && !canEdit.value) return
|
||||
const first = form.items[0]
|
||||
warehouseSuggestion.itemId = first?.itemId || ''
|
||||
warehouseSuggestion.packageCount = first?.packageCount || 1
|
||||
warehouseSuggestion.remark = first?.remark || ''
|
||||
warehouseSuggestion.coefficientMode = first?.coefficientMode === 'MULTIPLY' ? 'MULTIPLY' : 'ADD'
|
||||
warehouseSuggestionOpen.value = true
|
||||
}
|
||||
|
||||
function applyWarehouseSuggestion() {
|
||||
const item = suggestionItem.value
|
||||
if (!item) { ElMessage.warning('请选择当前实测 SKU'); return }
|
||||
if (!Number.isInteger(warehouseSuggestion.packageCount) || warehouseSuggestion.packageCount < 1) { ElMessage.warning('箱数必须是大于 0 的整数'); return }
|
||||
item.packageCount = warehouseSuggestion.packageCount
|
||||
item.remark = warehouseSuggestion.remark.trim() || null
|
||||
item.coefficientMode = warehouseSuggestion.coefficientMode
|
||||
warehouseSuggestionOpen.value = false
|
||||
ElMessage.success('仓库建议数据已加入,请保存测量数据')
|
||||
}
|
||||
|
||||
async function loadChargeRules() {
|
||||
if (!customerId.value || !access.can('firstleg:fba-pricing:rule:view')) return
|
||||
chargeRulesLoading.value = true
|
||||
try {
|
||||
chargeRules.value = await listCustomerChargeRules(customerId.value)
|
||||
} catch { chargeRules.value = [] }
|
||||
finally { chargeRulesLoading.value = false }
|
||||
}
|
||||
|
||||
function clearForm() {
|
||||
form.measurementId = null
|
||||
form.version = 0
|
||||
@@ -366,11 +325,6 @@ async function loadMeasurement() {
|
||||
const resource = await getInboundMeasurement(customerId.value, props.inboundId)
|
||||
if (sequence !== requestSequence) return
|
||||
applyDetail(resource)
|
||||
void loadChargeRules()
|
||||
if (props.openWarehouseSuggestion && !warehouseSuggestionAutoOpened.value && form.items.length) {
|
||||
warehouseSuggestionAutoOpened.value = true
|
||||
openWarehouseSuggestion()
|
||||
}
|
||||
} catch (error) {
|
||||
if (sequence !== requestSequence) return
|
||||
detail.value = null
|
||||
@@ -603,17 +557,6 @@ function measurementRowClassName({ row }: { row: EditableMeasurementItem }) {
|
||||
return validationErrors.value[row.clientKey]?.length ? 'measurement-table__row--error' : ''
|
||||
}
|
||||
|
||||
function chargeRuleName(ruleId: string | null | undefined) {
|
||||
if (!ruleId) return '默认规则'
|
||||
const rule = chargeRules.value.find((item) => item.id === ruleId)
|
||||
return rule ? chargeRuleLabel(rule) : ruleId
|
||||
}
|
||||
|
||||
function chargeRuleLabel(rule: CustomerChargeRule) {
|
||||
const remark = rule.remark?.trim()
|
||||
return `系数 ${rule.lengthFactor}${remark ? ` · ${remark}` : ''}${rule.defaultRule ? '(默认)' : ''}`
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [customerId.value, props.inboundId, canView.value] as const,
|
||||
() => {
|
||||
@@ -623,7 +566,6 @@ watch(
|
||||
operationError.value = null
|
||||
conflictDetected.value = false
|
||||
validationErrors.value = {}
|
||||
warehouseSuggestionAutoOpened.value = false
|
||||
clearForm()
|
||||
if (hasContext.value && canView.value) void loadMeasurement()
|
||||
},
|
||||
@@ -733,7 +675,6 @@ defineExpose({ saveDraft, submitForReview, updateActualWeights, mayMaintain, may
|
||||
<div class="measurement-section__heading">
|
||||
<div><h3>箱规明细</h3></div>
|
||||
<div class="measurement-section-actions">
|
||||
<el-button v-if="canWarehouseSuggestion" type="primary" plain :disabled="!mayMaintain || !form.items.length" @click="openWarehouseSuggestion"><Plus :size="15" />添加仓库建议数据</el-button>
|
||||
<el-button v-if="canManualEntry" type="primary" plain :disabled="!mayCreateManualEntry" @click="openManualEntry"><Plus :size="15" />手动录入箱规</el-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -789,15 +730,6 @@ defineExpose({ saveDraft, submitForReview, updateActualWeights, mayMaintain, may
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="计算方式" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<el-select v-model="row.coefficientMode" placeholder="选择计算方式">
|
||||
<el-option label="实测原尺寸(不调整)" value="ACTUAL" />
|
||||
<el-option label="按箱规尺寸策略" value="ADD" />
|
||||
<el-option label="按箱规尺寸系数" value="MULTIPLY" :disabled="!chargeRules.some((rule) => rule.defaultRule && rule.status === 'ACTIVE')" />
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="总体积(m³)" width="108" align="right">
|
||||
<template #default="{ row }">{{ formatNumber(row.serverValues.totalVolumeCbm, 4) }}</template>
|
||||
</el-table-column>
|
||||
@@ -812,18 +744,6 @@ defineExpose({ saveDraft, submitForReview, updateActualWeights, mayMaintain, may
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-empty v-else description="暂无扫描箱测量数据" />
|
||||
<div v-if="form.items.length" class="warehouse-suggestion-table">
|
||||
<h3>仓库建议数据</h3>
|
||||
<el-table :data="form.items" border stripe empty-text="暂无仓库建议数据">
|
||||
<el-table-column prop="itemNo" label="当前实测 SKU" min-width="150" />
|
||||
<el-table-column label="外箱规 长×宽×高(cm)" min-width="190"><template #default="{ row }">{{ formatNumber(row.outerLengthCm) }} × {{ formatNumber(row.outerWidthCm) }} × {{ formatNumber(row.outerHeightCm) }}</template></el-table-column>
|
||||
<el-table-column prop="packageCount" label="箱数" width="90" />
|
||||
<el-table-column label="周长(cm)" width="110"><template #default="{ row }">{{ formatNumber(row.serverValues.girthCm) }}</template></el-table-column>
|
||||
<el-table-column label="总体积(m³)" width="120"><template #default="{ row }">{{ formatNumber(row.serverValues.unitVolumeCbm == null ? null : row.serverValues.unitVolumeCbm * row.packageCount, 6) }}</template></el-table-column>
|
||||
<el-table-column label="计算策略" min-width="190"><template #default="{ row }">{{ row.coefficientMode === 'MULTIPLY' ? '按箱规尺寸系数' : '按箱规尺寸策略' }}<span v-if="row.coefficientMode === 'MULTIPLY'">({{ chargeRuleName(row.chargeRuleId) }})</span></template></el-table-column>
|
||||
<el-table-column prop="remark" label="备注" min-width="160" show-overflow-tooltip />
|
||||
</el-table>
|
||||
</div>
|
||||
</section>
|
||||
</fieldset>
|
||||
</form>
|
||||
@@ -837,21 +757,6 @@ defineExpose({ saveDraft, submitForReview, updateActualWeights, mayMaintain, may
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<AppDialog v-model="warehouseSuggestionOpen" title="添加仓库建议数据" width="min(680px, calc(100vw - 28px))" confirm-text="加入建议" @confirm="applyWarehouseSuggestion">
|
||||
<el-form label-position="top" class="warehouse-suggestion-form">
|
||||
<el-form-item label="当前实测 SKU" required><el-select v-model="warehouseSuggestion.itemId" filterable><el-option v-for="item in form.items" :key="item.itemId" :label="`${item.itemNo} · ${item.boxSpecification || item.boxCode || '实测SKU'}`" :value="item.itemId" /></el-select></el-form-item>
|
||||
<el-form-item label="箱数" required><el-input-number v-model="warehouseSuggestion.packageCount" :min="1" :max="1000000" step-strictly /></el-form-item>
|
||||
<el-form-item label="计算方式"><el-select v-model="warehouseSuggestion.coefficientMode"><el-option label="按箱规尺寸策略" value="ADD" /><el-option label="按箱规尺寸系数" value="MULTIPLY" /></el-select></el-form-item>
|
||||
<el-form-item label="备注"><el-input v-model="warehouseSuggestion.remark" type="textarea" :rows="3" maxlength="500" show-word-limit /></el-form-item>
|
||||
</el-form>
|
||||
<dl v-if="suggestionItem" class="suggestion-preview">
|
||||
<div><dt>长 × 宽 × 高</dt><dd>{{ formatNumber(suggestionItem.lengthCm) }} × {{ formatNumber(suggestionItem.widthCm) }} × {{ formatNumber(suggestionItem.heightCm) }} cm</dd></div>
|
||||
<div><dt>箱规周长</dt><dd>{{ formatNumber(suggestionGirth) }} cm</dd></div>
|
||||
<div><dt>单箱体积</dt><dd>{{ formatNumber(suggestionItem.serverValues.unitVolumeCbm, 6) }} m³</dd></div>
|
||||
<div><dt>总体积</dt><dd>{{ formatNumber(suggestionTotalVolume, 6) }} m³</dd></div>
|
||||
</dl>
|
||||
</AppDialog>
|
||||
|
||||
<el-dialog v-model="imagePreviewOpen" title="当前有效测量图片" width="min(720px, 92vw)" destroy-on-close @closed="releaseImagePreview">
|
||||
<div class="measurement-image-preview">
|
||||
<el-skeleton v-if="imagePreviewLoading" :rows="5" animated />
|
||||
|
||||
@@ -34,8 +34,12 @@ export interface InboundMeasurementItem {
|
||||
outerLengthCm: number | null
|
||||
outerWidthCm: number | null
|
||||
outerHeightCm: number | null
|
||||
calculatedOuterLengthCm?: number | null
|
||||
calculatedOuterWidthCm?: number | null
|
||||
calculatedOuterHeightCm?: number | null
|
||||
calculationRuleVersion?: string | null
|
||||
chargeRuleId?: MeasurementIdentifier | null
|
||||
coefficientMode?: 'ADD' | 'MULTIPLY' | null
|
||||
coefficientMode?: 'ACTUAL' | 'ADD' | 'MULTIPLY' | null
|
||||
remark: string | null
|
||||
}
|
||||
|
||||
@@ -133,7 +137,7 @@ export interface SaveInboundMeasurementItemRequest {
|
||||
outerWidthCm: number | null
|
||||
outerHeightCm: number | null
|
||||
chargeRuleId?: MeasurementIdentifier | null
|
||||
coefficientMode?: 'ADD' | 'MULTIPLY' | null
|
||||
coefficientMode?: 'ACTUAL' | 'ADD' | 'MULTIPLY' | null
|
||||
remark: string | null
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,12 @@ const source = readFileSync(resolve(process.cwd(), 'src/modules/unified-orders/p
|
||||
const routeSource = readFileSync(resolve(process.cwd(), 'src/modules/unified-orders/routes.ts'), 'utf8')
|
||||
|
||||
describe('UnifiedOrdersView contract', () => {
|
||||
it('blocks confirm-send when the customer has already confirmed the fee', () => {
|
||||
expect(source).toContain("snapshot.customerConfirmationStatus === 'CONFIRMED'")
|
||||
expect(source).toContain('客户已确认该费用,费用已锁定,不允许再修改或重新发送')
|
||||
expect(source).toContain('async function openFeeConfirmDialog()')
|
||||
})
|
||||
|
||||
it('only exposes the six product main statuses', () => {
|
||||
expect(unifiedOrderStatusOptions.map((item) => item.label)).toEqual([
|
||||
'待审核', '待测量数据', '待分柜', '装柜中', '装柜完成', '派送完成',
|
||||
@@ -337,35 +343,61 @@ describe('UnifiedOrdersView contract', () => {
|
||||
expect(source).toContain('getCustomerDimensionConfig')
|
||||
expect(source).toContain('matchingDimensionRule')
|
||||
expect(source).toContain('|| rules[rules.length - 1]')
|
||||
expect(source).toContain('`原尺寸 + ${values[0]}cm`')
|
||||
expect(source).toContain('`原尺寸 + ${Number(values[0]) * 2}cm(单边 +${values[0]}cm)`')
|
||||
expect(source).toContain('`原尺寸 × ${rule.lengthFactor}`')
|
||||
expect(source).toContain('label="箱规尺寸" value="ADD"')
|
||||
expect(source).toContain('label="箱规系数" value="MULTIPLY"')
|
||||
})
|
||||
|
||||
it('keeps the three section calculation modes independent and recalculates their display rows', () => {
|
||||
it('keeps preview modes independent and persists both outer-carton selectors immediately', () => {
|
||||
expect(source).toContain("measurementSectionModes = reactive<Record<MeasurementSection, MeasurementCalculationMode>>")
|
||||
expect(source).toContain(':model-value="measurementSectionModes.customer"')
|
||||
expect(source).toContain(':model-value="measurementSectionModes.outer"')
|
||||
expect(source).toContain(':model-value="measurementSectionModes.actual"')
|
||||
expect(source).toContain("updateMeasurementSectionMode('customer', $event)")
|
||||
expect(source).toContain("updateMeasurementSectionMode('outer', $event)")
|
||||
expect(source).toContain("updateMeasurementSectionMode('actual', $event)")
|
||||
expect(source).not.toContain("updateMeasurementSectionMode('outer', $event)")
|
||||
expect(source.match(/@change="selectAndSaveOuterMeasurementMode\(\$event\)"/g)).toHaveLength(2)
|
||||
expect(source.match(/:disabled="Boolean\(measurementStrategySavingId\) \|\| !can\('platform:firstleg:inbound:measure:edit'\)"/g)).toHaveLength(2)
|
||||
expect(source).toContain('const previousMode = measurementSectionModes.outer')
|
||||
expect(source).toContain('if (!saved) measurementSectionModes.outer = previousMode')
|
||||
expect(source).toContain('const saved = await updateMeasurementCalculationMode(null, mode)')
|
||||
expect(source).toContain('calculatedMeasurementRow(row, measurementSectionModes.customer, true)')
|
||||
expect(source).toContain('calculatedMeasurementRow(source, measurementSectionModes.outer)')
|
||||
expect(source).toContain('calculatedMeasurementRow(row as unknown as Record<string, unknown>, measurementSectionModes.actual)')
|
||||
expect(source).toContain("return Number(hasOuterOverride && override != null && override !== '' ? override : value)")
|
||||
expect(source).toContain('rawDimensions.map((value) => value + rule.edgeAdditionCm)')
|
||||
expect(source).toContain('rawDimensions.map((value) => value + rule.edgeAdditionCm * 2)')
|
||||
expect(source).toContain('matchedSpecificationCoefficient(chargeRule, calculationSource)')
|
||||
expect(source).toContain('listCustomerChargeRules(customerId, tenantId)')
|
||||
expect(source).toContain('matchedDimensions.map((value) => roundedUp(value * specificationCoefficient, 3))')
|
||||
expect(source).toContain("const selectedMode: 'ADD' | 'MULTIPLY' = measurementSectionModes.outer === 'MULTIPLY'")
|
||||
expect(source).toContain('? selectedMode')
|
||||
expect(source).toContain('const selectedMode: MeasurementCalculationMode = measurementSectionModes.outer')
|
||||
expect(source).not.toContain('[chargeRule.lengthFactor, chargeRule.widthFactor, chargeRule.heightFactor]')
|
||||
expect(source).not.toContain('column.key === \'coefficientMode\'')
|
||||
expect(source).not.toContain('@change="updateMeasurementCalculationMode(null, $event)"')
|
||||
})
|
||||
|
||||
it('confirms customer billing from the saved outer-carton totals', () => {
|
||||
expect(source).toContain('const outerTotals = measurementTotals(outerMeasurementRows.value)')
|
||||
expect(source).toContain("ElMessage.warning('外箱规数据不完整,请先保存外箱规尺寸和计算规则')")
|
||||
expect(source).toContain("weightSource: 'OUTER'")
|
||||
expect(source).toContain("volumeSource: 'OUTER'")
|
||||
expect(source).toContain('selectAndSaveOuterMeasurementMode($event)')
|
||||
expect(source).toContain('const saved = await updateMeasurementCalculationMode(null, mode)')
|
||||
expect(source).toContain('coefficientMode: targetIds.has(String(item.id)) ? mode')
|
||||
expect(source).not.toContain('feeConfirmVolume.value = Number(summary.measurement?.actualVolumeCbm)')
|
||||
const sendFeeSource = source.slice(source.indexOf('async function executeConfirmAndSendFee()'), source.indexOf('function handleOrderSelectionChange'))
|
||||
expect(sendFeeSource).not.toContain('saveInboundMeasurementDraft')
|
||||
expect(sendFeeSource).not.toContain('updateMeasurementCalculationMode')
|
||||
})
|
||||
|
||||
it('restores the saved outer-carton rule and server-calculated dimensions on re-entry', () => {
|
||||
expect(source).toContain("mode is MeasurementCalculationMode => mode === 'ACTUAL' || mode === 'ADD' || mode === 'MULTIPLY'")
|
||||
expect(source).toContain('measurementSectionModes.outer = savedModes.length === 1 ? savedModes[0]!')
|
||||
expect(source).toContain('row.calculatedOuterLengthCm')
|
||||
expect(source).toContain('measurementCalculationModeForRow(source) === measurementSectionModes.outer')
|
||||
expect(source).toContain('lengthCm: Number(savedDimensions[0])')
|
||||
})
|
||||
|
||||
it('binds dimension and weight evidence to their formal server records without mixing galleries', () => {
|
||||
expect(source).toContain('<el-table :data="actualMeasurementRows" row-key="id"')
|
||||
expect(source).toContain('<el-table-column label="箱号" prop="boxCode"')
|
||||
|
||||
@@ -292,7 +292,7 @@ function matchingDimensionRule(row: Record<string, unknown>): CustomerDimensionR
|
||||
function addCoefficientLabel(row?: Record<string, unknown>) {
|
||||
const rows = row ? [row] : (measurementDetail.value?.items || []) as unknown as Record<string, unknown>[]
|
||||
const values = [...new Set(rows.map((item) => matchingDimensionRule(item)?.edgeAdditionCm).filter((value) => value != null))]
|
||||
if (values.length === 1) return `原尺寸 + ${values[0]}cm`
|
||||
if (values.length === 1) return `原尺寸 + ${Number(values[0]) * 2}cm(单边 +${values[0]}cm)`
|
||||
if (measurementDimensionConfigLoading.value) return '原尺寸 + 加载中…'
|
||||
return values.length > 1 ? '原尺寸 + 按箱规匹配' : '原尺寸 + 未匹配箱规'
|
||||
}
|
||||
@@ -305,18 +305,22 @@ function multiplyCoefficientLabel() {
|
||||
|
||||
function matchedSpecificationCoefficient(rule: CustomerChargeRule, row: Record<string, unknown>) {
|
||||
const tiers = [...(rule.remark || '').matchAll(
|
||||
/(SMALL|MEDIUM|LARGE|EXTRA_LARGE):(\d+(?:\.\d+)?),(\d+(?:\.\d+)?),(\d+(?:\.\d+)?);系数:(\d+(?:\.\d+)?)/g,
|
||||
/(SMALL|MEDIUM|LARGE|EXTRA_LARGE):(\d+(?:\.\d+)?),(\d+(?:\.\d+)?),(\d+(?:\.\d+)?);系数:(\d+(?:\.\d+)?)(?:;重量:(\d+(?:\.\d+)?))?/g,
|
||||
)].map((match) => ({
|
||||
dimensions: [Number(match[2]), Number(match[3]), Number(match[4])].sort((left, right) => right - left),
|
||||
coefficient: Number(match[5]),
|
||||
maxWeightKg: match[6] == null ? null : Number(match[6]),
|
||||
})).filter((tier) => tier.dimensions.every((value) => Number.isFinite(value) && value > 0)
|
||||
&& Number.isFinite(tier.coefficient) && tier.coefficient > 0)
|
||||
&& Number.isFinite(tier.coefficient) && tier.coefficient > 0
|
||||
&& (tier.maxWeightKg == null || (Number.isFinite(tier.maxWeightKg) && tier.maxWeightKg > 0)))
|
||||
.sort((left, right) => left.dimensions.reduce((total, value) => total * value, 1)
|
||||
- right.dimensions.reduce((total, value) => total * value, 1))
|
||||
if (!tiers.length) return Number(rule.lengthFactor || 1)
|
||||
const measured = [Number(row.lengthCm), Number(row.widthCm), Number(row.heightCm)]
|
||||
.sort((left, right) => right - left)
|
||||
const matched = tiers.find((tier) => measured.every((value, index) => value <= tier.dimensions[index]))
|
||||
const unitWeight = Number(row.unitWeightKg)
|
||||
const matched = tiers.find((tier) => measured.every((value, index) => value <= tier.dimensions[index])
|
||||
&& (tier.maxWeightKg == null || !Number.isFinite(unitWeight) || unitWeight <= tier.maxWeightKg))
|
||||
return (matched || tiers[tiers.length - 1]).coefficient
|
||||
}
|
||||
|
||||
@@ -351,7 +355,7 @@ function calculatedMeasurementRow(
|
||||
if (!rawDimensions.every((value) => Number.isFinite(value) && value > 0)) return { ...source }
|
||||
|
||||
const matchedDimensions = mode === 'ADD' && rule
|
||||
? rawDimensions.map((value) => value + rule.edgeAdditionCm)
|
||||
? rawDimensions.map((value) => value + rule.edgeAdditionCm * 2)
|
||||
: rawDimensions
|
||||
const chargeRule = activeMeasurementChargeRule.value
|
||||
const specificationCoefficient = mode === 'MULTIPLY' && chargeRule
|
||||
@@ -364,9 +368,13 @@ function calculatedMeasurementRow(
|
||||
? Math.round((lengthCm * widthCm * heightCm / 1_000_000) * 1000) / 1000
|
||||
: roundedUp(lengthCm * widthCm * heightCm / 1_000_000, 4)
|
||||
const totalVolumeCbm = Number.isFinite(packageCount) ? roundedUp(unitVolumeCbm * packageCount, 4) : null
|
||||
const fallbackMeasuredUnitWeight = !customerSource && Number.isFinite(Number(measurementDetail.value?.actualGrossWeightKg))
|
||||
&& Number(measurementDetail.value?.actualPackageCount) > 0
|
||||
? Number(measurementDetail.value?.actualGrossWeightKg) / Number(measurementDetail.value?.actualPackageCount)
|
||||
: null
|
||||
const sourceUnitWeight = customerSource
|
||||
? Number(source.grossWeightKg) / packageCount
|
||||
: Number(source.unitWeightKg)
|
||||
: Number.isFinite(Number(source.unitWeightKg)) ? Number(source.unitWeightKg) : fallbackMeasuredUnitWeight
|
||||
const unitWeightKg = Number.isFinite(sourceUnitWeight) ? sourceUnitWeight : null
|
||||
const totalWeightKg = unitWeightKg != null && Number.isFinite(packageCount) ? unitWeightKg * packageCount : null
|
||||
return {
|
||||
@@ -383,6 +391,20 @@ const customerMeasurementRows = computed(() => (detail.value?.order.cargoes || [
|
||||
)))
|
||||
const outerMeasurementRows = computed(() => (measurementDetail.value?.items || []).map((row) => {
|
||||
const source = row as unknown as Record<string, unknown>
|
||||
const savedDimensions = [row.calculatedOuterLengthCm, row.calculatedOuterWidthCm, row.calculatedOuterHeightCm]
|
||||
if (savedDimensions.every((value) => Number.isFinite(Number(value)) && Number(value) > 0)
|
||||
&& measurementCalculationModeForRow(source) === measurementSectionModes.outer) {
|
||||
const packageCount = Number(row.packageCount)
|
||||
const unitWeightKg = Number.isFinite(Number(row.unitWeightKg)) ? Number(row.unitWeightKg) : null
|
||||
return {
|
||||
...source,
|
||||
lengthCm: Number(savedDimensions[0]),
|
||||
widthCm: Number(savedDimensions[1]),
|
||||
heightCm: Number(savedDimensions[2]),
|
||||
unitWeightKg,
|
||||
totalWeightKg: unitWeightKg != null && Number.isFinite(packageCount) ? unitWeightKg * packageCount : null,
|
||||
}
|
||||
}
|
||||
return calculatedMeasurementRow(source, measurementSectionModes.outer)
|
||||
}))
|
||||
const actualMeasurementRows = computed(() => (measurementDetail.value?.items || []).map((row) => (
|
||||
@@ -397,6 +419,18 @@ function updateMeasurementSectionMode(section: MeasurementSection, mode: Measure
|
||||
measurementSectionModes[section] = mode
|
||||
}
|
||||
|
||||
async function selectAndSaveOuterMeasurementMode(mode: MeasurementCalculationMode) {
|
||||
if (measurementStrategySavingId.value) return
|
||||
const previousMode = measurementSectionModes.outer
|
||||
if (mode === 'MULTIPLY' && !activeMeasurementChargeRule.value) {
|
||||
ElMessage.warning('该客户尚未启用系数配置,请先到客户计费规则中设置默认规则')
|
||||
return
|
||||
}
|
||||
measurementSectionModes.outer = mode
|
||||
const saved = await updateMeasurementCalculationMode(null, mode)
|
||||
if (!saved) measurementSectionModes.outer = previousMode
|
||||
}
|
||||
|
||||
const statusTabs = computed(() => [
|
||||
{ value: '' as const, label: '全部' },
|
||||
...unifiedOrderStatusOptions,
|
||||
@@ -777,6 +811,10 @@ function measurementValue(row: Record<string, unknown>, key: string, customerSou
|
||||
const count = Number(row.packageCount)
|
||||
return Number.isFinite(total) && Number.isFinite(count) && count > 0 ? formatNumber(total / count, 4) : '—'
|
||||
}
|
||||
if (key === 'unitWeightKg') {
|
||||
const value = Number(row[key])
|
||||
return Number.isFinite(value) ? formatNumber(value, 4) : '—'
|
||||
}
|
||||
if (customerSource && key === 'unitVolumeCbm') {
|
||||
const total = Number(row.volumeCbm)
|
||||
const count = Number(row.packageCount)
|
||||
@@ -804,10 +842,10 @@ function measurementCalculationModeForRow(row: Record<string, unknown>): Measure
|
||||
return measuredItem?.coefficientMode === 'MULTIPLY' ? 'MULTIPLY' : measuredItem?.coefficientMode === 'ADD' ? 'ADD' : 'ACTUAL'
|
||||
}
|
||||
|
||||
async function updateMeasurementCalculationMode(row: Record<string, unknown> | null, mode: Exclude<MeasurementCalculationMode, 'ACTUAL'>) {
|
||||
async function updateMeasurementCalculationMode(row: Record<string, unknown> | null, mode: MeasurementCalculationMode): Promise<boolean> {
|
||||
const summary = detail.value?.summary
|
||||
const measurement = measurementDetail.value
|
||||
if (!summary?.inboundId || !measurement?.measurementId || measurement.version == null || !can('platform:firstleg:inbound:measure:edit')) return
|
||||
if (!summary?.inboundId || !measurement?.measurementId || measurement.version == null || !can('platform:firstleg:inbound:measure:edit')) return false
|
||||
const itemId = normalizedIdentifier(row?.id)
|
||||
const itemNo = normalizedIdentifier(row?.itemNo)
|
||||
const targetIds = new Set(measurement.items
|
||||
@@ -815,7 +853,7 @@ async function updateMeasurementCalculationMode(row: Record<string, unknown> | n
|
||||
.map((item) => String(item.id)))
|
||||
if (!targetIds.size) {
|
||||
ElMessage.warning('当前数据尚未关联实测明细,暂不能设置计算方式')
|
||||
return
|
||||
return false
|
||||
}
|
||||
measurementStrategySavingId.value = row ? (itemId || `item-${itemNo}`) : 'all'
|
||||
try {
|
||||
@@ -826,7 +864,7 @@ async function updateMeasurementCalculationMode(row: Record<string, unknown> | n
|
||||
) === 'MULTIPLY')
|
||||
if (requiresMultiply && !activeRule) {
|
||||
ElMessage.warning('该客户尚未启用系数配置,请先到客户计费规则中设置默认规则')
|
||||
return
|
||||
return false
|
||||
}
|
||||
await saveInboundMeasurementDraft(String(summary.tenantId), String(summary.inboundId), {
|
||||
measurementId: String(measurement.measurementId),
|
||||
@@ -852,17 +890,19 @@ async function updateMeasurementCalculationMode(row: Record<string, unknown> | n
|
||||
chargeRuleId: (targetIds.has(String(item.id)) ? mode : item.coefficientMode) === 'MULTIPLY'
|
||||
? activeRule!.id
|
||||
: null,
|
||||
coefficientMode: targetIds.has(String(item.id)) ? mode : item.coefficientMode || 'ADD',
|
||||
coefficientMode: targetIds.has(String(item.id)) ? mode : item.coefficientMode || 'ACTUAL',
|
||||
remark: item.remark,
|
||||
})),
|
||||
})
|
||||
measurementLoadedKey.value = ''
|
||||
await loadMeasurementDetails()
|
||||
ElMessage.success(`${row ? '当前数据' : '全部测量数据'}的计算方式已更新,体积已重新计算`)
|
||||
return true
|
||||
} catch (error) {
|
||||
measurementLoadedKey.value = ''
|
||||
await loadMeasurementDetails()
|
||||
ElMessage.error(getApiErrorDetails(error, '计算方式更新失败').message)
|
||||
return false
|
||||
} finally {
|
||||
measurementStrategySavingId.value = ''
|
||||
}
|
||||
@@ -892,9 +932,7 @@ async function saveWarehouseOuterDimensions(row: Record<string, unknown>) {
|
||||
ElMessage.warning('仓库修改外箱尺寸必须大于 0')
|
||||
return
|
||||
}
|
||||
const selectedMode: 'ADD' | 'MULTIPLY' = measurementSectionModes.outer === 'MULTIPLY'
|
||||
? 'MULTIPLY'
|
||||
: 'ADD'
|
||||
const selectedMode: MeasurementCalculationMode = measurementSectionModes.outer
|
||||
const selectedChargeRuleId = selectedMode === 'MULTIPLY'
|
||||
? activeMeasurementChargeRule.value?.id || null
|
||||
: null
|
||||
@@ -924,7 +962,7 @@ async function saveWarehouseOuterDimensions(row: Record<string, unknown>) {
|
||||
chargeRuleId: String(item.id) === String(row.id) ? selectedChargeRuleId : item.chargeRuleId,
|
||||
coefficientMode: String(item.id) === String(row.id)
|
||||
? selectedMode
|
||||
: item.coefficientMode || 'ADD',
|
||||
: item.coefficientMode || 'ACTUAL',
|
||||
remark: item.remark,
|
||||
})),
|
||||
})
|
||||
@@ -1054,11 +1092,16 @@ async function loadMeasurementDetails() {
|
||||
loadMeasurementDimensionConfig(),
|
||||
])
|
||||
if (serial !== measurementRequestSerial) return
|
||||
const configuredMode = measurementDimensionConfig.value?.defaultCalculationMode
|
||||
measurementSectionModes.outer = configuredMode === 'MULTIPLY' && activeMeasurementChargeRule.value
|
||||
? 'MULTIPLY'
|
||||
: 'ADD'
|
||||
measurementDetail.value = response
|
||||
const savedModes = [...new Set(response.items.map((item) => item.coefficientMode)
|
||||
.filter((mode): mode is MeasurementCalculationMode => mode === 'ACTUAL' || mode === 'ADD' || mode === 'MULTIPLY'))]
|
||||
const configuredMode = measurementDimensionConfig.value?.defaultCalculationMode
|
||||
// 已保存的明细规则优先于客户默认配置,确保再次进入时回显上次选择及其计算结果。
|
||||
measurementSectionModes.outer = savedModes.length === 1 ? savedModes[0]!
|
||||
: configuredMode === 'MULTIPLY' && activeMeasurementChargeRule.value ? 'MULTIPLY'
|
||||
: configuredMode === 'ADD' ? 'ADD' : 'ACTUAL'
|
||||
measurementSectionModes.customer = measurementSectionModes.outer
|
||||
measurementSectionModes.actual = 'ACTUAL'
|
||||
measurementLoadedKey.value = key
|
||||
} catch (error) {
|
||||
if (serial !== measurementRequestSerial) return
|
||||
@@ -1286,7 +1329,7 @@ function openMeasurement(row: UnifiedOrderResource) {
|
||||
measurementDialogOpen.value = true
|
||||
}
|
||||
|
||||
function openFeeConfirmDialog() {
|
||||
async function openFeeConfirmDialog() {
|
||||
const summary = detail.value?.summary
|
||||
const reviewStatus = String(summary?.measurement?.reviewStatus ?? '').trim().toUpperCase()
|
||||
if (reviewStatus !== 'APPROVED') {
|
||||
@@ -1300,8 +1343,28 @@ function openFeeConfirmDialog() {
|
||||
if (confirmFeeSending.value) {
|
||||
return
|
||||
}
|
||||
feeConfirmWeight.value = Number(summary.measurement?.actualGrossWeightKg) || null
|
||||
feeConfirmVolume.value = Number(summary.measurement?.actualVolumeCbm) || null
|
||||
try {
|
||||
const snapshot = await getLatestInboundFee(String(summary.tenantId), String(summary.inboundId))
|
||||
if (snapshot.customerConfirmationStatus === 'CONFIRMED' || snapshot.customerConfirmedAt) {
|
||||
ElMessage.warning('客户已确认该费用,费用已锁定,不允许再修改或重新发送')
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error(getApiErrorDetails(error, '费用状态查询失败,请刷新后重试').message)
|
||||
return
|
||||
}
|
||||
if (measurementStrategySavingId.value) {
|
||||
ElMessage.warning('外箱规计算规则正在保存,请稍后再发送费用')
|
||||
return
|
||||
}
|
||||
const outerTotals = measurementTotals(outerMeasurementRows.value)
|
||||
if (!outerMeasurementRows.value.length || !(Number(outerTotals.gross) > 0)
|
||||
|| !(Number(outerTotals.volume) > 0)) {
|
||||
ElMessage.warning('外箱规数据不完整,请先保存外箱规尺寸和计算规则')
|
||||
return
|
||||
}
|
||||
feeConfirmWeight.value = Number(outerTotals.gross)
|
||||
feeConfirmVolume.value = Number(outerTotals.volume)
|
||||
feeConfirmOpen.value = true
|
||||
}
|
||||
|
||||
@@ -1319,12 +1382,17 @@ async function executeConfirmAndSendFee() {
|
||||
const tenantId = String(summary.tenantId)
|
||||
const inboundId = String(summary.inboundId)
|
||||
let snapshot = await getLatestInboundFee(tenantId, inboundId)
|
||||
if (snapshot.customerConfirmationStatus === 'CONFIRMED' || snapshot.customerConfirmedAt) {
|
||||
ElMessage.warning('客户已确认该费用,费用已锁定,不允许再修改或重新发送')
|
||||
feeConfirmOpen.value = false
|
||||
return
|
||||
}
|
||||
if (snapshot.feeStatus === 'CALCULATED') {
|
||||
snapshot = await calculateInboundFee(tenantId, inboundId, {
|
||||
selectedWeightKg,
|
||||
selectedVolumeCbm,
|
||||
weightSource: 'ACTUAL',
|
||||
volumeSource: 'ACTUAL',
|
||||
weightSource: 'OUTER',
|
||||
volumeSource: 'OUTER',
|
||||
}, createFeeActionIdempotencyKey('calculate'))
|
||||
snapshot = await confirmInboundFee(tenantId, inboundId, createFeeActionIdempotencyKey('confirm'))
|
||||
}
|
||||
@@ -1982,7 +2050,7 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
</section>
|
||||
<section class="measurement-section">
|
||||
<div class="measurement-section-title"><button type="button" class="measurement-section-toggle" :aria-expanded="!measurementCollapsed.outer" @click="toggleMeasurementSection('outer')"><h3>二、外箱规数据</h3><span class="measurement-section-summary"><span>总件数:{{ measurementTotals(outerMeasurementRows).count ?? '—' }}</span><span>总毛重:{{ formatNumber(measurementTotals(outerMeasurementRows).gross) }}</span><span>总体积:{{ formatNumber(measurementTotals(outerMeasurementRows).volume, 6) }}</span><span>总材积重:{{ formatNumber(measurementTotals(outerMeasurementRows).volumetric) }}</span><span>重量体积:{{ formatNumber(measurementTotals(outerMeasurementRows).weightVolume, 6) }}</span></span><ChevronUp v-if="!measurementCollapsed.outer" :size="18" /><ChevronDown v-else :size="18" /></button><el-select class="measurement-section-mode" :model-value="measurementSectionModes.outer" aria-label="外箱规数据计算方式" @change="updateMeasurementSectionMode('outer', $event)"><el-option label="实测原尺寸(不调整)" value="ACTUAL" /><el-option label="箱规尺寸" value="ADD" /><el-option label="箱规系数" value="MULTIPLY" :disabled="multiplyModeDisabled" /></el-select></div>
|
||||
<div class="measurement-section-title"><button type="button" class="measurement-section-toggle" :aria-expanded="!measurementCollapsed.outer" @click="toggleMeasurementSection('outer')"><h3>二、外箱规数据</h3><span class="measurement-section-summary"><span>总件数:{{ measurementTotals(outerMeasurementRows).count ?? '—' }}</span><span>总毛重:{{ formatNumber(measurementTotals(outerMeasurementRows).gross) }}</span><span>总体积:{{ formatNumber(measurementTotals(outerMeasurementRows).volume, 6) }}</span><span>总材积重:{{ formatNumber(measurementTotals(outerMeasurementRows).volumetric) }}</span><span>重量体积:{{ formatNumber(measurementTotals(outerMeasurementRows).weightVolume, 6) }}</span></span><ChevronUp v-if="!measurementCollapsed.outer" :size="18" /><ChevronDown v-else :size="18" /></button><el-select class="measurement-section-mode" :model-value="measurementSectionModes.outer" aria-label="外箱规数据计算方式" :loading="Boolean(measurementStrategySavingId)" :disabled="Boolean(measurementStrategySavingId) || !can('platform:firstleg:inbound:measure:edit')" @change="selectAndSaveOuterMeasurementMode($event)"><el-option label="实测原尺寸(不调整)" value="ACTUAL" /><el-option label="箱规尺寸" value="ADD" /><el-option label="箱规系数" value="MULTIPLY" :disabled="multiplyModeDisabled" /></el-select></div>
|
||||
<div v-if="!measurementCollapsed.outer">
|
||||
<div v-loading="measurementLoading" class="detail-table-wrap"><el-table :data="outerMeasurementRows" border stripe empty-text="暂无外箱规数据"><el-table-column label="#" prop="itemNo" min-width="70" /><el-table-column v-for="column in outerColumns.filter((item) => item.key !== 'itemNo')" :key="column.key" :label="column.label" :prop="column.key" min-width="150"><template #default="{ row }"><LineClampText v-if="column.key === 'remark'" :content="measurementValue(row as Record<string, unknown>, column.key)" /><template v-else>{{ measurementValue(row as Record<string, unknown>, column.key) }}</template></template></el-table-column></el-table></div>
|
||||
<div class="measurement-total"><strong>合计</strong><span>箱数:{{ measurementTotals(outerMeasurementRows).count ?? '—' }}</span><span>总毛重:{{ formatNumber(measurementTotals(outerMeasurementRows).gross) }}</span><span>总体积:{{ formatNumber(measurementTotals(outerMeasurementRows).volume, 6) }}</span><span>总材积:{{ formatNumber(measurementTotals(outerMeasurementRows).volumetric) }}</span></div>
|
||||
@@ -1990,7 +2058,7 @@ onBeforeUnmount(() => {
|
||||
</section>
|
||||
<div class="measurement-rule-selector">
|
||||
<span class="measurement-rule-selector__label">箱规计算规则选择</span>
|
||||
<el-select class="measurement-rule-selector__control" :model-value="measurementSectionModes.outer" aria-label="外箱规数据计算方式" @change="updateMeasurementSectionMode('outer', $event)">
|
||||
<el-select class="measurement-rule-selector__control" :model-value="measurementSectionModes.outer" aria-label="外箱规数据计算方式" :loading="Boolean(measurementStrategySavingId)" :disabled="Boolean(measurementStrategySavingId) || !can('platform:firstleg:inbound:measure:edit')" @change="selectAndSaveOuterMeasurementMode($event)">
|
||||
<el-option label="实测原尺寸(不调整)" value="ACTUAL" />
|
||||
<el-option label="箱规尺寸" value="ADD" />
|
||||
<el-option label="箱规系数" value="MULTIPLY" :disabled="multiplyModeDisabled" />
|
||||
|
||||
Reference in New Issue
Block a user