Merge remote-tracking branch 'origin/release/test' into dev

This commit is contained in:
2026-09-02 10:34:07 +08:00
80 changed files with 2219 additions and 591 deletions
@@ -1,5 +1,5 @@
import { mount } from '@vue/test-utils'
import { describe, expect, it } from 'vitest'
import { flushPromises, mount } from '@vue/test-utils'
import { describe, expect, it, vi } from 'vitest'
import OrderReferenceText from './OrderReferenceText.vue'
describe('OrderReferenceText', () => {
@@ -23,4 +23,15 @@ describe('OrderReferenceText', () => {
expect(wrapper.text()).toBe('LMO-260827-0001')
expect(wrapper.attributes('title')).toBe('LMO-260827-0001')
})
it('copies the exact server short number without frontend formatting', async () => {
const writeText = vi.fn().mockResolvedValue(undefined)
vi.stubGlobal('navigator', { clipboard: { writeText } })
const wrapper = mount(OrderReferenceText, { props: { value: 'LMO-9A7', copyable: true } })
await wrapper.get('el-button').trigger('click')
await flushPromises()
expect(writeText).toHaveBeenCalledWith('LMO-9A7')
expect(wrapper.attributes('title')).toBe('LMO-9A7')
vi.unstubAllGlobals()
})
})
@@ -1,10 +1,13 @@
<script setup lang="ts">
import { computed } from 'vue'
import CopyButton from '@/shared/components/CopyButton.vue'
const props = withDefaults(defineProps<{
value?: string | null
emptyText?: string
}>(), { value: null, emptyText: '—' })
copyable?: boolean
copyLabel?: string
}>(), { value: null, emptyText: '—', copyable: false, copyLabel: '尾程单号' })
const displayValue = computed(() => {
const value = String(props.value ?? '')
@@ -15,16 +18,23 @@ const fullTitle = computed(() => displayValue.value === props.emptyText ? undefi
</script>
<template>
<span class="order-reference-text" :title="fullTitle">{{ displayValue }}</span>
<span class="order-reference-text" :title="fullTitle"><span class="order-reference-text__value">{{ displayValue }}</span><CopyButton v-if="copyable && fullTitle" class="order-reference-text__copy" :value="displayValue" :label="copyLabel" button-text="" /></span>
</template>
<style scoped>
.order-reference-text {
display: inline-block;
display: inline-flex;
min-width: 0;
align-items: center;
gap: 4px;
max-width: 100%;
vertical-align: bottom;
}
.order-reference-text__value {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: bottom;
white-space: nowrap;
}
.order-reference-text__copy { flex: 0 0 auto; margin: 0; padding-inline: 6px; }
</style>
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { formatOverseasDateTime, toLastMileOrderListItem } from './lastMilePresentation'
import { formatOverseasDateTime, lastMileOrderReference, toLastMileOrderListItem } from './lastMilePresentation'
describe('last-mile order presentation adapter', () => {
it('formats overseas timestamps with zero-padded seconds', () => {
@@ -32,6 +32,14 @@ describe('last-mile order presentation adapter', () => {
}).owner).toBe('—')
})
it('preserves the exact server LMO short number without padding, prefixing or deriving it from id', () => {
const serverOrderNo = 'LMO-9A7'
const row = toLastMileOrderListItem({ id: '2092873463568596994', customerId: '100', orderNo: serverOrderNo })
expect(row.orderNo).toBe(serverOrderNo)
expect(lastMileOrderReference(serverOrderNo)).toBe(serverOrderNo)
expect(row.orderNo).not.toContain(row.id)
})
it('preserves real metrics and distinguishes zero from unknown fields and times', () => {
const estimatedArrivalAt = '2026-08-27T12:30:00Z'
const row = toLastMileOrderListItem({
@@ -45,6 +45,11 @@ export function lastMileOrderTypeLabel(value: unknown) {
return orderTypeLabels[code] || text(code)
}
export function lastMileOrderReference(value: unknown, emptyLabel = '—') {
const serverValue = value == null ? '' : String(value)
return serverValue || emptyLabel
}
export function formatOverseasDateTime(value: unknown) {
const raw = String(value ?? '').trim()
if (!raw) return '—'
@@ -71,7 +76,7 @@ export function toLastMileOrderListItem(dto: LastMileOrderSummaryDto): LastMileO
id: String(dto.id),
customerId: String(dto.customerId),
sourceContainerBookingId: String(dto.sourceContainerBookingId ?? '').trim(),
orderNo: text(dto.orderNo),
orderNo: lastMileOrderReference(dto.orderNo),
warning: dto.abnormal ? '异常' : '—',
orderType: text(dto.orderTypeName || lastMileOrderTypeLabel(dto.orderType)),
rejectedAt: '—',
@@ -11,7 +11,7 @@ import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.v
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import OperationErrorToast from '@/shared/components/OperationErrorToast.vue'
import { createLastMileOrderFee, getLastMileOrder, type LastMileOrderDetailDto } from '../api/lastMileOrders'
import { formatOverseasDateTime, lastMileOrderTypeLabel, lastMileStatusLabel } from '../lastMilePresentation'
import { formatOverseasDateTime, lastMileOrderReference, lastMileOrderTypeLabel, lastMileStatusLabel } from '../lastMilePresentation'
import { overseasWarehousePermissions as permission } from '../permissions'
import OverseasDetailShell from '../components/OverseasDetailShell.vue'
import OrderReferenceText from '../components/OrderReferenceText.vue'
@@ -39,7 +39,7 @@ function text(value: unknown) { return String(value ?? '').trim() || '—' }
function yesNo(value: boolean | null | undefined) { return value == null ? '—' : value ? '是' : '否' }
function feeStatusLabel(value: string) { return ({ PENDING_REVIEW: '待审核', APPROVED: '审核通过', REJECTED: '已驳回' } as Record<string, string>)[value] || value }
const referenceValue = computed(() => text(detail.value?.order.orderNo || '订单编码待补充'))
const referenceValue = computed(() => lastMileOrderReference(detail.value?.order.orderNo, '订单编码待补充'))
const orderFields = computed<DetailField[]>(() => {
const order = detail.value?.order
return [
@@ -151,7 +151,7 @@ onMounted(load)
<template>
<OverseasDetailShell title="尾程订单详情" reference-label="尾程单号" :reference-value="referenceValue" back-label="返回尾程订单" domain="尾程订单详情" :pending="false" :status-text="detail ? lastMileStatusLabel(detail.order.currentStatus) : '加载中'" @back="back">
<template #reference><OrderReferenceText :value="referenceValue" /></template>
<template #reference><OrderReferenceText :value="referenceValue" copyable /></template>
<OperationErrorToast :details="mutationError" />
<LoadErrorState v-if="loadError" title="尾程订单详情加载失败" :message="loadError.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="loadError" /></template></LoadErrorState>
<section v-else v-loading="loading" class="detail-surface" :aria-busy="loading">
@@ -410,7 +410,7 @@ onMounted(() => { void load() })
<section v-else-if="!loadError" class="surface-panel table-panel"><DataTable v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :loading="loading" :total="total" preference-key="overseas-last-mile-orders" row-key="id" empty-text="暂无尾程订单" @selection-change="onSelectionChange" @update:page="load()" @update:page-size="page = 1; load()">
<template #toolbar-actions><el-button v-if="access.can(permission.lastMileOrderExport)" plain :loading="exportLoading" @click="exportOrders"><Download :size="16" />导出订单</el-button><el-button v-if="access.can(permission.lastMileOrderTransfer)" plain :disabled="!selectedRows.length || actionLoading" @click="openTransfer">转仓</el-button><el-button v-if="access.can(permission.inboundOrderCreate)" type="primary" plain :disabled="selectedRows.length !== 1 || actionLoading" @click="convertInboundPlan">生成入库计划</el-button><el-button v-if="statusTab === '待分配' && access.can(permission.inboundOrderCreate)" type="primary" :disabled="actionLoading" @click="openContainerConversion">按柜批量生成</el-button></template>
<template #actions="{ row }"><el-button size="small" plain type="primary" @click="openDetail(row)"><Eye :size="15" />查看</el-button></template>
<template #orderNo="{ row }"><OrderReferenceText :value="String(row.orderNo || '')" /></template>
<template #orderNo="{ row }"><OrderReferenceText :value="String(row.orderNo || '')" copyable /></template>
<template #pagination-left>共 {{ total }} 条尾程订单 · 已选 {{ selectedRows.length }} 条</template>
</DataTable></section>
</ListPageLayout></AdminPage>
@@ -128,6 +128,16 @@ describe('overseas warehouse first-batch prototype contract', () => {
expect(value).not.toContain('海外派送')
})
it('renders and copies the server-provided tail-leg short number in list and detail', () => {
const list = source('LastMileOrdersView.vue')
const detail = source('LastMileOrderDetailView.vue')
expect(list).toContain('<OrderReferenceText :value="String(row.orderNo || \'\')" copyable />')
expect(detail).toContain('lastMileOrderReference(detail.value?.order.orderNo')
expect(detail).toContain('<OrderReferenceText :value="referenceValue" copyable />')
expect(list).not.toMatch(/padStart|LMO-\$\{|slice\([^)]*orderNo/)
expect(detail).not.toMatch(/padStart|LMO-\$\{|slice\([^)]*orderNo/)
})
it('connects the tail-leg detail to the real overseas API and keeps missing contracts pending', () => {
const tailLeg = source('LastMileOrderDetailView.vue')
expect(tailLeg).toContain('getLastMileOrder')
@@ -137,6 +147,8 @@ describe('overseas warehouse first-batch prototype contract', () => {
expect(tailLeg).toContain('label="费用明细"')
expect(tailLeg).toContain('label="操作记录"')
expect(tailLeg).toContain('title="新增费用"')
expect(tailLeg).toContain('lastMileOrderReference(detail.value?.order.orderNo')
expect(tailLeg).toContain('<OrderReferenceText :value="referenceValue" copyable />')
const inbound = source('InboundOrderDetailView.vue')
expect(inbound).toContain('getInboundPlan')
expect(inbound).toContain('sourceOrderId')