优化
This commit is contained in:
@@ -112,6 +112,19 @@ describe('inbound weight OCR API', () => {
|
||||
{ version: 1 },
|
||||
{ params: { tenantId: '10001' } },
|
||||
)
|
||||
|
||||
const confirmWithAdjustment = {
|
||||
version: 0,
|
||||
adjustedWeightValue: 286.125,
|
||||
adjustedWeightUnit: 'KG' as const,
|
||||
adjustmentReason: 'OCR 原值 285.500 KG,人工确认 286.125 KG',
|
||||
}
|
||||
await confirmWeightResult('10001', '20001', '30002', confirmWithAdjustment)
|
||||
expect(http.post).toHaveBeenLastCalledWith(
|
||||
'/platform/v1/first-leg/inbounds/20001/weight-results/30002/confirm',
|
||||
confirmWithAdjustment,
|
||||
{ params: { tenantId: '10001' } },
|
||||
)
|
||||
})
|
||||
|
||||
it('downloads an authenticated image as a blob', async () => {
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import ElementPlus from 'element-plus'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { useAuthStore } from '@/core/auth/auth'
|
||||
import {
|
||||
confirmWeightResult,
|
||||
correctWeightResult,
|
||||
getWeightWorkspace,
|
||||
} from '../api/weightOcr'
|
||||
import type { WeightResultResource, WeightWorkspaceResource } from '../types'
|
||||
import WeightOcrPanel from './WeightOcrPanel.vue'
|
||||
|
||||
vi.mock('../api/weightOcr', async () => {
|
||||
const actual = await vi.importActual<typeof import('../api/weightOcr')>('../api/weightOcr')
|
||||
return {
|
||||
...actual,
|
||||
confirmWeightResult: vi.fn(),
|
||||
correctWeightResult: vi.fn(),
|
||||
getWeightWorkspace: vi.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
const pendingWeight: WeightResultResource = {
|
||||
id: '30001',
|
||||
fileId: '40001',
|
||||
ocrProvider: 'FILE_WEIGHT_OCR',
|
||||
ocrRawText: '285.5',
|
||||
ocrWeightValue: 285.5,
|
||||
ocrWeightUnit: 'KG',
|
||||
confidence: 0.50972,
|
||||
adjustedWeightValue: null,
|
||||
adjustedWeightUnit: null,
|
||||
finalWeightKg: null,
|
||||
recognitionStatus: 'PENDING_CONFIRM',
|
||||
adjustmentReason: null,
|
||||
adjustedBy: null,
|
||||
adjustedAt: null,
|
||||
confirmedBy: null,
|
||||
confirmedAt: null,
|
||||
version: 0,
|
||||
}
|
||||
|
||||
const workspace: WeightWorkspaceResource = {
|
||||
inboundId: '20001',
|
||||
inboundCode: 'RK-20001',
|
||||
processStatus: 'MEASURING',
|
||||
actualGrossWeightKg: null,
|
||||
weightResults: [pendingWeight],
|
||||
}
|
||||
|
||||
enableAutoUnmount(afterEach)
|
||||
|
||||
function mountPanel() {
|
||||
const pinia = createPinia()
|
||||
setActivePinia(pinia)
|
||||
const auth = useAuthStore()
|
||||
auth.token = 'token'
|
||||
auth.permissions = [
|
||||
'platform:firstleg:inbound:measure:view',
|
||||
'platform:firstleg:inbound:weight:edit',
|
||||
'platform:firstleg:inbound:weight:confirm',
|
||||
]
|
||||
useAccessPreviewStore().initializeAfterLogin()
|
||||
return mount(WeightOcrPanel, {
|
||||
attachTo: document.body,
|
||||
props: { customerId: '10001', inboundId: '20001' },
|
||||
global: { plugins: [pinia, ElementPlus] },
|
||||
})
|
||||
}
|
||||
|
||||
describe('WeightOcrPanel confirmation behavior', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
document.body.innerHTML = ''
|
||||
vi.mocked(getWeightWorkspace).mockResolvedValue(workspace)
|
||||
vi.mocked(confirmWeightResult).mockResolvedValue({
|
||||
...pendingWeight,
|
||||
recognitionStatus: 'CONFIRMED',
|
||||
finalWeightKg: 285.5,
|
||||
version: 1,
|
||||
})
|
||||
})
|
||||
|
||||
it('confirms an unchanged OCR value with one request', async () => {
|
||||
const wrapper = mountPanel()
|
||||
await flushPromises()
|
||||
|
||||
await wrapper.get('.weight-row__editor .el-button--success').trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(confirmWeightResult).toHaveBeenCalledTimes(1)
|
||||
expect(confirmWeightResult).toHaveBeenCalledWith('10001', '20001', '30001', { version: 0 })
|
||||
expect(correctWeightResult).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('sends the adjusted value and a specific audit reason in the same confirmation request', async () => {
|
||||
const wrapper = mountPanel()
|
||||
await flushPromises()
|
||||
|
||||
await wrapper.get('input[aria-label="称重图片序号 1 的确认重量"]').setValue('286.125')
|
||||
await wrapper.get('.weight-row__editor .el-button--success').trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(confirmWeightResult).toHaveBeenCalledTimes(1)
|
||||
expect(confirmWeightResult).toHaveBeenCalledWith('10001', '20001', '30001', {
|
||||
version: 0,
|
||||
adjustedWeightValue: 286.125,
|
||||
adjustedWeightUnit: 'KG',
|
||||
adjustmentReason: 'OCR 原值 285.500 KG,人工确认 286.125 KG',
|
||||
})
|
||||
expect(correctWeightResult).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -25,8 +25,9 @@ describe('WeightOcrPanel contract', () => {
|
||||
expect(source).toContain('workspace.value?.actualGrossWeightKg')
|
||||
expect(source).toContain('v-model="weightDrafts[row.id].value"')
|
||||
expect(source).toContain('识别值已自动填入,可修改后直接确认保存。')
|
||||
expect(source).toContain("adjustmentReason: '人工核对后修改识别重量'")
|
||||
expect(source).toContain('version = corrected.version')
|
||||
expect(source).toContain('payload.adjustedWeightValue = Number(draft.value)')
|
||||
expect(source).toContain('payload.adjustmentReason = buildAdjustmentReason(row)')
|
||||
expect(source).not.toContain('version = corrected.version')
|
||||
expect(source).not.toContain('title="人工修正重量"')
|
||||
})
|
||||
|
||||
|
||||
@@ -106,6 +106,12 @@ function hasWeightDraftChanged(row: WeightResultResource) {
|
||||
return Number(draft.value) !== Number(savedValue) || draft.unit !== savedUnit
|
||||
}
|
||||
|
||||
function buildAdjustmentReason(row: WeightResultResource) {
|
||||
const draft = weightDrafts.value[row.id]
|
||||
const ocrUnit = normalizeUnit(row.ocrWeightUnit)
|
||||
return `OCR 原值 ${formatWeight(row.ocrWeightValue, ocrUnit)},人工确认 ${formatWeight(Number(draft.value), draft.unit)}`
|
||||
}
|
||||
|
||||
async function loadWorkspace() {
|
||||
if (!customerId.value || !props.inboundId || !canView.value) {
|
||||
workspace.value = null
|
||||
@@ -188,21 +194,17 @@ async function confirmResult(row: WeightResultResource) {
|
||||
activeAction.value = key
|
||||
actionError.value = null
|
||||
try {
|
||||
let version = row.version
|
||||
const payload = { version: row.version } as Parameters<typeof confirmWeightResult>[3]
|
||||
if (hasWeightDraftChanged(row)) {
|
||||
if (!canEdit.value) {
|
||||
ElMessage.warning('当前账号缺少重量修改权限')
|
||||
return
|
||||
}
|
||||
const corrected = await correctWeightResult(customerId.value, props.inboundId, row.id, {
|
||||
version,
|
||||
adjustedWeightValue: Number(draft.value),
|
||||
adjustedWeightUnit: draft.unit,
|
||||
adjustmentReason: '人工核对后修改识别重量',
|
||||
})
|
||||
version = corrected.version
|
||||
payload.adjustedWeightValue = Number(draft.value)
|
||||
payload.adjustedWeightUnit = draft.unit
|
||||
payload.adjustmentReason = buildAdjustmentReason(row)
|
||||
}
|
||||
await confirmWeightResult(customerId.value, props.inboundId, row.id, { version })
|
||||
await confirmWeightResult(customerId.value, props.inboundId, row.id, payload)
|
||||
ElMessage.success('重量已确认并计入实际总重量')
|
||||
await loadWorkspace()
|
||||
emit('changed')
|
||||
@@ -399,6 +401,7 @@ defineExpose({ reload: loadWorkspace })
|
||||
<span>单位</span>
|
||||
<el-segmented
|
||||
v-model="weightDrafts[row.id].unit"
|
||||
:aria-label="`称重图片序号 ${index + 1} 的重量单位`"
|
||||
:options="['KG', 'LB']"
|
||||
:disabled="Boolean(activeAction) || !editable || !canEdit || !canConfirm"
|
||||
/>
|
||||
|
||||
@@ -60,4 +60,7 @@ export interface CorrectWeightResultRequest {
|
||||
|
||||
export interface ConfirmWeightResultRequest {
|
||||
version: number
|
||||
adjustedWeightValue?: number
|
||||
adjustedWeightUnit?: 'KG' | 'LB'
|
||||
adjustmentReason?: string
|
||||
}
|
||||
|
||||
@@ -43,7 +43,17 @@ describe('UnifiedOrdersView contract', () => {
|
||||
expect(source).toContain("{ value: 'UNRECORDED', label: '未录入' }")
|
||||
expect(source).not.toContain("{ value: 'DRAFT', label: '草稿' }")
|
||||
expect(source).not.toContain("{ value: 'CANCELLED', label: '已取消' }")
|
||||
expect(source).toContain("placeholder: '请输入订单号'")
|
||||
expect(source).not.toContain("key: 'orderNo'")
|
||||
expect(source.match(/key: 'inboundCode', type: 'input'/g)).toHaveLength(1)
|
||||
expect(source).not.toContain("key: 'keyword'")
|
||||
const prototypeFieldOrder = [
|
||||
'receivingWarehouseId', 'destinationWarehouseId', 'inboundCode', 'internalOrderStatus', 'expectedArrivalDate',
|
||||
'feeReviewStatus', 'tenantId', 'containerNo', 'cabinetNo', 'channelName', 'sailingDate', 'loaded',
|
||||
'fullContainer', 'measurementReviewStatus', 'fbaCode', 'createdRange',
|
||||
]
|
||||
const fieldPositions = prototypeFieldOrder.map((key) => source.indexOf(`key: '${key}'`))
|
||||
expect(fieldPositions).toEqual([...fieldPositions].sort((left, right) => left - right))
|
||||
expect(source).toContain('preference-key="unified-orders-query-v2"')
|
||||
})
|
||||
|
||||
it('covers real list states and keeps operation column first and left-fixed', () => {
|
||||
@@ -61,6 +71,7 @@ describe('UnifiedOrdersView contract', () => {
|
||||
expect(source).toContain("NOT_SUBMITTED: '未提交'")
|
||||
expect(source).toContain("{ key: 'customerRemark', prop: 'customerRemark', label: '备注'")
|
||||
expect(source).toContain("{ key: 'latestInternalNote', prop: 'latestInternalNote', label: '客服内部备注'")
|
||||
expect(source.match(/<LineClampText v-if="column\.key === 'remark'"/g)).toHaveLength(3)
|
||||
})
|
||||
|
||||
it('keeps only confirmed business actions as explicit boundaries', () => {
|
||||
|
||||
@@ -23,6 +23,7 @@ import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||
import AppDialog from '@/shared/components/AppDialog.vue'
|
||||
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
|
||||
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
||||
import LineClampText from '@/shared/components/LineClampText.vue'
|
||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
|
||||
import StatusBadge from '@/shared/components/StatusBadge.vue'
|
||||
import {
|
||||
@@ -33,8 +34,6 @@ import {
|
||||
|
||||
type FilterModel = QueryModel & {
|
||||
tenantId: string
|
||||
keyword: string
|
||||
orderNo: string
|
||||
mainStatus: UnifiedOrderMainStatus | ''
|
||||
receivingWarehouseId: string
|
||||
destinationWarehouseId: string
|
||||
@@ -127,24 +126,6 @@ const receivingWarehouseOptions = computed(() => warehouses.value
|
||||
.map((warehouse) => ({ value: String(warehouse.id), label: `${warehouse.warehouseName}(${warehouse.warehouseCode})` })))
|
||||
|
||||
const primaryQueryFields = computed<QueryField[]>(() => [
|
||||
{
|
||||
key: 'tenantId',
|
||||
type: 'select',
|
||||
width: 220,
|
||||
label: '公司',
|
||||
placeholder: customerLoadError.value ? '客户列表加载失败' : customerLoading.value ? '正在加载客户' : '全部客户',
|
||||
options: customers.value.map((customer) => ({
|
||||
value: String(customer.id),
|
||||
label: `${customer.legalName?.trim() || customer.tenantName}(${customer.tenantCode || '无客户编码'})`,
|
||||
})),
|
||||
props: { 'aria-label': '客户', filterable: true, loading: customerLoading.value, disabled: Boolean(customerLoadError.value) },
|
||||
},
|
||||
{ key: 'keyword', type: 'input', width: 260, label: '综合搜索', placeholder: '请输入订单号', props: { 'aria-label': '订单号关键词' } },
|
||||
{ key: 'orderNo', type: 'input', width: 220, label: '订单号', placeholder: '请输入订单号', props: { 'aria-label': '订单号' } },
|
||||
{ key: 'createdRange', type: 'daterange', width: 300, label: '创建日期', placeholder: '创建日期范围', props: { 'aria-label': '创建日期范围' } },
|
||||
])
|
||||
|
||||
const advancedQueryFields = computed<QueryField[]>(() => [
|
||||
{ key: 'receivingWarehouseId', type: 'select', width: 220, label: '定位仓库', placeholder: warehouseLoadError.value ? '仓库加载失败' : '请选择定位仓库', options: receivingWarehouseOptions.value, props: { 'aria-label': '定位仓库', filterable: true, loading: warehouseLoading.value, disabled: Boolean(warehouseLoadError.value) } },
|
||||
{ key: 'destinationWarehouseId', type: 'select', width: 220, label: '交货仓', placeholder: warehouseLoadError.value ? '仓库加载失败' : '请选择交货仓', options: warehouseOptions.value, props: { 'aria-label': '交货仓', filterable: true, loading: warehouseLoading.value, disabled: Boolean(warehouseLoadError.value) } },
|
||||
{ key: 'inboundCode', type: 'input', width: 220, label: '入仓码', placeholder: '请输入入仓码', props: { 'aria-label': '入仓码' } },
|
||||
@@ -152,11 +133,26 @@ const advancedQueryFields = computed<QueryField[]>(() => [
|
||||
{ value: 'SUBMITTED', label: '待审核' }, { value: 'APPROVED', label: '已审核' },
|
||||
{ value: 'ACCEPTED', label: '已受理' }, { value: 'PROCESSING', label: '处理中' }, { value: 'COMPLETED', label: '已完成' },
|
||||
], props: { 'aria-label': '订单状态' } },
|
||||
])
|
||||
|
||||
const advancedQueryFields = computed<QueryField[]>(() => [
|
||||
{ key: 'expectedArrivalDate', type: 'date', width: 190, label: '预计到达时间', placeholder: '请选择预计到达日期', props: { 'aria-label': '预计到达时间' } },
|
||||
{ key: 'feeReviewStatus', type: 'select', width: 190, label: '费用审核状态', placeholder: '请选择费用审核状态', options: [
|
||||
{ value: 'UNRECORDED', label: '未录入' }, { value: 'PENDING', label: '待审核' },
|
||||
{ value: 'APPROVED', label: '审核通过' }, { value: 'REJECTED', label: '审核驳回' },
|
||||
], props: { 'aria-label': '费用审核状态' } },
|
||||
{
|
||||
key: 'tenantId',
|
||||
type: 'select',
|
||||
width: 220,
|
||||
label: '公司',
|
||||
placeholder: customerLoadError.value ? '客户列表加载失败' : customerLoading.value ? '正在加载客户' : '请选择公司',
|
||||
options: customers.value.map((customer) => ({
|
||||
value: String(customer.id),
|
||||
label: `${customer.legalName?.trim() || customer.tenantName}(${customer.tenantCode || '无客户编码'})`,
|
||||
})),
|
||||
props: { 'aria-label': '公司', filterable: true, loading: customerLoading.value, disabled: Boolean(customerLoadError.value) },
|
||||
},
|
||||
{ key: 'containerNo', type: 'input', width: 210, label: '集装箱号', placeholder: '请输入集装箱号', props: { 'aria-label': '集装箱号' } },
|
||||
{ key: 'cabinetNo', type: 'input', width: 190, label: '柜号', placeholder: '请输入柜号', props: { 'aria-label': '柜号' } },
|
||||
{ key: 'channelName', type: 'input', width: 210, label: '渠道', placeholder: '请输入渠道名称', props: { 'aria-label': '渠道' } },
|
||||
@@ -168,6 +164,7 @@ const advancedQueryFields = computed<QueryField[]>(() => [
|
||||
{ value: 'APPROVED', label: '审核通过' }, { value: 'REJECTED', label: '审核驳回' },
|
||||
], props: { 'aria-label': '测量数据审核状态' } },
|
||||
{ key: 'fbaCode', type: 'input', width: 220, label: '商品 FBA 编码', placeholder: '请输入商品 FBA 编码', props: { 'aria-label': '商品 FBA 编码' } },
|
||||
{ key: 'createdRange', type: 'daterange', width: 300, label: '创建日期', placeholder: '创建日期范围', props: { 'aria-label': '创建日期范围' } },
|
||||
])
|
||||
|
||||
const allQueryFields = computed(() => [...primaryQueryFields.value, ...advancedQueryFields.value])
|
||||
@@ -234,7 +231,7 @@ const columns: DataTableColumn[] = [
|
||||
|
||||
function emptyFilters(): FilterModel {
|
||||
return {
|
||||
tenantId: '', keyword: '', orderNo: '', mainStatus: '', receivingWarehouseId: '', destinationWarehouseId: '',
|
||||
tenantId: '', mainStatus: '', receivingWarehouseId: '', destinationWarehouseId: '',
|
||||
inboundCode: '', internalOrderStatus: '', expectedArrivalDate: '', feeReviewStatus: '', channelName: '',
|
||||
containerNo: '', cabinetNo: '', sailingDate: '', loaded: '', fullContainer: '', measurementReviewStatus: '',
|
||||
fbaCode: '', createdRange: [],
|
||||
@@ -622,8 +619,6 @@ async function loadOrders() {
|
||||
const range = filters.value.createdRange
|
||||
const result = await listPlatformUnifiedOrders({
|
||||
tenantId: filters.value.tenantId || undefined,
|
||||
keyword: trimmed(filters.value.keyword),
|
||||
orderNo: trimmed(filters.value.orderNo),
|
||||
mainStatus: filters.value.mainStatus || undefined,
|
||||
receivingWarehouseId: filters.value.receivingWarehouseId || undefined,
|
||||
destinationWarehouseId: filters.value.destinationWarehouseId || undefined,
|
||||
@@ -918,21 +913,21 @@ onMounted(() => {
|
||||
<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 v-if="!measurementCollapsed.customer">
|
||||
<div class="detail-table-wrap"><el-table :data="detail.order.cargoes" 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 }">{{ measurementValue(row as Record<string, unknown>, column.key, true) }}</template></el-table-column></el-table></div>
|
||||
<div class="detail-table-wrap"><el-table :data="detail.order.cargoes" 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>
|
||||
</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 v-if="!measurementCollapsed.outer">
|
||||
<div v-loading="measurementLoading" class="detail-table-wrap"><el-table :data="measurementDetail?.items || []" 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 }">{{ measurementValue(row as Record<string, unknown>, column.key) }}</template></el-table-column></el-table></div>
|
||||
<div v-loading="measurementLoading" class="detail-table-wrap"><el-table :data="measurementDetail?.items || []" 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>
|
||||
</section>
|
||||
<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 v-if="!measurementCollapsed.actual">
|
||||
<div v-loading="measurementLoading" class="detail-table-wrap"><el-table :data="measurementDetail?.items || []" 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 }">{{ measurementValue(row as Record<string, unknown>, column.key) }}</template></el-table-column></el-table></div>
|
||||
<div v-loading="measurementLoading" class="detail-table-wrap"><el-table :data="measurementDetail?.items || []" 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>箱数:{{ measurementDetail?.actualPackageCount ?? '—' }}</span><span>总毛重:{{ formatNumber(measurementDetail?.actualGrossWeightKg) }}</span><span>总体积:{{ formatNumber(measurementDetail?.actualVolumeCbm, 6) }}</span><span>总材积:{{ formatNumber(measurementDetail?.volumeWeightKg) }}</span></div>
|
||||
<div class="measurement-media"><h4>测量图片</h4><div v-if="measurementImageUrls.length" class="measurement-media-grid"><el-image v-for="url in measurementImageUrls" :key="url" :src="url" fit="contain" :preview-src-list="measurementImageUrls" class="measurement-media-image" /></div><el-empty v-else description="暂无测量图片" :image-size="48" /><h4>测量视频</h4><el-empty description="暂无测量视频" :image-size="48" /></div>
|
||||
</div>
|
||||
@@ -966,7 +961,7 @@ onMounted(() => {
|
||||
>{{ tab.label }}</button>
|
||||
</div>
|
||||
|
||||
<QueryToolbar id="unified-orders-advanced-filters" v-model="filters" ariaLabel="订单列表筛选" :fields="queryFields" :available-fields="allQueryFields" preference-key="unified-orders-query" :search-on-reset="false" @search="search" @reset="reset">
|
||||
<QueryToolbar id="unified-orders-advanced-filters" v-model="filters" ariaLabel="订单列表筛选" :fields="queryFields" :available-fields="allQueryFields" preference-key="unified-orders-query-v2" :search-on-reset="false" @search="search" @reset="reset">
|
||||
<template #actions>
|
||||
<el-button :aria-expanded="showAdvancedFilters" aria-controls="unified-orders-advanced-filters" @click="showAdvancedFilters = !showAdvancedFilters">
|
||||
<SlidersHorizontal :size="16" />{{ showAdvancedFilters ? '收起筛选' : '高级筛选' }}
|
||||
@@ -1004,7 +999,7 @@ onMounted(() => {
|
||||
</template>
|
||||
<template #orderStatus="{ row }"><StatusBadge :status="orderStatusLabel((row as UnifiedOrderResource).internalOrderStatus || (row as UnifiedOrderResource).mainStatus)" /></template>
|
||||
<template #measurementReviewStatus="{ row }"><StatusBadge :status="measurementStatusLabel((row as UnifiedOrderResource).measurement?.reviewStatus)" /></template>
|
||||
<template #field="{ row, column }">{{ displayField(row as UnifiedOrderResource, column.property) }}</template>
|
||||
<template #field="{ row, column }"><LineClampText v-if="['customerRemark', 'latestInternalNote'].includes(String(column.property))" :content="displayField(row as UnifiedOrderResource, column.property)" :lines="2" /><template v-else>{{ displayField(row as UnifiedOrderResource, column.property) }}</template></template>
|
||||
<template #order="{ row }"><div class="primary-cell"><strong>{{ (row as UnifiedOrderResource).orderNo || `订单 ${(row as UnifiedOrderResource).orderId}` }}</strong><span v-if="(row as UnifiedOrderResource).inboundCode">入仓码 {{ (row as UnifiedOrderResource).inboundCode }}</span></div></template>
|
||||
<template #mainStatus="{ row }"><StatusBadge :status="statusLabel((row as UnifiedOrderResource).mainStatus)" /></template>
|
||||
<template #customer="{ row }"><div class="primary-cell"><strong>{{ customerName(row as UnifiedOrderResource) }}</strong><span>客户 ID {{ (row as UnifiedOrderResource).tenantId }}</span></div></template>
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { defineComponent } from 'vue'
|
||||
import { shallowMount } from '@vue/test-utils'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import LineClampText from './LineClampText.vue'
|
||||
|
||||
const TooltipStub = defineComponent({
|
||||
props: { content: String, disabled: Boolean, visible: Boolean },
|
||||
template: '<div class="tooltip-stub" :data-content="content" :data-visible="visible"><slot /></div>',
|
||||
})
|
||||
|
||||
describe('LineClampText', () => {
|
||||
it('defaults to one line and exposes the full text to hover and focus', async () => {
|
||||
const wrapper = shallowMount(LineClampText, {
|
||||
props: { content: '第一行\n第二行' },
|
||||
global: { stubs: { ElTooltip: TooltipStub } },
|
||||
})
|
||||
const text = wrapper.get('.line-clamp-text')
|
||||
expect(text.attributes('style')).toContain('--cc-line-clamp: 1')
|
||||
expect(text.attributes('aria-label')).toBe('第一行\n第二行')
|
||||
expect(wrapper.get('.tooltip-stub').attributes('data-content')).toBe('第一行\n第二行')
|
||||
expect(wrapper.get('.tooltip-stub').attributes('data-visible')).toBe('false')
|
||||
|
||||
await text.trigger('mouseenter')
|
||||
expect(wrapper.get('.tooltip-stub').attributes('data-visible')).toBe('true')
|
||||
await text.trigger('mouseleave')
|
||||
expect(wrapper.get('.tooltip-stub').attributes('data-visible')).toBe('false')
|
||||
await text.trigger('focus')
|
||||
expect(wrapper.get('.tooltip-stub').attributes('data-visible')).toBe('true')
|
||||
await text.trigger('blur')
|
||||
expect(wrapper.get('.tooltip-stub').attributes('data-visible')).toBe('false')
|
||||
})
|
||||
|
||||
it('accepts two and three line limits', () => {
|
||||
expect(shallowMount(LineClampText, {
|
||||
props: { content: 'text', lines: 2 },
|
||||
global: { stubs: { ElTooltip: TooltipStub } },
|
||||
}).get('.line-clamp-text').attributes('style')).toContain('--cc-line-clamp: 2')
|
||||
expect(shallowMount(LineClampText, {
|
||||
props: { content: 'text', lines: 3 },
|
||||
global: { stubs: { ElTooltip: TooltipStub } },
|
||||
}).get('.line-clamp-text').attributes('style')).toContain('--cc-line-clamp: 3')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,48 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
content?: string | number | null
|
||||
lines?: 1 | 2 | 3
|
||||
}>(), {
|
||||
content: '',
|
||||
lines: 1,
|
||||
})
|
||||
|
||||
const text = computed(() => props.content === null || props.content === undefined ? '' : String(props.content))
|
||||
const lineStyle = computed(() => ({ '--cc-line-clamp': props.lines }))
|
||||
const tooltipVisible = ref(false)
|
||||
|
||||
function showTooltip() {
|
||||
if (text.value) tooltipVisible.value = true
|
||||
}
|
||||
|
||||
function hideTooltip() {
|
||||
tooltipVisible.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-tooltip
|
||||
:content="text || '—'"
|
||||
:disabled="!text"
|
||||
:visible="Boolean(text) && tooltipVisible"
|
||||
placement="top-start"
|
||||
>
|
||||
<span
|
||||
class="line-clamp-text"
|
||||
:style="lineStyle"
|
||||
:tabindex="text ? 0 : undefined"
|
||||
:aria-label="text || '—'"
|
||||
@mouseenter="showTooltip"
|
||||
@mouseleave="hideTooltip"
|
||||
@focus="showTooltip"
|
||||
@blur="hideTooltip"
|
||||
>{{ text || '—' }}</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.line-clamp-text { display: -webkit-box; min-width: 0; overflow: hidden; line-height: 1.5; -webkit-box-orient: vertical; -webkit-line-clamp: var(--cc-line-clamp); overflow-wrap: anywhere; white-space: pre-line; }
|
||||
.line-clamp-text:focus-visible { outline: 2px solid color-mix(in srgb, var(--cc-primary) 45%, transparent); outline-offset: 2px; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user