提交
This commit is contained in:
@@ -297,6 +297,14 @@ describe('UnifiedOrdersView contract', () => {
|
||||
expect(source).toContain('<ChevronUp v-if="!measurementCollapsed.customer"')
|
||||
expect(source).toContain('<ChevronDown v-else :size="18" />')
|
||||
expect(source).toContain("{ key: 'girthCm', label: '箱规周长(cm)' }")
|
||||
expect(source).toContain('label="#" prop="itemNo"')
|
||||
expect(source).toContain("const actualMeasuredColumns = measuredColumns.filter((column) => column.key !== 'itemNo')")
|
||||
expect(source).toContain('v-for="column in actualMeasuredColumns"')
|
||||
expect(source).toContain('仓库修改外箱(长)')
|
||||
expect(source).toContain('仓库修改外箱(宽)')
|
||||
expect(source).toContain('仓库修改外箱(高)')
|
||||
expect(source).toContain('saveWarehouseOuterDimensions')
|
||||
expect(source).toContain('相关数值已重新计算')
|
||||
expect(source).toContain('runMoreAction($event, row as UnifiedOrderResource)')
|
||||
expect(source).toContain(':aria-label="moreUnifiedOrderActionsLabel(row as UnifiedOrderResource)"')
|
||||
expect(source).toContain(':title="moreUnifiedOrderActionsLabel(row as UnifiedOrderResource)"')
|
||||
@@ -313,8 +321,37 @@ describe('UnifiedOrdersView contract', () => {
|
||||
expect(source).not.toContain('formatNumber(2 * (width + height))')
|
||||
})
|
||||
|
||||
it('shows the matched dimension addition and enabled multiplier in calculation choices', () => {
|
||||
expect(source).toContain('getCustomerDimensionConfig')
|
||||
expect(source).toContain('matchingDimensionRule')
|
||||
expect(source).toContain('`原尺寸 + ${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', () => {
|
||||
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).toContain('calculatedMeasurementRow(row, measurementSectionModes.customer, true)')
|
||||
expect(source).toContain('}, measurementSectionModes.outer)')
|
||||
expect(source).toContain('calculatedMeasurementRow(row as unknown as Record<string, unknown>, measurementSectionModes.actual)')
|
||||
expect(source).toContain('rawDimensions.map((value) => value + rule.edgeAdditionCm)')
|
||||
expect(source).toContain('matchedSpecificationCoefficient(chargeRule, calculationSource)')
|
||||
expect(source).toContain('listCustomerChargeRules(customerId, tenantId)')
|
||||
expect(source).toContain('matchedDimensions.map((value) => roundedUp(value * specificationCoefficient, 3))')
|
||||
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('binds dimension and weight evidence to their formal server records without mixing galleries', () => {
|
||||
expect(source).toContain('<el-table :data="measurementDetail?.items || []" row-key="id"')
|
||||
expect(source).toContain('<el-table :data="actualMeasurementRows" row-key="id"')
|
||||
expect(source).toContain('<el-table-column label="箱号" prop="boxCode"')
|
||||
expect(source).toContain("row.boxCode || '—'")
|
||||
expect(source).toContain("`箱号 ${row.boxCode || row.itemNo || '箱规编码待补充'} 的尺寸图片`")
|
||||
@@ -444,9 +481,10 @@ describe('UnifiedOrdersView contract', () => {
|
||||
|
||||
it('matches the prototype table grid and distinguishes container identifiers', () => {
|
||||
expect(source).toContain('row-key="orderId" border stripe')
|
||||
expect(source).toContain('<el-table :data="detail.order.cargoes" border stripe')
|
||||
expect(source.match(/<el-table :data="measurementDetail\?\.items \|\| \[\]"[^>]*border stripe/g)).toHaveLength(2)
|
||||
expect(source).toContain('<el-table :data="detail.order.cargoes" border stripe empty-text="暂无客户下单数据"')
|
||||
expect(source).toContain('<el-table :data="customerMeasurementRows" border stripe')
|
||||
expect(source).toContain('<el-table :data="outerMeasurementRows" border stripe')
|
||||
expect(source).toContain('<el-table :data="actualMeasurementRows" row-key="id" border stripe')
|
||||
expect(source).toContain('<el-table :data="customerMeasurementRows" border stripe empty-text="暂无客户下单数据"')
|
||||
expect(source).toContain('--el-table-border-color: #e5e7eb')
|
||||
expect(source).toContain('--el-table-row-hover-bg-color: #f3f7fb')
|
||||
expect(source).toContain('.el-table--striped .el-table__body tr.el-table__row--striped')
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch, type Component } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { ArrowLeft, Box, ChevronDown, ChevronUp, CircleCheck, Download, Eye, GitBranch, History, Link2, ListChecks, MoreHorizontal, PackageOpen, Ruler, SlidersHorizontal, StickyNote, WalletCards } from '@lucide/vue'
|
||||
import { ArrowLeft, Box, ChevronDown, ChevronUp, CircleCheck, Download, Eye, GitBranch, History, Link2, ListChecks, MoreHorizontal, PackageOpen, Ruler, Send, SlidersHorizontal, StickyNote, WalletCards } from '@lucide/vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import {
|
||||
getPlatformUnifiedOrder,
|
||||
@@ -57,12 +57,20 @@ import {
|
||||
} from '@/modules/unified-orders/status'
|
||||
import {
|
||||
downloadWeightImage,
|
||||
calculateInboundFee,
|
||||
confirmInboundFee,
|
||||
createFeeActionIdempotencyKey,
|
||||
getInboundMeasurement,
|
||||
saveInboundMeasurementDraft,
|
||||
getLatestInboundFee,
|
||||
InboundFeePanel,
|
||||
InboundMeasurementDialog,
|
||||
MeasurementEvidenceThumbnail,
|
||||
sendInboundFee,
|
||||
type InboundMeasurementDetail,
|
||||
} from '@/modules/inbounds'
|
||||
import { listCustomerChargeRules, type CustomerChargeRule } from '@/modules/fba-pricing'
|
||||
import { getCustomerDimensionConfig, type CustomerDimensionConfig, type CustomerDimensionRule } from '@/modules/dimension-config'
|
||||
import {
|
||||
OrderBasicInfoIcon,
|
||||
OrderCargoInfoIcon,
|
||||
@@ -151,14 +159,44 @@ const measurementDetail = ref<InboundMeasurementDetail | null>(null)
|
||||
const measurementLoading = ref(false)
|
||||
const measurementError = ref<ApiErrorDetails | null>(null)
|
||||
const measurementLoadedKey = ref('')
|
||||
const measurementChargeRules = ref<CustomerChargeRule[]>([])
|
||||
const measurementChargeRulesLoading = ref(false)
|
||||
const measurementChargeRulesLoadedKey = ref('')
|
||||
const measurementDimensionConfig = ref<CustomerDimensionConfig | null>(null)
|
||||
const measurementDimensionConfigLoading = ref(false)
|
||||
const measurementDimensionConfigLoadedKey = ref('')
|
||||
const measurementStrategySavingId = ref('')
|
||||
const measurementOuterSavingId = ref('')
|
||||
const measurementPreviewOpen = ref(false)
|
||||
const measurementPreviewLoading = ref(false)
|
||||
const measurementPreviewError = ref<ApiErrorDetails | null>(null)
|
||||
const measurementPreviewUrl = ref('')
|
||||
const measurementPreviewTitle = ref('测量图片')
|
||||
const measurementCollapsed = reactive({ customer: false, outer: false, actual: false })
|
||||
type MeasurementSection = keyof typeof measurementCollapsed
|
||||
type MeasurementCalculationMode = 'ACTUAL' | 'ADD' | 'MULTIPLY'
|
||||
const measurementSectionModes = reactive<Record<MeasurementSection, MeasurementCalculationMode>>({
|
||||
customer: 'ACTUAL', outer: 'ACTUAL', actual: 'ACTUAL',
|
||||
})
|
||||
const cargoImagePreviewUrls = new Set<string>()
|
||||
const measurementDialogOpen = ref(false)
|
||||
const openWarehouseSuggestionOnMeasurementLoad = ref(false)
|
||||
const confirmFeeSending = ref(false)
|
||||
const feeConfirmOpen = ref(false)
|
||||
const feeConfirmWeight = ref<number | null>(null)
|
||||
const feeConfirmVolume = ref<number | null>(null)
|
||||
const feeConfirmRows = computed(() => {
|
||||
const summary = detail.value?.summary
|
||||
const measurement = summary?.measurement
|
||||
const actualWeight = Number(measurement?.actualGrossWeightKg)
|
||||
const actualVolume = Number(measurement?.actualVolumeCbm)
|
||||
const customerTotals = measurementTotals(customerMeasurementRows.value)
|
||||
return [
|
||||
{ source: '客户下单数据', count: summary?.totalPackageCount, weight: customerTotals.gross, volume: customerTotals.volume ?? summary?.expectedVolumeCbm },
|
||||
{ source: '外箱规数据', count: measurementTotals(outerMeasurementRows.value).count, weight: measurementTotals(outerMeasurementRows.value).gross, volume: measurementTotals(outerMeasurementRows.value).volume },
|
||||
{ source: '实际测量数据', count: measurementDetail.value?.actualPackageCount, weight: actualWeight, volume: actualVolume },
|
||||
]
|
||||
})
|
||||
const smartLoadingOpen = ref(false)
|
||||
const measurementOrder = ref<UnifiedOrderResource | null>(null)
|
||||
const loadingAssociation = ref<LoadingAssociationSummary | null>(null)
|
||||
@@ -213,8 +251,140 @@ const customerMeasurementColumns = [
|
||||
]
|
||||
const measuredColumns = [
|
||||
...customerMeasurementColumns.slice(0, 7), { key: 'girthCm', label: '箱规周长(cm)' }, { key: 'totalWeightKg', label: '总毛重(T.K.GW)' },
|
||||
{ key: 'totalVolumeCbm', label: '总体积(T.CBM)' }, { key: 'volumeWeightKg', label: '总材积' }, { key: 'remark', label: '备注' },
|
||||
{ key: 'totalVolumeCbm', label: '总体积(T.CBM)' }, { key: 'volumeWeightKg', label: '总材积' },
|
||||
{ key: 'remark', label: '备注' },
|
||||
]
|
||||
const outerColumns = measuredColumns
|
||||
const actualMeasuredColumns = measuredColumns.filter((column) => column.key !== 'itemNo')
|
||||
|
||||
function sortedDimensions(row: Record<string, unknown>) {
|
||||
const values = [Number(row.lengthCm), Number(row.widthCm), Number(row.heightCm)]
|
||||
return values.every((value) => Number.isFinite(value) && value > 0)
|
||||
? values.sort((left, right) => right - left)
|
||||
: null
|
||||
}
|
||||
|
||||
function matchingDimensionRule(row: Record<string, unknown>): CustomerDimensionRule | null {
|
||||
const measured = sortedDimensions(row)
|
||||
if (!measured) return null
|
||||
return [...(measurementDimensionConfig.value?.rules || [])]
|
||||
.sort((left, right) => (
|
||||
left.innerLengthCm * left.innerWidthCm * left.innerHeightCm
|
||||
- right.innerLengthCm * right.innerWidthCm * right.innerHeightCm
|
||||
))
|
||||
.find((rule) => {
|
||||
const inner = [rule.innerLengthCm, rule.innerWidthCm, rule.innerHeightCm].sort((left, right) => right - left)
|
||||
return measured.every((value, index) => value <= inner[index])
|
||||
}) || null
|
||||
}
|
||||
|
||||
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 (measurementDimensionConfigLoading.value) return '原尺寸 + 加载中…'
|
||||
return values.length > 1 ? '原尺寸 + 按箱规匹配' : '原尺寸 + 未匹配箱规'
|
||||
}
|
||||
|
||||
function multiplyCoefficientLabel() {
|
||||
const rule = measurementChargeRules.value.find((item) => item.defaultRule && item.status === 'ACTIVE')
|
||||
if (rule) return `原尺寸 × ${rule.lengthFactor}`
|
||||
return measurementChargeRulesLoading.value ? '原尺寸 × 加载中…' : '原尺寸 × 未启用系数'
|
||||
}
|
||||
|
||||
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,
|
||||
)].map((match) => ({
|
||||
dimensions: [Number(match[2]), Number(match[3]), Number(match[4])].sort((left, right) => right - left),
|
||||
coefficient: Number(match[5]),
|
||||
})).filter((tier) => tier.dimensions.every((value) => Number.isFinite(value) && value > 0)
|
||||
&& Number.isFinite(tier.coefficient) && tier.coefficient > 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]))
|
||||
return (matched || tiers[tiers.length - 1]).coefficient
|
||||
}
|
||||
|
||||
const activeMeasurementChargeRule = computed(() => measurementChargeRules.value
|
||||
.find((rule) => rule.defaultRule && rule.status === 'ACTIVE') || null)
|
||||
const multiplyModeDisabled = computed(() => !measurementChargeRulesLoading.value && !activeMeasurementChargeRule.value)
|
||||
|
||||
function roundedUp(value: number, digits: number) {
|
||||
const factor = 10 ** digits
|
||||
return Math.ceil((value - Number.EPSILON) * factor) / factor
|
||||
}
|
||||
|
||||
function calculatedMeasurementRow(
|
||||
source: Record<string, unknown>,
|
||||
mode: MeasurementCalculationMode,
|
||||
customerSource = false,
|
||||
) {
|
||||
const calculationSource = !customerSource && mode !== 'ACTUAL'
|
||||
? {
|
||||
...source,
|
||||
lengthCm: source.outerLengthCm ?? source.lengthCm,
|
||||
widthCm: source.outerWidthCm ?? source.widthCm,
|
||||
heightCm: source.outerHeightCm ?? source.heightCm,
|
||||
}
|
||||
: source
|
||||
const rule = matchingDimensionRule(calculationSource)
|
||||
const rawDimensions = [Number(calculationSource.lengthCm), Number(calculationSource.widthCm), Number(calculationSource.heightCm)]
|
||||
if (!rawDimensions.every((value) => Number.isFinite(value) && value > 0)) return { ...source }
|
||||
|
||||
const matchedDimensions = mode === 'ADD' && rule
|
||||
? rawDimensions.map((value) => value + rule.edgeAdditionCm)
|
||||
: rawDimensions
|
||||
const chargeRule = activeMeasurementChargeRule.value
|
||||
const specificationCoefficient = mode === 'MULTIPLY' && chargeRule
|
||||
? matchedSpecificationCoefficient(chargeRule, calculationSource)
|
||||
: 1
|
||||
const [lengthCm, widthCm, heightCm] = matchedDimensions.map((value) => roundedUp(value * specificationCoefficient, 3))
|
||||
const packageCount = Number(source.packageCount)
|
||||
const unitVolumeCbm = mode === 'ADD' && rule
|
||||
? 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 sourceUnitWeight = customerSource
|
||||
? Number(source.grossWeightKg) / packageCount
|
||||
: Number(source.unitWeightKg)
|
||||
const unitWeightKg = Number.isFinite(sourceUnitWeight) ? sourceUnitWeight : null
|
||||
const totalWeightKg = unitWeightKg != null && Number.isFinite(packageCount) ? unitWeightKg * packageCount : null
|
||||
return {
|
||||
...source,
|
||||
lengthCm, widthCm, heightCm, unitWeightKg, unitVolumeCbm, totalWeightKg, totalVolumeCbm,
|
||||
girthCm: roundedUp(lengthCm + 2 * (widthCm + heightCm), 3),
|
||||
volumeWeightKg: totalVolumeCbm == null ? null : roundedUp(totalVolumeCbm * 167, 3),
|
||||
remark: customerSource ? source.cargoRemark : source.remark,
|
||||
}
|
||||
}
|
||||
|
||||
const customerMeasurementRows = computed(() => (detail.value?.order.cargoes || []).map((row) => (
|
||||
calculatedMeasurementRow(row, measurementSectionModes.customer, true)
|
||||
)))
|
||||
const outerMeasurementRows = computed(() => (measurementDetail.value?.items || []).map((row) => {
|
||||
const source = row as unknown as Record<string, unknown>
|
||||
return calculatedMeasurementRow({
|
||||
...source,
|
||||
lengthCm: source.outerLengthCm ?? source.lengthCm,
|
||||
widthCm: source.outerWidthCm ?? source.widthCm,
|
||||
heightCm: source.outerHeightCm ?? source.heightCm,
|
||||
}, measurementSectionModes.outer)
|
||||
}))
|
||||
const actualMeasurementRows = computed(() => (measurementDetail.value?.items || []).map((row) => (
|
||||
calculatedMeasurementRow(row as unknown as Record<string, unknown>, measurementSectionModes.actual)
|
||||
)))
|
||||
|
||||
function updateMeasurementSectionMode(section: MeasurementSection, mode: MeasurementCalculationMode) {
|
||||
if (mode === 'MULTIPLY' && !activeMeasurementChargeRule.value) {
|
||||
ElMessage.warning('该客户尚未启用系数配置,请先到客户计费规则中设置默认规则')
|
||||
return
|
||||
}
|
||||
measurementSectionModes[section] = mode
|
||||
}
|
||||
|
||||
const statusTabs = computed(() => [
|
||||
{ value: '' as const, label: '全部' },
|
||||
@@ -529,7 +699,12 @@ function feeReviewLabel(value: string | null | undefined) {
|
||||
return unifiedOrderReviewStatusLabel(value)
|
||||
}
|
||||
|
||||
function measurementValue(row: Record<string, unknown>, key: string, customerSource = false) {
|
||||
function measurementValue(row: Record<string, unknown>, key: string, customerSource = false, outerSource = false) {
|
||||
if (key === 'coefficientMode') return measurementCalculationLabel(row)
|
||||
if (outerSource && ['lengthCm', 'widthCm', 'heightCm'].includes(key)) {
|
||||
const outerValue = row[`outer${key[0].toUpperCase()}${key.slice(1)}`]
|
||||
return outerValue == null ? '—' : formatNumber(Number(outerValue), 6)
|
||||
}
|
||||
if (customerSource && key === 'unitWeightKg') {
|
||||
const total = Number(row.grossWeightKg)
|
||||
const count = Number(row.packageCount)
|
||||
@@ -546,6 +721,191 @@ function measurementValue(row: Record<string, unknown>, key: string, customerSou
|
||||
return typeof value === 'number' ? formatNumber(value, 6) : String(value)
|
||||
}
|
||||
|
||||
function measurementCalculationLabel(row: Record<string, unknown>) {
|
||||
if (measurementCalculationModeForRow(row) === 'MULTIPLY') {
|
||||
return `乘以${measurementChargeRuleName(row.chargeRuleId)}`
|
||||
}
|
||||
if (measurementCalculationModeForRow(row) === 'ACTUAL') return '实测原尺寸(不调整)'
|
||||
return addCoefficientLabel(row).replace('原尺寸 + ', '加')
|
||||
}
|
||||
|
||||
function measurementCalculationModeForRow(row: Record<string, unknown>): MeasurementCalculationMode {
|
||||
if (row.coefficientMode === 'MULTIPLY') return 'MULTIPLY'
|
||||
if (row.coefficientMode === 'ADD') return 'ADD'
|
||||
const itemNo = normalizedIdentifier(row.itemNo)
|
||||
const measuredItem = measurementDetail.value?.items.find((item) => normalizedIdentifier(item.itemNo) === itemNo)
|
||||
return measuredItem?.coefficientMode === 'MULTIPLY' ? 'MULTIPLY' : measuredItem?.coefficientMode === 'ADD' ? 'ADD' : 'ACTUAL'
|
||||
}
|
||||
|
||||
async function updateMeasurementCalculationMode(row: Record<string, unknown> | null, mode: Exclude<MeasurementCalculationMode, 'ACTUAL'>) {
|
||||
const summary = detail.value?.summary
|
||||
const measurement = measurementDetail.value
|
||||
if (!summary?.inboundId || !measurement?.measurementId || measurement.version == null || !can('platform:firstleg:inbound:measure:edit')) return
|
||||
const itemId = normalizedIdentifier(row?.id)
|
||||
const itemNo = normalizedIdentifier(row?.itemNo)
|
||||
const targetIds = new Set(measurement.items
|
||||
.filter((item) => !row || (itemId ? String(item.id) === itemId : normalizedIdentifier(item.itemNo) === itemNo))
|
||||
.map((item) => String(item.id)))
|
||||
if (!targetIds.size) {
|
||||
ElMessage.warning('当前数据尚未关联实测明细,暂不能设置计算方式')
|
||||
return
|
||||
}
|
||||
measurementStrategySavingId.value = row ? (itemId || `item-${itemNo}`) : 'all'
|
||||
try {
|
||||
await Promise.all([loadMeasurementChargeRules(), loadMeasurementDimensionConfig()])
|
||||
const activeRule = measurementChargeRules.value.find((rule) => rule.defaultRule && rule.status === 'ACTIVE')
|
||||
const requiresMultiply = measurement.items.some((item) => (
|
||||
targetIds.has(String(item.id)) ? mode : item.coefficientMode
|
||||
) === 'MULTIPLY')
|
||||
if (requiresMultiply && !activeRule) {
|
||||
ElMessage.warning('该客户尚未启用系数配置,请先到客户计费规则中设置默认规则')
|
||||
return
|
||||
}
|
||||
await saveInboundMeasurementDraft(String(summary.tenantId), String(summary.inboundId), {
|
||||
measurementId: String(measurement.measurementId),
|
||||
version: measurement.version,
|
||||
actualGoodsName: measurement.actualGoodsName,
|
||||
items: measurement.items.map((item) => ({
|
||||
inboundDetailId: item.inboundDetailId,
|
||||
boxCode: item.boxCode,
|
||||
itemNo: item.itemNo,
|
||||
boxSpecification: item.boxSpecification,
|
||||
packageCount: item.packageCount,
|
||||
lengthCm: item.lengthCm,
|
||||
widthCm: item.widthCm,
|
||||
heightCm: item.heightCm,
|
||||
unitWeightKg: item.unitWeightKg,
|
||||
intelligentImageFileId: item.intelligentImageFileId,
|
||||
machineNo: item.machineNo,
|
||||
measuredAt: item.deviceMeasuredAt,
|
||||
overLimitWarning: item.overLimitWarning,
|
||||
outerLengthCm: item.outerLengthCm,
|
||||
outerWidthCm: item.outerWidthCm,
|
||||
outerHeightCm: item.outerHeightCm,
|
||||
chargeRuleId: (targetIds.has(String(item.id)) ? mode : item.coefficientMode) === 'MULTIPLY'
|
||||
? activeRule!.id
|
||||
: null,
|
||||
coefficientMode: targetIds.has(String(item.id)) ? mode : item.coefficientMode || 'ADD',
|
||||
remark: item.remark,
|
||||
})),
|
||||
})
|
||||
measurementLoadedKey.value = ''
|
||||
await loadMeasurementDetails()
|
||||
ElMessage.success(`${row ? '当前数据' : '全部测量数据'}的计算方式已更新,体积已重新计算`)
|
||||
} catch (error) {
|
||||
measurementLoadedKey.value = ''
|
||||
await loadMeasurementDetails()
|
||||
ElMessage.error(getApiErrorDetails(error, '计算方式更新失败').message)
|
||||
} finally {
|
||||
measurementStrategySavingId.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
async function saveWarehouseOuterDimensions(row: Record<string, unknown>) {
|
||||
const summary = detail.value?.summary
|
||||
const measurement = measurementDetail.value
|
||||
const editableMeasurementStatuses = new Set(['WAIT_MEASURE', 'MEASURING', 'MEASURE_PENDING', 'MEASURE_REJECTED'])
|
||||
const currentStatus = editableMeasurementStatuses.has(measurement?.processStatus || '')
|
||||
? measurement!.processStatus
|
||||
: summary?.mainStatus || measurement?.processStatus || ''
|
||||
if (measurement && !editableMeasurementStatuses.has(currentStatus)) {
|
||||
const statusLabel = orderStatusLabel(summary?.mainStatus) || measurementStatusLabel(currentStatus)
|
||||
ElMessage.warning(`当前状态为“${statusLabel}”,该状态不可以修改测量数据`)
|
||||
return
|
||||
}
|
||||
if (!summary?.inboundId || !measurement?.measurementId || measurement.version == null
|
||||
|| !can('platform:firstleg:inbound:measure:edit')) return
|
||||
const dimensions = [row.outerLengthCm, row.outerWidthCm, row.outerHeightCm].map(Number)
|
||||
if (dimensions.some((value) => !Number.isFinite(value) || value <= 0)) {
|
||||
ElMessage.warning('仓库修改外箱长、宽、高必须同时填写且大于 0')
|
||||
return
|
||||
}
|
||||
measurementOuterSavingId.value = String(row.id)
|
||||
try {
|
||||
await saveInboundMeasurementDraft(String(summary.tenantId), String(summary.inboundId), {
|
||||
measurementId: String(measurement.measurementId),
|
||||
version: measurement.version,
|
||||
actualGoodsName: measurement.actualGoodsName,
|
||||
items: measurement.items.map((item) => ({
|
||||
inboundDetailId: item.inboundDetailId,
|
||||
boxCode: item.boxCode,
|
||||
itemNo: item.itemNo,
|
||||
boxSpecification: item.boxSpecification,
|
||||
packageCount: item.packageCount,
|
||||
lengthCm: item.lengthCm,
|
||||
widthCm: item.widthCm,
|
||||
heightCm: item.heightCm,
|
||||
unitWeightKg: item.unitWeightKg,
|
||||
intelligentImageFileId: item.intelligentImageFileId,
|
||||
machineNo: item.machineNo,
|
||||
measuredAt: item.deviceMeasuredAt,
|
||||
overLimitWarning: item.overLimitWarning,
|
||||
outerLengthCm: String(item.id) === String(row.id) ? dimensions[0] : item.outerLengthCm,
|
||||
outerWidthCm: String(item.id) === String(row.id) ? dimensions[1] : item.outerWidthCm,
|
||||
outerHeightCm: String(item.id) === String(row.id) ? dimensions[2] : item.outerHeightCm,
|
||||
chargeRuleId: item.chargeRuleId,
|
||||
coefficientMode: item.coefficientMode || 'ADD',
|
||||
remark: item.remark,
|
||||
})),
|
||||
})
|
||||
measurementLoadedKey.value = ''
|
||||
await loadMeasurementDetails()
|
||||
ElMessage.success(`序号 ${row.itemNo || '—'} 的外箱规已更新,相关数值已重新计算`)
|
||||
} catch (error) {
|
||||
measurementLoadedKey.value = ''
|
||||
await loadMeasurementDetails()
|
||||
ElMessage.error(getApiErrorDetails(error, '仓库外箱尺寸保存失败').message)
|
||||
} finally {
|
||||
measurementOuterSavingId.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
function measurementChargeRuleName(ruleId: unknown) {
|
||||
const normalizedRuleId = normalizedIdentifier(ruleId)
|
||||
const selected = normalizedRuleId
|
||||
? measurementChargeRules.value.find((rule) => rule.id === normalizedRuleId)
|
||||
: null
|
||||
const fallback = measurementChargeRules.value.find((rule) => rule.defaultRule && rule.status === 'ACTIVE')
|
||||
|| measurementChargeRules.value.find((rule) => rule.status === 'ACTIVE')
|
||||
const rule = selected || fallback
|
||||
if (rule) {
|
||||
const remark = rule.remark?.trim()
|
||||
return `系数 ${rule.lengthFactor}${remark ? ` · ${remark}` : ''}${rule.defaultRule ? '(默认)' : ''}`
|
||||
}
|
||||
if (measurementChargeRulesLoading.value) return '加载中…'
|
||||
return normalizedRuleId || '未配置'
|
||||
}
|
||||
|
||||
async function loadMeasurementChargeRules() {
|
||||
const tenantId = normalizedIdentifier(detail.value?.summary.tenantId)
|
||||
const customerId = normalizedIdentifier(detail.value?.summary.customerId) || tenantId
|
||||
const loadedKey = `${tenantId}:${customerId}`
|
||||
if (!customerId || measurementChargeRulesLoadedKey.value === loadedKey || measurementChargeRulesLoading.value) return
|
||||
measurementChargeRulesLoading.value = true
|
||||
try {
|
||||
measurementChargeRules.value = await listCustomerChargeRules(customerId, tenantId)
|
||||
measurementChargeRulesLoadedKey.value = loadedKey
|
||||
} catch {
|
||||
measurementChargeRules.value = []
|
||||
} finally {
|
||||
measurementChargeRulesLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMeasurementDimensionConfig() {
|
||||
const tenantId = normalizedIdentifier(detail.value?.summary.tenantId)
|
||||
if (!tenantId || measurementDimensionConfigLoadedKey.value === tenantId || measurementDimensionConfigLoading.value) return
|
||||
measurementDimensionConfigLoading.value = true
|
||||
try {
|
||||
measurementDimensionConfig.value = await getCustomerDimensionConfig(tenantId)
|
||||
measurementDimensionConfigLoadedKey.value = tenantId
|
||||
} catch {
|
||||
measurementDimensionConfig.value = null
|
||||
} finally {
|
||||
measurementDimensionConfigLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
interface MeasurementTotals {
|
||||
count: number
|
||||
gross: number
|
||||
@@ -608,8 +968,16 @@ async function loadMeasurementDetails() {
|
||||
measurementLoading.value = true
|
||||
measurementError.value = null
|
||||
try {
|
||||
const response = await getInboundMeasurement(String(summary.tenantId), String(summary.inboundId))
|
||||
const [response] = await Promise.all([
|
||||
getInboundMeasurement(String(summary.tenantId), String(summary.inboundId)),
|
||||
loadMeasurementChargeRules(),
|
||||
loadMeasurementDimensionConfig(),
|
||||
])
|
||||
if (serial !== measurementRequestSerial) return
|
||||
const configuredMode = measurementDimensionConfig.value?.defaultCalculationMode
|
||||
measurementSectionModes.outer = configuredMode === 'MULTIPLY' && activeMeasurementChargeRule.value
|
||||
? 'MULTIPLY'
|
||||
: 'ADD'
|
||||
measurementDetail.value = response
|
||||
measurementLoadedKey.value = key
|
||||
} catch (error) {
|
||||
@@ -837,6 +1205,64 @@ function openMeasurement(row: UnifiedOrderResource) {
|
||||
measurementDialogOpen.value = true
|
||||
}
|
||||
|
||||
function openFeeConfirmDialog() {
|
||||
const summary = detail.value?.summary
|
||||
const reviewStatus = String(summary?.measurement?.reviewStatus ?? '').trim().toUpperCase()
|
||||
if (reviewStatus !== 'APPROVED') {
|
||||
ElMessage.warning('测量审核通过后才能确认并发送费用')
|
||||
return
|
||||
}
|
||||
if (!summary?.inboundId) {
|
||||
ElMessage.warning('当前单据缺少入仓信息,暂不能发送费用')
|
||||
return
|
||||
}
|
||||
if (confirmFeeSending.value) {
|
||||
return
|
||||
}
|
||||
feeConfirmWeight.value = Number(summary.measurement?.actualGrossWeightKg) || null
|
||||
feeConfirmVolume.value = Number(summary.measurement?.actualVolumeCbm) || null
|
||||
feeConfirmOpen.value = true
|
||||
}
|
||||
|
||||
async function executeConfirmAndSendFee() {
|
||||
const summary = detail.value?.summary
|
||||
if (!summary?.inboundId) return
|
||||
const selectedWeightKg = Number(feeConfirmWeight.value)
|
||||
const selectedVolumeCbm = Number(feeConfirmVolume.value)
|
||||
if (!(selectedWeightKg > 0) || !(selectedVolumeCbm > 0)) {
|
||||
ElMessage.warning('计费毛重和计费体积必须大于 0')
|
||||
return
|
||||
}
|
||||
confirmFeeSending.value = true
|
||||
try {
|
||||
const tenantId = String(summary.tenantId)
|
||||
const inboundId = String(summary.inboundId)
|
||||
let snapshot = await getLatestInboundFee(tenantId, inboundId)
|
||||
if (snapshot.feeStatus === 'CALCULATED') {
|
||||
snapshot = await calculateInboundFee(tenantId, inboundId, {
|
||||
selectedWeightKg,
|
||||
selectedVolumeCbm,
|
||||
weightSource: 'ACTUAL',
|
||||
volumeSource: 'ACTUAL',
|
||||
}, createFeeActionIdempotencyKey('calculate'))
|
||||
snapshot = await confirmInboundFee(tenantId, inboundId, createFeeActionIdempotencyKey('confirm'))
|
||||
}
|
||||
if (snapshot.feeStatus === 'CONFIRMED') {
|
||||
await sendInboundFee(tenantId, inboundId, createFeeActionIdempotencyKey('send'))
|
||||
ElMessage.success('费用已确认并发送给客户')
|
||||
feeConfirmOpen.value = false
|
||||
await loadDetail(true)
|
||||
return
|
||||
}
|
||||
if (snapshot.feeStatus === 'SENT') ElMessage.info('该费用已经发送给客户,无需重复发送')
|
||||
else ElMessage.warning('当前没有可发送的试算费用,请先在费用信息中完成费用试算')
|
||||
} catch (error) {
|
||||
ElMessage.error(getApiErrorDetails(error, '确认费用发送失败').message)
|
||||
} finally {
|
||||
confirmFeeSending.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleOrderSelectionChange(rows: UnifiedOrderResource[]) {
|
||||
selectedOrders.value = rows
|
||||
selectedApprovalOrders.value = rows.filter(canApprove)
|
||||
@@ -1021,6 +1447,7 @@ function handleMeasurementSubmitted() {
|
||||
|
||||
function clearMeasurementOrder() {
|
||||
measurementOrder.value = null
|
||||
openWarehouseSuggestionOnMeasurementLoad.value = false
|
||||
}
|
||||
|
||||
type UnifiedOrderRowAction = 'note' | 'approve' | 'measurement'
|
||||
@@ -1163,6 +1590,17 @@ async function loadDetail(force = false) {
|
||||
measurementDetail.value = null
|
||||
measurementError.value = null
|
||||
measurementLoadedKey.value = ''
|
||||
measurementSectionModes.customer = 'ACTUAL'
|
||||
measurementSectionModes.outer = 'ACTUAL'
|
||||
measurementSectionModes.actual = 'ACTUAL'
|
||||
if (measurementChargeRulesLoadedKey.value !== tenantId) {
|
||||
measurementChargeRules.value = []
|
||||
measurementChargeRulesLoadedKey.value = ''
|
||||
}
|
||||
if (measurementDimensionConfigLoadedKey.value !== tenantId) {
|
||||
measurementDimensionConfig.value = null
|
||||
measurementDimensionConfigLoadedKey.value = ''
|
||||
}
|
||||
measurementPreviewOpen.value = false
|
||||
releaseMeasurementPreview()
|
||||
releaseCargoImagePreviews()
|
||||
@@ -1226,6 +1664,22 @@ onBeforeUnmount(() => {
|
||||
<template>
|
||||
<AdminPage>
|
||||
<section class="business-page unified-orders-page">
|
||||
<AppDialog v-model="feeConfirmOpen" title="编辑计费数据" confirm-text="确认" :confirm-loading="confirmFeeSending" width="min(920px, 94vw)" :teleported="false" @confirm="executeConfirmAndSendFee">
|
||||
<el-form label-position="top" class="fee-confirm-form">
|
||||
<el-form-item label="入仓码"><el-input :model-value="detail?.summary.inboundCode || '—'" disabled /></el-form-item>
|
||||
<el-form-item label="渠道"><el-input :model-value="detail?.summary.channelName || detail?.summary.channelCode || '—'" disabled /></el-form-item>
|
||||
<el-form-item label="计费毛重"><div class="fee-confirm-number-wrap"><el-input-number v-model="feeConfirmWeight" :min="0" :precision="3" :controls="false" class="fee-confirm-number" /><span class="fee-confirm-unit">kg</span></div></el-form-item>
|
||||
<el-form-item label="计费体积"><div class="fee-confirm-number-wrap"><el-input-number v-model="feeConfirmVolume" :min="0" :precision="6" :controls="false" class="fee-confirm-number" /><span class="fee-confirm-unit">m³</span></div></el-form-item>
|
||||
</el-form>
|
||||
<h4 class="fee-confirm-reference-title">参考数据</h4>
|
||||
<el-table :data="feeConfirmRows" border size="small">
|
||||
<el-table-column prop="source" label="数据来源" />
|
||||
<el-table-column prop="count" label="件数" />
|
||||
<el-table-column label="总毛重(T.K.GW)"><template #default="{ row }">{{ row.weight == null ? '—' : formatNumber(row.weight, 2) }}</template></el-table-column>
|
||||
<el-table-column label="总体积(m³)"><template #default="{ row }">{{ row.volume == null ? '—' : formatNumber(row.volume, 4) }}</template></el-table-column>
|
||||
<el-table-column label="总材积重"><template #default="{ row }">{{ row.volume == null ? '—' : formatNumber(Number(row.volume) * 167, 2) }}</template></el-table-column>
|
||||
</el-table>
|
||||
</AppDialog>
|
||||
<template v-if="isDetailRoute">
|
||||
<DetailPageLayout ariaLabel="统一订单详情">
|
||||
<header class="order-detail-header">
|
||||
@@ -1420,7 +1874,19 @@ onBeforeUnmount(() => {
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="activeDetailTab === 'measurement'">
|
||||
<div class="panel-title"><div><h2>测量数据</h2><p>{{ detail.summary.measurement?.completedAt ? `完成于 ${formatDate(detail.summary.measurement.completedAt)}` : '尚未完成测量' }}</p></div><Ruler :size="19" /></div>
|
||||
<div class="panel-title">
|
||||
<div><h2>测量数据</h2><p>{{ detail.summary.measurement?.completedAt ? `完成于 ${formatDate(detail.summary.measurement.completedAt)}` : '尚未完成测量' }}</p></div>
|
||||
<div class="measurement-title-actions">
|
||||
<el-button
|
||||
v-if="can('firstleg:inbound:confirm-send-fee')"
|
||||
type="primary"
|
||||
:loading="confirmFeeSending"
|
||||
:title="detail.summary.measurement?.reviewStatus === 'APPROVED' ? '确认并发送费用' : '测量审核通过后才能发送费用'"
|
||||
data-testid="confirm-send-fee-button"
|
||||
@click="openFeeConfirmDialog"
|
||||
><Send :size="15" />确认费用发送</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-alert v-if="!can('platform:firstleg:inbound:measure:view')" title="当前账号缺少测量查看权限" type="warning" :closable="false" show-icon />
|
||||
<LoadErrorState v-else-if="measurementError" title="测量数据暂时无法加载" :message="measurementError.message" :loading="measurementLoading" @retry="loadMeasurementDetails">
|
||||
<template #details><ApiErrorMeta :details="measurementError" /></template>
|
||||
@@ -1428,28 +1894,50 @@ onBeforeUnmount(() => {
|
||||
</LoadErrorState>
|
||||
<template v-else>
|
||||
<section class="measurement-section">
|
||||
<button type="button" class="measurement-section-title" :aria-expanded="!measurementCollapsed.customer" @click="toggleMeasurementSection('customer')"><h3>一、客户下单数据</h3><span class="measurement-section-summary"><span>总件数:{{ measurementTotals(detail.order.cargoes, true).count ?? '—' }}</span><span>总毛重:{{ formatNumber(measurementTotals(detail.order.cargoes, true).gross) }}</span><span>总体积:{{ formatNumber(measurementTotals(detail.order.cargoes, true).volume, 6) }}</span><span>总材积重:{{ formatNumber(measurementTotals(detail.order.cargoes, true).volumetric) }}</span><span>重量体积:{{ formatNumber(measurementTotals(detail.order.cargoes, true).weightVolume, 6) }}</span></span><ChevronUp v-if="!measurementCollapsed.customer" :size="18" /><ChevronDown v-else :size="18" /></button>
|
||||
<div class="measurement-section-title"><button type="button" class="measurement-section-toggle" :aria-expanded="!measurementCollapsed.customer" @click="toggleMeasurementSection('customer')"><h3>一、客户下单数据</h3><span class="measurement-section-summary"><span>总件数:{{ measurementTotals(customerMeasurementRows).count ?? '—' }}</span><span>总毛重:{{ formatNumber(measurementTotals(customerMeasurementRows).gross) }}</span><span>总体积:{{ formatNumber(measurementTotals(customerMeasurementRows).volume, 6) }}</span><span>总材积重:{{ formatNumber(measurementTotals(customerMeasurementRows).volumetric) }}</span><span>重量体积:{{ formatNumber(measurementTotals(customerMeasurementRows).weightVolume, 6) }}</span></span><ChevronUp v-if="!measurementCollapsed.customer" :size="18" /><ChevronDown v-else :size="18" /></button><el-select class="measurement-section-mode" :model-value="measurementSectionModes.customer" aria-label="客户下单数据计算方式" @change="updateMeasurementSectionMode('customer', $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.customer">
|
||||
<div class="detail-table-wrap"><el-table :data="detail.order.cargoes" border stripe empty-text="暂无客户下单数据"><el-table-column v-for="column in customerMeasurementColumns" :key="column.key" :label="column.label" :prop="column.key" min-width="120"><template #default="{ row }"><LineClampText v-if="column.key === 'remark'" :content="measurementValue(row as Record<string, unknown>, column.key, true)" /><template v-else>{{ measurementValue(row as Record<string, unknown>, column.key, true) }}</template></template></el-table-column></el-table></div>
|
||||
<div class="measurement-total"><strong>合计</strong><span>箱数:{{ measurementTotals(detail.order.cargoes, true).count ?? '—' }}</span><span>总毛重:{{ formatNumber(measurementTotals(detail.order.cargoes, true).gross) }}</span><span>总体积:{{ formatNumber(measurementTotals(detail.order.cargoes, true).volume, 6) }}</span><span>总材积:{{ formatNumber(measurementTotals(detail.order.cargoes, true).volumetric) }}</span></div>
|
||||
<div class="detail-table-wrap"><el-table :data="customerMeasurementRows" border stripe empty-text="暂无客户下单数据"><el-table-column v-for="column in customerMeasurementColumns" :key="column.key" :label="column.label" :prop="column.key" min-width="120"><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(customerMeasurementRows).count ?? '—' }}</span><span>总毛重:{{ formatNumber(measurementTotals(customerMeasurementRows).gross) }}</span><span>总体积:{{ formatNumber(measurementTotals(customerMeasurementRows).volume, 6) }}</span><span>总材积:{{ formatNumber(measurementTotals(customerMeasurementRows).volumetric) }}</span></div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="measurement-section">
|
||||
<button type="button" class="measurement-section-title" :aria-expanded="!measurementCollapsed.outer" @click="toggleMeasurementSection('outer')"><h3>二、外箱规数据</h3><span class="measurement-section-summary"><span>总件数:{{ measurementTotals((measurementDetail?.items || []) as unknown as Record<string, unknown>[]).count ?? '—' }}</span><span>总毛重:{{ formatNumber(measurementTotals((measurementDetail?.items || []) as unknown as Record<string, unknown>[]).gross) }}</span><span>总体积:{{ formatNumber(measurementTotals((measurementDetail?.items || []) as unknown as Record<string, unknown>[]).volume, 6) }}</span><span>总材积重:{{ formatNumber(measurementTotals((measurementDetail?.items || []) as unknown as Record<string, unknown>[]).volumetric) }}</span><span>重量体积:{{ formatNumber(measurementTotals((measurementDetail?.items || []) as unknown as Record<string, unknown>[]).weightVolume, 6) }}</span></span><ChevronUp v-if="!measurementCollapsed.outer" :size="18" /><ChevronDown v-else :size="18" /></button>
|
||||
<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 v-if="!measurementCollapsed.outer">
|
||||
<div v-loading="measurementLoading" class="detail-table-wrap"><el-table :data="measurementDetail?.items || []" border stripe empty-text="暂无外箱规数据"><el-table-column v-for="column in measuredColumns" :key="column.key" :label="column.label" :prop="column.key" min-width="120"><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((measurementDetail?.items || []) as unknown as Record<string, unknown>[]).count ?? '—' }}</span><span>总毛重:{{ formatNumber(measurementTotals((measurementDetail?.items || []) as unknown as Record<string, unknown>[]).gross) }}</span><span>总体积:{{ formatNumber(measurementTotals((measurementDetail?.items || []) as unknown as Record<string, unknown>[]).volume, 6) }}</span><span>总材积:{{ formatNumber(measurementTotals((measurementDetail?.items || []) as unknown as Record<string, unknown>[]).volumetric) }}</span></div>
|
||||
<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>
|
||||
</div>
|
||||
</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-option label="实测原尺寸(不调整)" value="ACTUAL" />
|
||||
<el-option label="箱规尺寸" value="ADD" />
|
||||
<el-option label="箱规系数" value="MULTIPLY" :disabled="multiplyModeDisabled" />
|
||||
</el-select>
|
||||
<span class="measurement-rule-selector__hint">规则参数来源:客户管理</span>
|
||||
</div>
|
||||
<section class="measurement-section">
|
||||
<button type="button" class="measurement-section-title" :aria-expanded="!measurementCollapsed.actual" @click="toggleMeasurementSection('actual')"><h3>三、实际测量数据</h3><span class="measurement-section-summary"><span>总件数:{{ measurementDetail?.actualPackageCount ?? '—' }}</span><span>总毛重:{{ formatNumber(measurementDetail?.actualGrossWeightKg) }}</span><span>总体积:{{ formatNumber(measurementDetail?.actualVolumeCbm, 6) }}</span><span>总材积重:{{ formatNumber(measurementDetail?.volumeWeightKg) }}</span><span>重量体积:{{ formatNumber(measurementDetail?.actualVolumeCbm, 6) }}</span></span><ChevronUp v-if="!measurementCollapsed.actual" :size="18" /><ChevronDown v-else :size="18" /></button>
|
||||
<div class="measurement-section-title"><button type="button" class="measurement-section-toggle" :aria-expanded="!measurementCollapsed.actual" @click="toggleMeasurementSection('actual')"><h3>三、实际测量数据</h3><span class="measurement-section-summary"><span>总件数:{{ measurementTotals(actualMeasurementRows).count ?? '—' }}</span><span>总毛重:{{ formatNumber(measurementTotals(actualMeasurementRows).gross) }}</span><span>总体积:{{ formatNumber(measurementTotals(actualMeasurementRows).volume, 6) }}</span><span>总材积重:{{ formatNumber(measurementTotals(actualMeasurementRows).volumetric) }}</span><span>重量体积:{{ formatNumber(measurementTotals(actualMeasurementRows).weightVolume, 6) }}</span></span><ChevronUp v-if="!measurementCollapsed.actual" :size="18" /><ChevronDown v-else :size="18" /></button><el-select class="measurement-section-mode" :model-value="measurementSectionModes.actual" aria-label="实际测量数据计算方式" @change="updateMeasurementSectionMode('actual', $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.actual">
|
||||
<div v-loading="measurementLoading" class="detail-table-wrap">
|
||||
<el-table :data="measurementDetail?.items || []" row-key="id" border stripe empty-text="暂无实际测量数据">
|
||||
<el-table :data="actualMeasurementRows" row-key="id" border stripe empty-text="暂无实际测量数据">
|
||||
<el-table-column label="箱号" prop="boxCode" min-width="190" fixed="left" show-overflow-tooltip>
|
||||
<template #default="{ row }"><strong class="measurement-box-code">{{ row.boxCode || '—' }}</strong></template>
|
||||
</el-table-column>
|
||||
<el-table-column v-for="column in measuredColumns" :key="column.key" :label="column.label" :prop="column.key" min-width="120">
|
||||
<el-table-column label="#" prop="itemNo" min-width="70" fixed="left" />
|
||||
<el-table-column label="仓库修改外箱(长)" min-width="160">
|
||||
<template #default="{ row }"><el-input-number v-if="can('platform:firstleg:inbound:measure:edit')" v-model="row.outerLengthCm" aria-label="仓库修改外箱长" :min="0.001" :precision="3" :controls="false" placeholder="请输入" /><template v-else>{{ formatNumber(row.outerLengthCm) }}</template></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="仓库修改外箱(宽)" min-width="160">
|
||||
<template #default="{ row }"><el-input-number v-if="can('platform:firstleg:inbound:measure:edit')" v-model="row.outerWidthCm" aria-label="仓库修改外箱宽" :min="0.001" :precision="3" :controls="false" placeholder="请输入" /><template v-else>{{ formatNumber(row.outerWidthCm) }}</template></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="仓库修改外箱(高)" min-width="160">
|
||||
<template #default="{ row }"><el-input-number v-if="can('platform:firstleg:inbound:measure:edit')" v-model="row.outerHeightCm" aria-label="仓库修改外箱高" :min="0.001" :precision="3" :controls="false" placeholder="请输入" /><template v-else>{{ formatNumber(row.outerHeightCm) }}</template></template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="can('platform:firstleg:inbound:measure:edit')" label="仓库外箱操作" min-width="120">
|
||||
<template #default="{ row }"><el-button type="primary" plain size="small" :loading="measurementOuterSavingId === String(row.id)" :disabled="Boolean(measurementOuterSavingId)" @click="saveWarehouseOuterDimensions(row as Record<string, unknown>)">保存外箱</el-button></template>
|
||||
</el-table-column>
|
||||
<el-table-column v-for="column in actualMeasuredColumns" :key="column.key" :label="column.label" :prop="column.key" min-width="120">
|
||||
<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-column label="尺寸图片" min-width="148">
|
||||
@@ -1466,7 +1954,7 @@ onBeforeUnmount(() => {
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="measurement-total"><strong>合计</strong><span>箱数:{{ measurementDetail?.actualPackageCount ?? '—' }}</span><span>总毛重:{{ formatNumber(measurementDetail?.actualGrossWeightKg) }}</span><span>总体积:{{ formatNumber(measurementDetail?.actualVolumeCbm, 6) }}</span><span>总材积:{{ formatNumber(measurementDetail?.volumeWeightKg) }}</span></div>
|
||||
<div class="measurement-total"><strong>合计</strong><span>箱数:{{ measurementTotals(actualMeasurementRows).count ?? '—' }}</span><span>总毛重:{{ formatNumber(measurementTotals(actualMeasurementRows).gross) }}</span><span>总体积:{{ formatNumber(measurementTotals(actualMeasurementRows).volume, 6) }}</span><span>总材积:{{ formatNumber(measurementTotals(actualMeasurementRows).volumetric) }}</span></div>
|
||||
<section class="weight-evidence" aria-label="称重图片与服务端确认重量">
|
||||
<div class="weight-evidence__heading">
|
||||
<div><h4>称重图片与确认重量</h4><p>称重图片独立展示;重量值直接采用服务端确认结果。</p></div>
|
||||
@@ -1670,6 +2158,7 @@ onBeforeUnmount(() => {
|
||||
:customer-id="String(measurementOrder.tenantId)"
|
||||
:inbound-id="String(measurementOrder.inboundId)"
|
||||
:inbound-code="measurementOrder.inboundCode"
|
||||
:open-warehouse-suggestion="openWarehouseSuggestionOnMeasurementLoad"
|
||||
@updated="handleMeasurementUpdated"
|
||||
@submitted="handleMeasurementSubmitted"
|
||||
@closed="clearMeasurementOrder"
|
||||
@@ -1765,6 +2254,15 @@ onBeforeUnmount(() => {
|
||||
.panel-title h2 { margin: 0; color: #344054; font-size: 16px; }
|
||||
.panel-title p { margin: 5px 0 0; color: var(--cc-muted); font-size: 11px; }
|
||||
.panel-title > svg { color: var(--cc-primary); }
|
||||
.measurement-title-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px; }
|
||||
.measurement-title-actions :deep(.el-button) { margin-left: 0; }
|
||||
.fee-confirm-form { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 22px; }
|
||||
.fee-confirm-number-wrap { position: relative; width: 100%; }
|
||||
.fee-confirm-number { width: 100%; }
|
||||
.fee-confirm-number :deep(.el-input__wrapper) { padding-right: 42px; }
|
||||
.fee-confirm-unit { position: absolute; top: 50%; right: 12px; z-index: 1; transform: translateY(-50%); color: var(--cc-muted); pointer-events: none; }
|
||||
.fee-confirm-reference-title { margin: 8px 0 12px; color: var(--cc-text); font-size: 14px; }
|
||||
@media (max-width: 680px) { .fee-confirm-form { grid-template-columns: 1fr; } .fee-confirm-unit { right: 12px; } }
|
||||
.fee-section-title { margin: 14px 0 8px; color: #344054; font-size: 13px; line-height: 1.4; }
|
||||
.fee-overview { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); margin: 8px 0 0; border: 1px solid var(--cc-border); }
|
||||
.fee-overview > div { display: grid; grid-template-columns: auto 1fr; align-items: baseline; gap: 4px 12px; padding: 10px 12px; }
|
||||
@@ -1783,13 +2281,19 @@ onBeforeUnmount(() => {
|
||||
.cargo-image { width: 72px; height: 56px; border: 1px solid var(--cc-border); border-radius: 6px; background: #f8fafc; }
|
||||
.measurement-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 1px; margin: 0; border: 1px solid var(--cc-border); background: var(--cc-border); }
|
||||
.measurement-section { margin-top: 18px; }
|
||||
.measurement-section-title { display: flex; width: 100%; min-height: 48px; align-items: center; gap: 16px; padding: 9px 14px; border: 1px solid var(--cc-border); border-radius: 7px 7px 0 0; background: #f8fafc; color: var(--cc-text); font: inherit; text-align: left; cursor: pointer; }
|
||||
.measurement-section-title { display: flex; width: 100%; min-height: 48px; align-items: center; gap: 12px; padding: 7px 12px; border: 1px solid var(--cc-border); border-radius: 7px 7px 0 0; background: #f8fafc; color: var(--cc-text); }
|
||||
.measurement-section-title:hover { background: #f2f6fb; }
|
||||
.measurement-section-title:focus-visible { outline: 2px solid var(--cc-primary); outline-offset: 2px; }
|
||||
.measurement-section-title[aria-expanded='false'] { border-radius: 7px; }
|
||||
.measurement-section-title h3 { flex: 0 0 auto; margin: 0; color: var(--cc-text); font-size: 14px; }
|
||||
.measurement-section-summary { display: flex; min-width: 0; flex: 1; align-items: center; justify-content: flex-end; gap: 18px; color: var(--cc-muted); font-size: 12px; white-space: nowrap; }
|
||||
.measurement-section-title > svg { flex: 0 0 auto; color: var(--cc-muted); }
|
||||
.measurement-section-toggle { display: flex; min-width: 0; flex: 1; align-items: center; gap: 16px; padding: 2px; border: 0; background: transparent; color: inherit; font: inherit; text-align: left; cursor: pointer; }
|
||||
.measurement-section-toggle:focus-visible { outline: 2px solid var(--cc-primary); outline-offset: 2px; }
|
||||
.measurement-section-toggle > svg { flex: 0 0 auto; color: var(--cc-muted); }
|
||||
.measurement-section-mode { flex: 0 0 190px; }
|
||||
.measurement-section-title > .measurement-section-mode { display: none; }
|
||||
.measurement-rule-selector { display: flex; align-items: center; gap: 14px; margin: 8px 0 12px; padding: 12px 16px; border: 1px solid var(--cc-border); border-radius: 7px; background: #f8fafc; }
|
||||
.measurement-rule-selector__label { color: var(--cc-text); font-size: 12px; font-weight: 650; white-space: nowrap; }
|
||||
.measurement-rule-selector__control { width: 270px; }
|
||||
.measurement-rule-selector__hint { color: var(--cc-muted); font-size: 11px; }
|
||||
.measurement-total { display: flex; min-height: 42px; align-items: center; justify-content: flex-end; gap: 24px; padding: 9px 14px; border: 1px solid var(--cc-border); background: #f8fafc; color: var(--cc-muted); font-size: 12px; }
|
||||
.measurement-total { border-top: 0; border-radius: 0 0 7px 7px; }
|
||||
.measurement-total strong { margin-right: auto; color: var(--cc-text); font-size: 13px; }
|
||||
@@ -1824,5 +2328,6 @@ onBeforeUnmount(() => {
|
||||
@media (max-width: 720px) { .page-intro { align-items: stretch; flex-direction: column; } .page-intro :deep(.el-button) { width: 100%; min-height: 44px; } .status-tab { min-height: 46px; padding: 0 14px; } .table-actions :deep(.el-button) { width: 100%; min-height: 40px; } .row-actions { flex-wrap: wrap; } .order-detail-header { align-items: stretch; flex-direction: column; } .order-detail-heading { display: grid; grid-template-columns: auto 36px minmax(0, 1fr) auto; } .detail-header-actions { display: grid; grid-template-columns: 1fr; } .detail-header-actions :deep(.el-button) { min-height: 40px; } .detail-overview, .order-info-grid, .measurement-grid { grid-template-columns: 1fr; } .detail-overview article { border-right: 0; border-bottom: 1px solid var(--cc-border); } .detail-overview article:last-child { border-bottom: 0; } .order-info-grid > div.wide { grid-column: auto; } .detail-tabs button { padding: 0 18px; } .detail-panel { padding: 16px; } .measurement-section-title { align-items: flex-start; flex-wrap: wrap; padding: 12px; } .measurement-section-summary { width: 100%; flex-basis: calc(100% - 34px); flex-wrap: wrap; justify-content: flex-start; gap: 6px 16px; white-space: normal; } .measurement-section-title > svg { margin-left: auto; } .measurement-total { flex-wrap: wrap; justify-content: flex-start; gap: 6px 16px; } .measurement-total strong { width: 100%; } }
|
||||
@media (max-width: 720px) { .weight-evidence__heading { flex-direction: column; } .weight-evidence__heading dl { text-align: left; } }
|
||||
@media (max-width: 720px) { .fee-overview { grid-template-columns: 1fr; } .fee-overview > div + div { border-top: 1px solid var(--cc-border); border-left: 0; } }
|
||||
@media (max-width: 720px) { .measurement-section-title { align-items: stretch; flex-direction: column; } .measurement-section-toggle { align-items: flex-start; flex-wrap: wrap; } .measurement-section-toggle > svg { margin-left: auto; } .measurement-section-mode { width: 100%; flex-basis: auto; } }
|
||||
@media (max-width: 420px) { .order-detail-heading { grid-template-columns: auto minmax(0, 1fr) auto; } .order-mark { display: none; } .weight-evidence { padding: 10px; } .weight-evidence-grid { grid-template-columns: 1fr; } }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user