This commit is contained in:
2026-08-07 18:44:55 +08:00
parent 2425e0cac1
commit 38a0ffd3e3
9 changed files with 272 additions and 40 deletions
@@ -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"
/>
+3
View File
@@ -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')
})
})
+48
View File
@@ -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>