fix: preserve required inputs and refine operations screens
This commit is contained in:
@@ -124,13 +124,16 @@ it('preserves real FBA fields and entered values when expanding the create dialo
|
||||
expect(creationBody().get('[aria-label="预约保存回执"]').findAll('input, textarea, select')).toHaveLength(0)
|
||||
expect(creationBody().text()).toContain('AP-001')
|
||||
})
|
||||
it('shows every saved server field as readonly text including confirmed offsets and long remarks', async () => {
|
||||
it('shows saved dates, confirmed offsets and long remarks without planned time ranges', async () => {
|
||||
const saved = { ...batchContext().appointment, plannedDate: '2026-10-26', plannedAt: '2026-10-26T08:00:00', plannedEndAt: '2026-10-26T18:00:00', timezone: 'America/Los_Angeles', appointmentStatus: 'SUCCESS', confirmationNo: 'CONFIRM-001', appointmentAt: '2026-11-01T01:30:00', appointmentEndAt: '2026-11-01T01:45:00', appointmentStartUtc: '2026-11-01T08:30:00Z', appointmentEndUtc: '2026-11-01T09:45:00Z', appointmentRemark: '完整预约备注'.repeat(30) }
|
||||
vi.mocked(createSourceAppointment).mockResolvedValue(saved)
|
||||
const wrapper = await open(); await create(wrapper)
|
||||
await creationBody().findAll('button').find(button => button.text() === '保存预约安排')!.trigger('click'); await flushPromises()
|
||||
const receipt = creationBody().get('[aria-label="预约保存回执"]')
|
||||
for (const text of ['AP-001', saved.plannedDate, saved.plannedAt, saved.plannedEndAt, saved.timezone, '已取得对方正式确认', saved.confirmationNo, saved.appointmentAt, saved.appointmentEndAt, '-07:00', '-08:00', saved.appointmentRemark]) expect(receipt.text()).toContain(text)
|
||||
for (const text of ['AP-001', saved.plannedDate, saved.timezone, '已取得对方正式确认', saved.confirmationNo, saved.appointmentAt, saved.appointmentEndAt, '-07:00', '-08:00', saved.appointmentRemark]) expect(receipt.text()).toContain(text)
|
||||
expect(receipt.text()).not.toMatch(/计划开始时段|计划结束时段/)
|
||||
expect(receipt.text()).not.toContain(saved.plannedAt)
|
||||
expect(receipt.text()).not.toContain(saved.plannedEndAt)
|
||||
expect(receipt.findAll('input, textarea, select')).toHaveLength(0)
|
||||
expect(receipt.text()).not.toMatch(/item-1|customer-a|FBA_APPOINTMENT:1/)
|
||||
})
|
||||
|
||||
@@ -32,9 +32,9 @@ it('names the missing material and supplements the canonical booking inside the
|
||||
const wrapper = render(); await flushPromises()
|
||||
expect(resolvePlanningOrder).toHaveBeenCalledWith(lookup)
|
||||
expect(wrapper.text()).toContain('IN-TEST'); expect(wrapper.text()).toContain('缺少本票 FBA 货件号或追踪号')
|
||||
expect(wrapper.text()).toContain('不能代替货件号'); expect(wrapper.text()).not.toContain(source.orderId)
|
||||
expect(wrapper.text()).not.toContain(source.orderId)
|
||||
await wrapper.findAll('button').find(button => button.text() === '在此补齐资料')!.trigger('click'); await flushPromises()
|
||||
expect((wrapper.get('input[type=number]').element as HTMLInputElement).value).toBe('20')
|
||||
expect(wrapper.find('input[type=number]').exists()).toBe(false)
|
||||
await wrapper.get('form').trigger('submit'); await flushPromises()
|
||||
expect(saveCargoVerification).not.toHaveBeenCalled()
|
||||
await wrapper.get('textarea[aria-label="本票货件号"]').setValue('FBA-TEST')
|
||||
@@ -44,6 +44,22 @@ it('names the missing material and supplements the canonical booking inside the
|
||||
expect(wrapper.text()).toContain('资料齐全,无需重复填写')
|
||||
expect(wrapper.emitted('ready')?.at(-1)).toEqual([true]); wrapper.unmount()
|
||||
})
|
||||
it('allows missing or changed quantities to be verified before saving shipment references', async () => {
|
||||
for (const cargo of [
|
||||
{ ...context, effectivePackages: null },
|
||||
{ ...context, sourceChanged: true },
|
||||
]) {
|
||||
vi.mocked(getCargoVerification).mockResolvedValue(cargo)
|
||||
vi.mocked(saveCargoVerification).mockClear()
|
||||
const wrapper = render(); await flushPromises()
|
||||
await wrapper.findAll('button').find(button => button.text() === '在此补齐资料')!.trigger('click'); await flushPromises()
|
||||
await wrapper.get('input[type=number]').setValue('20')
|
||||
await wrapper.get('textarea[aria-label="本票货件号"]').setValue('FBA-TEST')
|
||||
await wrapper.get('form').trigger('submit'); await flushPromises()
|
||||
expect(saveCargoVerification).toHaveBeenCalledWith(source, expect.objectContaining({ packageCount: 20, shipmentReferences: ['FBA-TEST'] }))
|
||||
wrapper.unmount()
|
||||
}
|
||||
})
|
||||
it('leaves pending arrangements lazy and reads only when formal confirmation is selected', async () => {
|
||||
const wrapper = render(false); await flushPromises()
|
||||
expect(resolvePlanningOrder).not.toHaveBeenCalled()
|
||||
|
||||
@@ -66,7 +66,6 @@ onBeforeUnmount(() => { ++generation; unregister?.() })
|
||||
<section v-show="active || editing" class="density-dialog-section" aria-label="正式确认前的货物资料" :aria-busy="loading">
|
||||
<h3 class="density-dialog-section__title">正式确认前:核对本次送仓货物</h3>
|
||||
<el-button :disabled="loading || !!editing || disabled" :loading="loading" @click="load">重新核对资料</el-button>
|
||||
<p>本票总箱数用于校验预约货量;FBA 货件号或追踪号用于识别送的是哪批货。外部约号只代表仓库的预约确认号,不能代替货件号。</p>
|
||||
<LoadErrorState v-if="error" title="货物资料未核对" :message="error.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState>
|
||||
<table class="density-table"><thead><tr><th>操作</th><th>入仓码 / 外接登记号</th><th>本票总箱数</th><th>FBA 货件号 / 追踪号</th><th>需要补充的资料</th></tr></thead><tbody>
|
||||
<tr v-for="row in rows" :key="key(row)">
|
||||
|
||||
@@ -129,7 +129,6 @@ onBeforeUnmount(() => { ++generation; ++selectionGeneration; ++destinationsReque
|
||||
<table class="density-table"><tbody>
|
||||
<tr><th scope="row">系统单号</th><td>{{ created.appointmentNo || '—' }}</td><th scope="row">目的仓时区</th><td>{{ created.timezone || '待核实' }}</td></tr>
|
||||
<tr><th scope="row">计划送仓日期</th><td>{{ createdForm.plannedDate || '—' }}</td><th scope="row">本次登记</th><td>{{ ({ PENDING: '等待对方确认', SUCCESS: '已取得对方正式确认', FAILED: '未约成', CANCELLED: '已取消' })[createdForm.appointmentStatus] }}</td></tr>
|
||||
<tr><th scope="row">计划开始时段</th><td>{{ createdForm.plannedAt || '—' }}</td><th scope="row">计划结束时段</th><td>{{ createdForm.plannedEndAt || '—' }}</td></tr>
|
||||
<tr><th scope="row">外部约号</th><td colspan="3">{{ createdForm.confirmationNo || '—' }}</td></tr>
|
||||
<tr v-if="createdForm.appointmentAt || createdForm.appointmentEndAt"><th scope="row">对方确认开始时间</th><td>{{ createdForm.appointmentAt || '—' }}</td><th scope="row">对方确认结束时间</th><td>{{ createdForm.appointmentEndAt || '—' }}</td></tr>
|
||||
<tr v-if="createdForm.startOffset || createdForm.endOffset"><th scope="row">确认开始时差</th><td>{{ createdForm.startOffset || '—' }}</td><th scope="row">确认结束时差</th><td>{{ createdForm.endOffset || '—' }}</td></tr>
|
||||
|
||||
@@ -59,14 +59,13 @@ onMounted(() => load())
|
||||
<template>
|
||||
<section aria-label="货物核实资料">
|
||||
<h3>{{ confirmation ? '补齐本票送仓资料' : '核实货物资料' }}<InlineHelp title="核实资料说明"><p>未约成也可保存;只用于预约核实,保留原订单资料。</p></InlineHelp></h3>
|
||||
<p v-if="confirmation">请向客户或货物负责人取得本票完整的 FBA 货件号或追踪号,每行一个;不要填写预约外部约号。下方保存只补充货物资料,完成后还需保存预约安排。</p>
|
||||
<LoadErrorState v-if="error" title="核实资料读取失败" :message="error.message" :loading="loading" @retry="load(true)"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState>
|
||||
<p v-if="loading">正在读取货物资料…</p>
|
||||
<template v-if="context">
|
||||
<dl class="cargo-facts"><dt>原票</dt><dd>{{ context.source.inboundCode || context.source.bookingNo || '—' }}</dd><dt>目的仓</dt><dd>{{ context.source.destinationWarehouseCode || '—' }}</dd><dt>原箱数</dt><dd>{{ context.source.packages ?? '待核实' }}</dd><template v-if="!confirmation"><dt>已分配</dt><dd>{{ context.allocatedPackages }} 箱<span v-if="context.unknownAllocationCount">;另有 {{ context.unknownAllocationCount }} 项数量未知</span></dd><dt>可分配</dt><dd>{{ context.availablePackages ?? '待核实' }}</dd></template></dl>
|
||||
<dl v-if="!confirmation" class="cargo-facts"><dt>原票</dt><dd>{{ context.source.inboundCode || context.source.bookingNo || '—' }}</dd><dt>目的仓</dt><dd>{{ context.source.destinationWarehouseCode || '—' }}</dd><dt>原箱数</dt><dd>{{ context.source.packages ?? '待核实' }}</dd><dt>已分配</dt><dd>{{ context.allocatedPackages }} 箱<span v-if="context.unknownAllocationCount">;另有 {{ context.unknownAllocationCount }} 项数量未知</span></dd><dt>可分配</dt><dd>{{ context.availablePackages ?? '待核实' }}</dd></dl>
|
||||
<p v-if="context.sourceChanged" role="alert">原货源已变化,请核对当前原票和已分配量后重新保存。</p><p v-for="reason in context.blockReasons" :key="reason" role="status">{{ reason }}</p>
|
||||
<form class="density-form work-form appointment-native-form" @submit.prevent="save"><fieldset :disabled="!canWrite || loading || !!error || saving || !!pending">
|
||||
<label>{{ confirmation ? '本票核实总箱数(必填)' : '核实箱数(可暂不填)' }}<input v-model="form.packages" type="number" min="1" step="1" aria-label="核实箱数" class="workbench-input"></label>
|
||||
<label v-if="!confirmation || context.effectivePackages == null || context.sourceChanged">{{ confirmation ? '本票核实总箱数(必填)' : '核实箱数(可暂不填)' }}<input v-model="form.packages" type="number" min="1" step="1" aria-label="核实箱数" class="workbench-input"></label>
|
||||
<label>本票货件号 / 追踪号(每行一个)<textarea v-model="form.references" rows="4" aria-label="本票货件号" class="workbench-input" /></label>
|
||||
<label>核实说明<textarea v-model="form.remark" maxlength="1000" rows="2" class="workbench-input" /></label>
|
||||
</fieldset><p v-if="!canWrite">当前仅可查看核实资料;需核实权限及有效货源资格才能保存。</p>
|
||||
|
||||
@@ -63,7 +63,7 @@ onMounted(load)
|
||||
</tbody></table>
|
||||
<p v-for="field in visibleMissingFields" :key="field" role="status" class="density-dialog-note">{{ workReason(field) }}</p>
|
||||
</section>
|
||||
<FbaAppointmentFields continuous hide-confirmation-times ref="appointmentFields" :sources="sources.map(source => ({ ...source, destinationKey: source.destinationKey || undefined }))" @confirmation-ready="confirmationReady = $event" :model="form" :timezone="preview.destinationTimezone" :disabled="!writable || loading || saving || !!pending" />
|
||||
<FbaAppointmentFields continuous date-only ref="appointmentFields" :sources="sources.map(source => ({ ...source, destinationKey: source.destinationKey || undefined }))" @confirmation-ready="confirmationReady = $event" :model="form" :timezone="preview.destinationTimezone" :disabled="!writable || loading || saving || !!pending" />
|
||||
<LoadErrorState v-if="saveError" title="预约尚未创建" :message="saveError.message" :show-retry="false"><template #details><ApiErrorMeta :details="saveError" /></template></LoadErrorState>
|
||||
<div class="density-actions density-actions--footer"><el-button type="primary" :disabled="!writable || loading" :loading="saving" @click="save">{{ pending ? '重试本次创建' : '保存预约安排' }}</el-button></div>
|
||||
</template>
|
||||
|
||||
@@ -43,12 +43,15 @@ describe('LoginView authentication lifecycle', () => {
|
||||
expect(loginSource).not.toContain('demo-tip')
|
||||
})
|
||||
|
||||
it('uses the shared blue-white theme and preserves the existing compact brand layout', () => {
|
||||
it('preserves the dark brand panel and white login card independently of workbench typography', () => {
|
||||
expect(loginSource).toContain("import '@/styles/adminWorkbench.css'")
|
||||
expect(loginSource).toContain('class="login-page admin-workbench"')
|
||||
expect(loginSource).toContain('<h1 class="workbench-title">柏悦物流管理后台</h1>')
|
||||
expect(loginSource).toContain('class="login-mobile-brand workbench-brand-mark"')
|
||||
expect(loginSource).toContain('class="login-card workbench-panel"')
|
||||
expect(loginSource).toContain('<h1>柏悦物流管理后台</h1>')
|
||||
expect(loginSource).toContain('class="login-mobile-brand"')
|
||||
expect(loginSource).toContain('class="login-card"')
|
||||
expect(loginSource).toContain('--login-brand-background: #172a46')
|
||||
expect(loginSource).toContain('background: var(--login-brand-background); color: #fff;')
|
||||
expect(loginSource).not.toMatch(/class="[^"]*workbench-(?:title|heading|panel|brand-mark)/)
|
||||
expect(loginSource).toContain('class="login-error workbench-notice workbench-tone" data-tone="rose"')
|
||||
expect(loginSource).toContain('aria-label="平台员工登录"')
|
||||
expect(loginSource).toContain('label-position="top" size="large"')
|
||||
|
||||
@@ -57,23 +57,23 @@ async function submit() {
|
||||
<template>
|
||||
<div class="login-page admin-workbench">
|
||||
<section class="login-intro">
|
||||
<div class="login-brand workbench-brand-mark"><img class="login-brand-logo" :src="baiyueLogoWhite" alt="柏悦物流" /></div>
|
||||
<div class="workbench-heading">
|
||||
<h1 class="workbench-title">柏悦物流管理后台</h1>
|
||||
<p class="login-intro-copy workbench-note">平台内部运营与管理系统</p>
|
||||
<div class="login-brand"><img class="login-brand-logo" :src="baiyueLogoWhite" alt="柏悦物流" /></div>
|
||||
<div>
|
||||
<h1>柏悦物流管理后台</h1>
|
||||
<p class="login-intro-copy">平台内部运营与管理系统</p>
|
||||
</div>
|
||||
<div class="login-security-note workbench-meta">
|
||||
<div class="login-security-note">
|
||||
<ShieldCheck :size="22" aria-hidden="true" />
|
||||
<span>平台内部系统,请使用授权账号登录</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="login-panel">
|
||||
<div class="login-card workbench-panel">
|
||||
<div class="login-mobile-brand workbench-brand-mark"><img :src="baiyueLogoWhite" alt="柏悦物流" /></div>
|
||||
<div class="login-card-icon workbench-icon"><LockKeyhole :size="24" aria-hidden="true" /></div>
|
||||
<h2 class="workbench-title">登录管理后台</h2>
|
||||
<p class="workbench-note">使用平台内部员工账号登录</p>
|
||||
<div class="login-card">
|
||||
<div class="login-mobile-brand"><img :src="baiyueLogoWhite" alt="柏悦物流" /></div>
|
||||
<div class="login-card-icon"><LockKeyhole :size="24" aria-hidden="true" /></div>
|
||||
<h2>登录管理后台</h2>
|
||||
<p>使用平台内部员工账号登录</p>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-position="top" size="large" aria-label="平台员工登录" @submit.prevent="submit">
|
||||
<el-form-item label="账号" prop="account" required>
|
||||
<el-input v-model.trim="form.account" placeholder="请输入账号" :prefix-icon="UserRound" autocomplete="username" maxlength="100" autofocus />
|
||||
@@ -92,17 +92,20 @@ async function submit() {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.login-page { display: grid; min-width: 0; min-height: 100svh; grid-template-columns: minmax(420px, 1.05fr) minmax(420px, .95fr); background: var(--workbench-surface); color: var(--workbench-text); }
|
||||
.login-intro { display: flex; min-width: 0; flex-direction: column; justify-content: space-between; padding: 46px clamp(42px, 7vw, 100px); }
|
||||
.login-brand { display: flex; width: fit-content; min-height: 64px; align-items: center; padding: 8px 20px; }
|
||||
/* Brand layout is local to login; workbench styles only supply shared controls and errors. */
|
||||
.login-page { --login-brand-background: #172a46; display: grid; min-width: 0; min-height: 100svh; grid-template-columns: minmax(420px, 1.05fr) minmax(420px, .95fr); background: var(--cc-surface); color: var(--cc-text); }
|
||||
.login-intro { display: flex; min-width: 0; flex-direction: column; justify-content: space-between; padding: 46px clamp(42px, 7vw, 100px); background: var(--login-brand-background); color: #fff; }
|
||||
.login-brand { display: flex; width: fit-content; min-height: 64px; align-items: center; }
|
||||
.login-brand-logo { display: block; width: 205px; height: 64px; object-fit: contain; object-position: left center; }
|
||||
.login-intro h1 { max-width: 560px; }
|
||||
.login-intro-copy { max-width: 560px; }
|
||||
.login-intro h1 { max-width: 560px; margin: 16px 0 14px; font-size: 40px; line-height: 1.2; overflow-wrap: anywhere; }
|
||||
.login-intro-copy { max-width: 560px; margin: 0; color: #cbd5e1; font-size: 15px; line-height: 1.7; overflow-wrap: anywhere; }
|
||||
.login-security-note { display: flex; align-items: center; gap: 10px; color: #cbd5e1; font-size: 13px; line-height: 1.6; }
|
||||
.login-security-note > svg { flex: 0 0 auto; }
|
||||
.login-panel { display: grid; min-width: 0; place-items: center; padding: 48px; background: var(--workbench-surface-tint); }
|
||||
.login-card.workbench-panel { width: min(410px, 100%); min-width: 0; padding: 32px; }
|
||||
.login-card h2 { margin-top: 22px; }
|
||||
.login-card > p { margin: 8px 0 30px; }
|
||||
.login-panel { display: grid; min-width: 0; place-items: center; padding: 48px; background: var(--cc-surface-muted); }
|
||||
.login-card { width: min(410px, 100%); min-width: 0; padding: 32px; border: 1px solid var(--cc-border); border-radius: var(--cc-radius-lg); background: var(--cc-surface); box-shadow: var(--cc-shadow-md); }
|
||||
.login-card-icon { display: grid; width: 48px; height: 48px; place-items: center; border-radius: var(--cc-radius-lg); background: var(--workbench-accent-soft); color: var(--workbench-primary); }
|
||||
.login-card h2 { margin: 22px 0 8px; font-size: 28px; line-height: 1.3; overflow-wrap: anywhere; }
|
||||
.login-card > p { margin: 0 0 30px; color: var(--cc-muted); font-size: 14px; line-height: 1.6; overflow-wrap: anywhere; }
|
||||
.login-mobile-brand { display: none; }
|
||||
.login-submit { width: 100%; min-height: 44px; }
|
||||
.login-submit :deep(> span) { display: flex; align-items: center; gap: 8px; }
|
||||
@@ -114,14 +117,14 @@ async function submit() {
|
||||
.login-page { grid-template-columns: 1fr; }
|
||||
.login-intro { display: none; }
|
||||
.login-panel { align-items: start; padding: 24px; }
|
||||
.login-card.workbench-panel { width: min(440px, 100%); margin: auto; padding: 28px; }
|
||||
.login-mobile-brand { display: flex; width: fit-content; min-height: 52px; align-items: center; margin-bottom: 28px; padding: 8px 16px; }
|
||||
.login-card { width: min(440px, 100%); margin: auto; padding: 28px; }
|
||||
.login-mobile-brand { display: flex; width: fit-content; min-height: 52px; align-items: center; margin-bottom: 28px; padding: 8px 16px; border-radius: var(--cc-radius-lg); background: var(--login-brand-background); }
|
||||
.login-mobile-brand img { display: block; width: 154px; height: 48px; object-fit: contain; object-position: left center; }
|
||||
}
|
||||
|
||||
@media (max-width: 360px) {
|
||||
.login-panel { padding: 12px; }
|
||||
.login-card.workbench-panel { padding: 20px; }
|
||||
.login-card { padding: 20px; }
|
||||
.login-mobile-brand { margin-bottom: 24px; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -245,6 +245,7 @@ const allColumns: DataTableColumn[] = [
|
||||
{ key: 'exceptionActive', label: '异常', width: 90, formatter: (row: ContainerBookingResource) => row.exceptionActive ? '异常' : '正常', afterColumn: 'containerNo' },
|
||||
{ key: 'planNo', prop: 'planNo', label: '装柜清单号', minWidth: 165, formatter: (row: ContainerBookingResource) => row.planNo || '—' },
|
||||
{ key: 'cabinetNo', label: '柜次号', minWidth: 180, slotName: 'cabinet' },
|
||||
{ key: 'createdAt', label: '创建时间', width: 165, formatter: (row: ContainerBookingResource) => formatTime(row.createdAt), afterColumn: 'cabinetNo' },
|
||||
{ key: 'pickupCode', label: 'PU码', minWidth: 140, formatter: (row: ContainerBookingResource) => row.pickupCode || '—' },
|
||||
{ key: 'oceanBooking', label: '真实订舱', minWidth: 170, slotName: 'oceanBooking' },
|
||||
{ key: 'warehouseName', prop: 'warehouseName', label: '装柜仓库', minWidth: 160 },
|
||||
@@ -267,8 +268,8 @@ const allColumns: DataTableColumn[] = [
|
||||
{ key: 'estimatedPortArrivalAt', label: '预计到港', minWidth: 160, slotName: 'estimatedPortArrivalAt' },
|
||||
{ key: 'actualPortArrivalAt', label: '实际到港', minWidth: 160, slotName: 'actualPortArrivalAt' },
|
||||
]
|
||||
const domesticColumnKeys = new Set(['actions', 'containerNo', 'planNo', 'planStatus', 'exceptionActive', 'cabinetNo', 'oceanBooking', 'warehouseName', 'channelGroupCode', 'channelName', 'billOfLadingNo', 'route', 'containerType', 'capacity', 'vessel', 'shippingCompanyName', 'loadingAt', 'estimatedSailingAt'])
|
||||
const overseasColumnKeys = new Set(['actions', 'emptyReturnWarning', 'containerNo', 'cabinetNo', 'destinationClearanceDocumentStatus', 'deliveryOrderDocumentStatus', 'pickupCode', 'terminalTruckingCompany', 'pickupAppointmentAt', 'actualPickupAt', 'estimatedEmptyReturnAt', 'actualEmptyReturnAt', 'billOfLadingNo', 'route', 'vessel', 'actualSailingAt', 'estimatedPortArrivalAt', 'actualPortArrivalAt'])
|
||||
const domesticColumnKeys = new Set(['actions', 'containerNo', 'planNo', 'planStatus', 'exceptionActive', 'cabinetNo', 'createdAt', 'oceanBooking', 'warehouseName', 'channelGroupCode', 'channelName', 'billOfLadingNo', 'route', 'containerType', 'capacity', 'vessel', 'shippingCompanyName', 'loadingAt', 'estimatedSailingAt'])
|
||||
const overseasColumnKeys = new Set(['actions', 'emptyReturnWarning', 'containerNo', 'cabinetNo', 'createdAt', 'destinationClearanceDocumentStatus', 'deliveryOrderDocumentStatus', 'pickupCode', 'terminalTruckingCompany', 'pickupAppointmentAt', 'actualPickupAt', 'estimatedEmptyReturnAt', 'actualEmptyReturnAt', 'billOfLadingNo', 'route', 'vessel', 'actualSailingAt', 'estimatedPortArrivalAt', 'actualPortArrivalAt'])
|
||||
const columns = computed(() => allColumns
|
||||
.filter((column) => (isDomestic.value ? domesticColumnKeys : overseasColumnKeys).has(String(column.key)))
|
||||
// 国内同时展示查看、运输信息、装柜流程和更多菜单,须为四个入口预留空间。
|
||||
|
||||
@@ -4,7 +4,7 @@ import { resolve } from 'node:path'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { ApiResponseError } from '@/shared/api/http'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { useAuthStore } from '@/core/auth/auth'
|
||||
@@ -367,6 +367,7 @@ describe('InboundFeePanel', () => {
|
||||
|
||||
it('confirms and then sends a calculated fee through the implemented backend transitions', async () => {
|
||||
vi.mocked(getLatestInboundFee).mockResolvedValue({ ...snapshot, feeStatus: 'CALCULATED', confirmedAt: null, sentAt: null })
|
||||
vi.mocked(getInboundMeasurement).mockRejectedValue(new ApiResponseError('无测量查看权限', 'A0403', 403))
|
||||
vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue(undefined as never)
|
||||
const wrapper = mountPanel()
|
||||
await flushPromises()
|
||||
@@ -379,10 +380,47 @@ describe('InboundFeePanel', () => {
|
||||
await wrapper.get('[data-testid="send-fee-button"]').trigger('click')
|
||||
await flushPromises()
|
||||
expect(sendInboundFee).toHaveBeenCalledWith('10001', '88', expect.stringMatching(/^fee-send-/))
|
||||
expect(getInboundMeasurement).not.toHaveBeenCalled()
|
||||
expect(wrapper.emitted('changed')).toHaveLength(2)
|
||||
expect(wrapper.find('[data-testid="send-fee-button"]').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it.each(['confirm', 'push'])('shows the backend review rejection for the %s entry point without sending', async (entry) => {
|
||||
const message = '测量数据尚未审核通过,请先完成测量审核后再确认费用'
|
||||
const showError = vi.spyOn(ElMessage, 'error')
|
||||
vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue(undefined as never)
|
||||
vi.mocked(getLatestInboundFee).mockResolvedValue({ ...snapshot, feeStatus: 'CALCULATED' })
|
||||
vi.mocked(confirmInboundFee).mockRejectedValue(new ApiResponseError(message, 'A0409', 200))
|
||||
const wrapper = mountPanel()
|
||||
await flushPromises()
|
||||
const button = entry === 'confirm'
|
||||
? wrapper.get('[data-testid="confirm-fee-button"]')
|
||||
: wrapper.findAll('button').find(button => button.text().includes('确认推送费用'))!
|
||||
await button.trigger('click')
|
||||
await flushPromises()
|
||||
expect(confirmInboundFee).toHaveBeenCalledTimes(1)
|
||||
expect(sendInboundFee).not.toHaveBeenCalled()
|
||||
expect(getInboundMeasurement).not.toHaveBeenCalled()
|
||||
expect(showError).toHaveBeenCalledWith(expect.objectContaining({ message }))
|
||||
expect(wrapper.find('[data-testid="send-fee-button"]').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('shows the backend review rejection when sending an already confirmed fee', async () => {
|
||||
const message = '测量数据尚未审核通过,请先完成测量审核后再发送客户'
|
||||
const showError = vi.spyOn(ElMessage, 'error')
|
||||
vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue(undefined as never)
|
||||
vi.mocked(getLatestInboundFee).mockResolvedValue({ ...snapshot, feeStatus: 'CONFIRMED' })
|
||||
vi.mocked(sendInboundFee).mockRejectedValue(new ApiResponseError(message, 'A0409', 200))
|
||||
const wrapper = mountPanel()
|
||||
await flushPromises()
|
||||
await wrapper.get('[data-testid="send-fee-button"]').trigger('click')
|
||||
await flushPromises()
|
||||
expect(sendInboundFee).toHaveBeenCalledTimes(1)
|
||||
expect(getInboundMeasurement).not.toHaveBeenCalled()
|
||||
expect(wrapper.emitted('changed')).toBeUndefined()
|
||||
expect(showError).toHaveBeenCalledWith(expect.objectContaining({ message }))
|
||||
})
|
||||
|
||||
it('does not expose fee transitions without their independent permissions', async () => {
|
||||
vi.mocked(getLatestInboundFee).mockResolvedValue({ ...snapshot, feeStatus: 'CALCULATED', confirmedAt: null, sentAt: null })
|
||||
const wrapper = mountPanel(['firstleg:fba-pricing:view', 'firstleg:fba-pricing:adjust'])
|
||||
@@ -392,6 +430,20 @@ describe('InboundFeePanel', () => {
|
||||
expect(wrapper.find('[data-testid="send-fee-button"]').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('confirms and publishes without requiring measurement-view permission', async () => {
|
||||
vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue(undefined as never)
|
||||
vi.mocked(getLatestInboundFee).mockResolvedValue({ ...snapshot, feeStatus: 'CALCULATED' })
|
||||
vi.mocked(getInboundMeasurement).mockRejectedValue(new ApiResponseError('无测量查看权限', 'A0403', 403))
|
||||
const wrapper = mountPanel(['firstleg:fba-pricing:view', 'firstleg:fba-pricing:confirm', 'firstleg:inbound:send-fee'])
|
||||
await flushPromises()
|
||||
await wrapper.findAll('button').find(button => button.text().includes('确认推送费用'))!.trigger('click')
|
||||
await flushPromises()
|
||||
expect(confirmInboundFee).toHaveBeenCalledWith('10001', '88', expect.stringMatching(/^fee-confirm-/))
|
||||
expect(sendInboundFee).toHaveBeenCalledWith('10001', '88', expect.stringMatching(/^fee-send-/))
|
||||
expect(getInboundMeasurement).not.toHaveBeenCalled()
|
||||
expect(wrapper.emitted('changed')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('submits a reasoned adjustment and emits the refreshed snapshot', async () => {
|
||||
vi.mocked(getLatestInboundFee).mockResolvedValue({ ...snapshot, feeStatus: 'CALCULATED' })
|
||||
const wrapper = mountPanel()
|
||||
|
||||
@@ -381,8 +381,8 @@ describe('InboundMeasurementPanel', () => {
|
||||
vi.mocked(getInboundMeasurement).mockResolvedValueOnce(measurement({ measurementId: null, items: [], weightResults: [] }))
|
||||
const wrapper = await mountPanel()
|
||||
expect(wrapper.find('.measurement-summary-header h4').text()).toBe('申报与实测对比')
|
||||
expect(wrapper.find('.measurement-declaration-header').text()).toContain('DOC-001')
|
||||
expect(wrapper.find('.measurement-section__heading .measurement-goods-name input').exists()).toBe(true)
|
||||
expect(wrapper.find('.measurement-declaration-header').text()).not.toContain('DOC-001')
|
||||
expect(wrapper.find('.measurement-section__heading .measurement-goods-name').exists()).toBe(false)
|
||||
expect(wrapper.find('.measurement-section__heading .measurement-section-actions').text()).toContain('手动录入箱规')
|
||||
expect(wrapper.findAll('.el-alert').some(alert => alert.text().includes('请先完成箱规测量'))).toBe(false)
|
||||
expect(wrapper.find('.measurement-panel__footer').text()).toContain('请先完成箱规测量')
|
||||
@@ -396,11 +396,11 @@ describe('InboundMeasurementPanel', () => {
|
||||
expect(comparison.text()).toContain('118.5')
|
||||
expect(comparison.text()).toContain('0.8')
|
||||
expect(comparison.findAll('tbody tr')[0].text()).toContain('10')
|
||||
expect(declaration.text()).toContain('原单据号码')
|
||||
expect(declaration.text()).toContain('DOC-001')
|
||||
expect(declaration.findAll('.measurement-readonly-field')).toHaveLength(1)
|
||||
expect(declaration.text()).not.toContain('原单据号码')
|
||||
expect(declaration.text()).not.toContain('DOC-001')
|
||||
expect(declaration.findAll('.measurement-readonly-field')).toHaveLength(0)
|
||||
expect(declaration.findAll('input')).toHaveLength(0)
|
||||
expect(wrapper.find('.measurement-items-section .measurement-goods-name input').exists()).toBe(true)
|
||||
expect(wrapper.find('.measurement-items-section .measurement-goods-name').exists()).toBe(false)
|
||||
expect(declaration.find('[aria-label="客户申报箱规明细"]').text()).toContain('蓝牙音箱')
|
||||
expect(declaration.find('[aria-label="客户申报箱规明细"]').text()).toContain('50 × 40 × 30')
|
||||
})
|
||||
@@ -418,18 +418,14 @@ describe('InboundMeasurementPanel', () => {
|
||||
|
||||
expect(declaration.find('input').exists()).toBe(false)
|
||||
expect(declaration.text()).toContain('蓝牙音箱')
|
||||
expect(declaration.findAll('.measurement-readonly-field')).toHaveLength(1)
|
||||
const actualGoods = wrapper.get('.measurement-items-section .measurement-goods-name')
|
||||
expect(actualGoods.text()).toContain('蓝牙音箱')
|
||||
expect(actualGoods.find('input').exists()).toBe(false)
|
||||
expect(declaration.findAll('.measurement-readonly-field')).toHaveLength(0)
|
||||
expect(wrapper.find('.measurement-items-section .measurement-goods-name').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('saves the edited actual goods name without overriding declaration facts', async () => {
|
||||
it('preserves the hidden actual goods name without overriding declaration facts', async () => {
|
||||
const wrapper = await mountPanel()
|
||||
expect(wrapper.get<HTMLInputElement>('input[aria-label="实际品名"]').element.value).toBe('蓝牙音箱')
|
||||
await wrapper.get('input[aria-label="实际品名"]').setValue(' 实测蓝牙音箱 ')
|
||||
expect(wrapper.get('.measurement-declaration').text()).toContain('蓝牙音箱')
|
||||
expect(wrapper.get('.measurement-declaration').text()).not.toContain('实测蓝牙音箱')
|
||||
expect(wrapper.text()).not.toContain('实际品名')
|
||||
await wrapper.get('input[aria-label="仓库修改外箱长"]').setValue('55')
|
||||
const save = wrapper.findAll('button').find((button) => button.text().includes('保存测量数据'))
|
||||
await save?.trigger('click')
|
||||
await flushPromises()
|
||||
@@ -437,7 +433,7 @@ describe('InboundMeasurementPanel', () => {
|
||||
expect(saveInboundMeasurementDraft).toHaveBeenCalledWith('10001', '7001', expect.objectContaining({
|
||||
measurementId: '9001',
|
||||
version: 2,
|
||||
actualGoodsName: '实测蓝牙音箱',
|
||||
actualGoodsName: '蓝牙音箱',
|
||||
items: expect.any(Array),
|
||||
}))
|
||||
const payload = vi.mocked(saveInboundMeasurementDraft).mock.calls[0]?.[2]
|
||||
@@ -446,6 +442,60 @@ describe('InboundMeasurementPanel', () => {
|
||||
expect(payload).not.toHaveProperty('originalVolumeCbm')
|
||||
})
|
||||
|
||||
it('preserves a 255-character goods name without exposing correction controls', async () => {
|
||||
const actualGoodsName = '品'.repeat(255)
|
||||
vi.mocked(getInboundMeasurement).mockResolvedValueOnce(measurement({ actualGoodsName }))
|
||||
const wrapper = await mountPanel()
|
||||
expect(wrapper.find('input[aria-label="实际品名"]').exists()).toBe(false)
|
||||
await wrapper.get('input[aria-label="仓库修改外箱长"]').setValue('55')
|
||||
await wrapper.findAll('button').find(button => button.text().includes('保存测量数据'))!.trigger('click')
|
||||
await flushPromises()
|
||||
expect(saveInboundMeasurementDraft).toHaveBeenCalledWith('10001', '7001', expect.objectContaining({ actualGoodsName }))
|
||||
})
|
||||
|
||||
it('blocks an overlong or cleared goods name and keeps its correction editable until a successful retry', async () => {
|
||||
const actualGoodsName = '品'.repeat(256)
|
||||
vi.mocked(getInboundMeasurement)
|
||||
.mockResolvedValueOnce(measurement({ actualGoodsName }))
|
||||
.mockResolvedValueOnce(measurement({ actualGoodsName: '精简后的品名', version: 3 }))
|
||||
vi.mocked(saveInboundMeasurementDraft).mockRejectedValueOnce(new ApiResponseError('资料保存失败', 'S0503'))
|
||||
const wrapper = await mountPanel()
|
||||
const input = () => wrapper.get<HTMLInputElement>('input[aria-label="实际品名"]')
|
||||
const save = () => wrapper.findAll('button').find(button => button.text().includes('保存测量数据'))!
|
||||
expect(input().element.value).toBe(actualGoodsName)
|
||||
expect(wrapper.get('.measurement-panel__footer').text()).toContain('实际品名超过255字')
|
||||
await save().trigger('click'); await flushPromises()
|
||||
expect(saveInboundMeasurementDraft).not.toHaveBeenCalled()
|
||||
await (wrapper.vm as unknown as { submitForReview(): Promise<void> }).submitForReview()
|
||||
expect(submitInboundMeasurement).not.toHaveBeenCalled()
|
||||
|
||||
await input().setValue('')
|
||||
await save().trigger('click'); await flushPromises()
|
||||
expect(saveInboundMeasurementDraft).not.toHaveBeenCalled()
|
||||
expect(wrapper.get('.measurement-panel__footer').text()).toContain('请填写精简后的实际品名')
|
||||
|
||||
await input().setValue(' 精简后的品名 ')
|
||||
expect(wrapper.find('input[aria-label="实际品名"]').exists()).toBe(true)
|
||||
expect(wrapper.get('.measurement-declaration').text()).toContain('蓝牙音箱')
|
||||
await save().trigger('click'); await flushPromises()
|
||||
expect(saveInboundMeasurementDraft).toHaveBeenCalledWith('10001', '7001', expect.objectContaining({ actualGoodsName: '精简后的品名' }))
|
||||
expect(input().element.value).toBe(' 精简后的品名 ')
|
||||
expect(wrapper.emitted('saved')).toBeUndefined()
|
||||
|
||||
await save().trigger('click'); await flushPromises()
|
||||
expect(saveInboundMeasurementDraft).toHaveBeenCalledTimes(2)
|
||||
expect(updateConfirmedPackageCount).not.toHaveBeenCalled()
|
||||
expect(wrapper.find('input[aria-label="实际品名"]').exists()).toBe(false)
|
||||
expect(wrapper.emitted('saved')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('does not expose goods name correction without measurement edit permission', async () => {
|
||||
vi.mocked(getInboundMeasurement).mockResolvedValueOnce(measurement({ actualGoodsName: '品'.repeat(256) }))
|
||||
const wrapper = await mountPanel(['platform:firstleg:inbound:measure:view'])
|
||||
expect(wrapper.find('input[aria-label="实际品名"]').exists()).toBe(false)
|
||||
expect(saveInboundMeasurementDraft).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('allows saving one warehouse outer dimension against the measured sequence', async () => {
|
||||
const wrapper = await mountPanel()
|
||||
expect(wrapper.text()).toContain('001')
|
||||
@@ -634,7 +684,7 @@ describe('InboundMeasurementPanel', () => {
|
||||
const wrapper = await mountPanel()
|
||||
expect(wrapper.find('.measurement-declaration input').exists()).toBe(false)
|
||||
expect(wrapper.find('.measurement-items-section .measurement-expected-pallets input').exists()).toBe(true)
|
||||
expect(wrapper.find('.measurement-items-section .measurement-goods-name input').exists()).toBe(true)
|
||||
expect(wrapper.find('.measurement-items-section .measurement-goods-name').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('requires at least one confirmed weight image before submitting for review', async () => {
|
||||
|
||||
@@ -149,12 +149,21 @@ const packageCountDifference = computed(() => expectedPackageCount.value == null
|
||||
const packageCountMatched = computed(() => packageCountDifference.value === null || packageCountDifference.value === 0)
|
||||
const hasConfirmedWeightResult = computed(() => confirmedWeightAvailable.value)
|
||||
const isDropship = computed(() => detail.value?.fulfillmentType === 'ONE_CLICK_DROPSHIP')
|
||||
// 以服务端原值决定修正入口,避免输入缩短后立即收起;保存成功重读后才隐藏。
|
||||
const needsGoodsNameCorrection = computed(() => (detail.value?.actualGoodsName?.trim().length ?? 0) > 255)
|
||||
const goodsNameCorrectionError = computed(() => {
|
||||
const length = form.actualGoodsName.trim().length
|
||||
if (length > 255) return '实际品名超过255字,请先精简后再保存'
|
||||
if (needsGoodsNameCorrection.value && !length) return '请填写精简后的实际品名,最多255字'
|
||||
return ''
|
||||
})
|
||||
const expectedPalletCountValid = computed(() => !isDropship.value || (Number.isInteger(form.expectedPalletCount) && Number(form.expectedPalletCount) >= 1))
|
||||
const maySubmit = computed(() => canSubmit.value && submitEditableProcess.value && Boolean(form.measurementId) && packageCountMatched.value && hasConfirmedWeightResult.value && expectedPalletCountValid.value && !loading.value && !saving.value)
|
||||
const maySubmit = computed(() => canSubmit.value && submitEditableProcess.value && Boolean(form.measurementId) && packageCountMatched.value && hasConfirmedWeightResult.value && expectedPalletCountValid.value && !goodsNameCorrectionError.value && !loading.value && !saving.value)
|
||||
const submissionBlockReason = computed(() => {
|
||||
if (!canSubmit.value || !detail.value || loading.value) return ''
|
||||
if (detail.value.processStatus === 'PREPARING') return '订单尚待客服审核,审核通过后才能提交测量审核'
|
||||
if (!submitEditableProcess.value) return `当前${processStatusLabel(detail.value.processStatus)},不能提交测量审核`
|
||||
if (goodsNameCorrectionError.value) return goodsNameCorrectionError.value
|
||||
if (!form.measurementId) return hasConfirmedWeightResult.value ? '请先完成箱规测量并保存测量数据' : '请先完成箱规测量,并上传确认至少一张称重图片'
|
||||
if (!hasConfirmedWeightResult.value) return '即使只有一箱,也必须上传并确认至少一张称重图片后才能提交审核'
|
||||
if (!packageCountMatched.value) return '确认箱数合计必须与订单总箱数一致后才能提交审核'
|
||||
@@ -416,6 +425,7 @@ async function reloadLatest() {
|
||||
function validateDraft() {
|
||||
const errors: Record<string, string[]> = {}
|
||||
const general: string[] = []
|
||||
if (goodsNameCorrectionError.value) general.push(goodsNameCorrectionError.value)
|
||||
if (!form.items.length) general.push('请至少保留一条实际扫描箱测量记录')
|
||||
if (isDropship.value && (!Number.isInteger(form.expectedPalletCount) || Number(form.expectedPalletCount) < 1 || Number(form.expectedPalletCount) > 1000)) general.push('一件代发预计托数必须是 1 至 1,000 的整数')
|
||||
else if (isDropship.value && Number(form.expectedPalletCount) > confirmedPackageCount.value) general.push('一件代发预计托数不能超过实测箱数')
|
||||
@@ -759,7 +769,6 @@ defineExpose({ saveDraft, submitForReview, updateWeightEvidence, submissionBlock
|
||||
<section class="measurement-declaration" aria-labelledby="measurement-declaration-title">
|
||||
<header class="measurement-declaration-header">
|
||||
<h4 id="measurement-declaration-title">客户申报明细</h4>
|
||||
<div class="measurement-readonly-field measurement-document-number"><span>原单据号码</span><strong>{{ detail.originalDocumentNo || '—' }}</strong></div>
|
||||
</header>
|
||||
<el-table
|
||||
v-if="detail.latestDeclaration?.items.length"
|
||||
@@ -783,9 +792,8 @@ defineExpose({ saveDraft, submitForReview, updateWeightEvidence, submissionBlock
|
||||
<div class="measurement-section__heading">
|
||||
<div><h3>平台实测箱规</h3></div>
|
||||
|
||||
<div class="measurement-data-grid measurement-platform-fields">
|
||||
<label v-if="mayMaintain" class="measurement-goods-name"><span>实际品名</span><el-input v-model="form.actualGoodsName" aria-label="实际品名" placeholder="请输入实际品名" maxlength="255" /></label>
|
||||
<div v-else class="measurement-readonly-field measurement-goods-name"><span>实际品名</span><strong>{{ detail.actualGoodsName || '—' }}</strong></div>
|
||||
<div v-if="isDropship || (needsGoodsNameCorrection && mayMaintain)" class="measurement-data-grid measurement-platform-fields">
|
||||
<label v-if="needsGoodsNameCorrection && mayMaintain" class="measurement-goods-name"><span>实际品名(最多255字)</span><el-input v-model="form.actualGoodsName" aria-label="实际品名" placeholder="请精简汇总品名后保存" maxlength="255" show-word-limit :disabled="saving" /></label>
|
||||
<label v-if="isDropship && mayMaintain" class="measurement-expected-pallets"><span>预计托数</span><el-input-number v-model="form.expectedPalletCount" placeholder="请输入预计托数" :min="1" :max="1000" :step="1" step-strictly controls-position="right" /></label>
|
||||
<div v-else-if="isDropship" class="measurement-readonly-field measurement-expected-pallets"><span>预计托数</span><strong>{{ detail.expectedPalletCount ?? '—' }}</strong></div>
|
||||
</div>
|
||||
|
||||
@@ -20,9 +20,6 @@
|
||||
.inbound-measurement-workspace-dialog.inbound-density .measurement-summary-header h4,
|
||||
.inbound-measurement-workspace-dialog.inbound-density .measurement-declaration-header h4 { flex-shrink: 0; margin: 0; }
|
||||
.inbound-measurement-workspace-dialog.inbound-density .measurement-declaration-header { display: flex; align-items: baseline; gap: var(--admin-space-6, 16px); }
|
||||
.inbound-measurement-workspace-dialog.inbound-density .measurement-document-number { display: flex; flex: 1; align-items: baseline; gap: var(--admin-space-3, 8px); }
|
||||
.inbound-measurement-workspace-dialog.inbound-density .measurement-document-number > span { flex-shrink: 0; }
|
||||
.inbound-measurement-workspace-dialog.inbound-density .measurement-document-number strong { min-width: 0; min-height: 0; padding: 0; }
|
||||
.inbound-measurement-workspace-dialog.inbound-density .measurement-items-section .measurement-section__heading { flex-wrap: wrap; gap: var(--admin-space-3, 8px) var(--admin-space-6, 16px); }
|
||||
.inbound-measurement-workspace-dialog.inbound-density .measurement-platform-fields { display: flex; flex: 1 1 520px; align-items: center; flex-wrap: wrap; margin: 0; gap: var(--admin-space-5, 12px); }
|
||||
.inbound-measurement-workspace-dialog.inbound-density .measurement-platform-fields > label,
|
||||
|
||||
@@ -226,7 +226,7 @@ onMounted(() => {
|
||||
<ContainerSimulationCreate :dense="isPreload" v-if="canCreate" success-message="模拟柜及预装清单已创建" @created="onSimulationCreated" />
|
||||
</template>
|
||||
<template #actions="{ row }"><div class="row-actions density-actions admin-text-actions" :class="{ 'density-actions': isPreload }">
|
||||
<el-button v-if="canView" :class="{ 'density-action': isPreload }" size="small" :type="isPreload ? undefined : 'primary'" :plain="!isPreload" @click="openDetail(row)">查看</el-button>
|
||||
<el-button v-if="canView" :class="{ 'density-action': isPreload }" size="small" :type="isPreload ? undefined : 'primary'" :plain="!isPreload" @click="openDetail(row)">{{ isPreload ? '编辑' : '查看' }}</el-button>
|
||||
<el-button v-if="canView && !isPreload" class="density-action" size="small" @click="printConfirmed(row as LoadingListSummary)">打印</el-button>
|
||||
</div></template>
|
||||
<template #customers="{ row }"><LineClampText :content="(row as PreloadSummary).customers.map((item) => item.customerName || '客户信息缺失').join('、')" /></template>
|
||||
|
||||
@@ -24,9 +24,22 @@ it.each([['本趟承运公司', '555-0123'], [null, null]])('shows continuous ma
|
||||
const field = (label: string) => wrapper.findAllComponents({ name: 'ElFormItem' }).find(item => item.props('label') === label)!.getComponent({ name: 'ElInput' })
|
||||
expect(field('拖车公司名称').props('modelValue')).toBe(carrierName || '—'); expect(field('拖车公司名称').props('readonly')).toBe(true)
|
||||
expect(field('联系电话').props('modelValue')).toBe(phone || '—'); expect(field('联系电话').props('readonly')).toBe(true)
|
||||
expect(wrapper.text()).toContain('预计任务结束')
|
||||
expect(listVehicles).toHaveBeenCalled(); expect(listDrivers).not.toHaveBeenCalled()
|
||||
expect(wrapper.emitted('update:vehicle')).toBeUndefined(); expect(wrapper.emitted('update:driver')).toBeUndefined(); wrapper.unmount()
|
||||
})
|
||||
it.each(['MASTER', 'EXTERNAL'] as const)('lets a %s driver fill a missing trip end and respects the disabled state', async mode => {
|
||||
const value = resources(); value.driver.mode = mode; value.driver.evidence!.expectedTaskEndAt = null
|
||||
const wrapper = mount(DispatchResourceFields, { props: { ...value, disabled: false, continuous: true }, global: { plugins: [ElementPlus] } })
|
||||
const endField = wrapper.findAllComponents({ name: 'ElFormItem' }).find(item => item.props('label') === '预计任务结束')!
|
||||
const picker = endField.getComponent({ name: 'ElDatePicker' })
|
||||
picker.vm.$emit('update:modelValue', '2030-01-01T17:00:00Z'); await flushPromises()
|
||||
expect(value.driver.evidence!.expectedTaskEndAt).toBe('2030-01-01T17:00:00Z')
|
||||
expect(wrapper.findAllComponents({ name: 'ElFormItem' }).some(item => item.props('label') === '驾驶证到期日')).toBe(mode === 'EXTERNAL')
|
||||
await wrapper.setProps({ disabled: true })
|
||||
expect(picker.props('disabled')).toBe(true)
|
||||
wrapper.unmount()
|
||||
})
|
||||
it('keeps master company and phone fields out of the existing non-continuous creation form', async () => {
|
||||
const value = resources(); value.vehicle.mode = 'MASTER'; value.driver.mode = 'MASTER'
|
||||
const wrapper = mount(DispatchResourceFields, { props: { vehicle: value.vehicle, driver: value.driver, disabled: false }, global: { plugins: [ElementPlus] } }); await flushPromises()
|
||||
|
||||
@@ -31,7 +31,7 @@ function confirmedAt(appointment: DispatchDestinationAppointment) {
|
||||
<div class="density-dialog-sections">
|
||||
<section class="density-dialog-section" aria-label="本车资源登记">
|
||||
<h3 class="density-dialog-section__title">车辆与司机</h3>
|
||||
<table class="density-table density-table--editable maintenance-table"><tbody><tr><td><el-form-item label="排车单号"><el-input :model-value="detail.dispatchNo" readonly /></el-form-item></td><td><el-form-item label="发车时间"><el-input :model-value="formatOverseasDateTime(detail.dispatchInfo.departedAt)" readonly /><DispatchTimeZonePreview :value="detail.dispatchInfo.departedAt" label="发车时间" /></el-form-item></td></tr></tbody></table>
|
||||
<table class="density-table density-table--editable maintenance-table"><tbody><tr><td colspan="2"><el-form-item label="排车单号"><el-input :model-value="detail.dispatchNo" readonly /></el-form-item></td></tr></tbody></table>
|
||||
<DispatchResourceFields :dispatch-no="detail.dispatchNo" continuous v-model:vehicle="vehicle" v-model:driver="driver" :disabled="disabled" />
|
||||
</section>
|
||||
<section class="density-dialog-section" aria-label="本车时间登记">
|
||||
|
||||
@@ -60,6 +60,7 @@ describe('dispatch workflow native layout', () => {
|
||||
const fields = dialog.findComponent(DispatchResourceFields)
|
||||
const selected = { ...resources().driver, mode: 'MASTER' as const, driverId: '22', evidence: { ...resources().driver.evidence!, fileIds: [], photoFileIds: [], masterLicenseFileId: '33' } }
|
||||
fields.vm.$emit('update:driver', selected); await flushPromises()
|
||||
expect(dialog.text()).toContain('预计任务结束')
|
||||
const license = () => dialog.findAllComponents(DispatchEvidenceFiles).find(item => item.props('kind') === 'DRIVER')!
|
||||
expect(license().find('img').attributes('alt')).toBe('驾驶证照片:驾驶证照片')
|
||||
expect(downloadDriverLicense).toHaveBeenCalledExactlyOnceWith('22', '33')
|
||||
@@ -70,6 +71,17 @@ describe('dispatch workflow native layout', () => {
|
||||
expect(license().find('img').exists()).toBe(false)
|
||||
expect(license().text()).toContain('暂无照片')
|
||||
})
|
||||
it('saves the entered task end for a master driver in the maintenance command', async () => {
|
||||
const saved = resources()
|
||||
saved.driver = { ...saved.driver, mode: 'MASTER', driverId: '22', evidence: { ...saved.driver.evidence!, expectedTaskEndAt: null } }
|
||||
const { dialog } = await render('EDIT', undefined, false, { vehicleInfo: saved })
|
||||
field(dialog, '预计任务结束').getComponent({ name: 'ElDatePicker' }).vm.$emit('update:modelValue', '2030-01-01T17:00:00Z')
|
||||
await flushPromises()
|
||||
dialog.vm.$emit('confirm'); await flushPromises()
|
||||
expect(api.updateDispatch).toHaveBeenCalledWith('PC/1', expect.objectContaining({
|
||||
driver: expect.objectContaining({ mode: 'MASTER', driverId: '22', evidence: expect.objectContaining({ expectedTaskEndAt: '2030-01-01T17:00:00Z' }) }),
|
||||
}))
|
||||
})
|
||||
it('reads an already frozen master license through the dispatch permission without a new driver directory request', async () => {
|
||||
const saved = resources()
|
||||
saved.driver = { ...saved.driver, mode: 'MASTER', driverId: '22', evidence: { ...saved.driver.evidence!, fileIds: ['33'], masterLicenseFileId: '33' } }
|
||||
@@ -151,7 +163,7 @@ describe('dispatch workflow native layout', () => {
|
||||
const { dialog } = await render('EDIT', undefined, false, { dispatchInfo: { ...source.dispatchInfo,
|
||||
departedAt: '2026-07-01T15:00:00Z', plannedDepartureAt: '2026-07-01T16:00:00Z', estimatedArrivalAt: '2026-07-01T17:00:00Z',
|
||||
} })
|
||||
expect(field(dialog, '发车时间').get('[aria-label="发车时间时区换算"]').text()).toBe('美东2026-07-01 11:00:00美西2026-07-01 08:00:00')
|
||||
expect(dialog.find('[aria-label="发车时间时区换算"]').exists()).toBe(false)
|
||||
expect(field(dialog, '计划发车时间').get('[aria-label="计划发车时间时区换算"]').text()).toBe('美东2026-07-01 12:00:00美西2026-07-01 09:00:00')
|
||||
expect(field(dialog, '预计到达时间').get('[aria-label="预计到达时间时区换算"]').text()).toBe('美东2026-07-01 13:00:00美西2026-07-01 10:00:00')
|
||||
expect(field(dialog, '预计任务结束').get('[aria-label="预计任务结束时区换算"]').text()).toBe('美东2026-11-01 05:00:00美西2026-11-01 02:00:00')
|
||||
@@ -178,7 +190,8 @@ describe('dispatch workflow native layout', () => {
|
||||
for (const [label, value] of [['派送方式', '外配'], ['提货方式', '一提多卸'], ['装车类型', '混装'], ['车辆类型', '厢式货车']]) field(dialog, label!).getComponent({ name: 'ElSelect' }).vm.$emit('update:modelValue', value)
|
||||
await field(dialog, '车队邮箱').get('input').setValue(' fleet@example.test ')
|
||||
await field(dialog, '预约账号').get('input').setValue(' fleet-account ')
|
||||
expect(field(dialog, '发车时间').get('input').attributes('readonly')).toBeDefined()
|
||||
expect(field(dialog, '发车时间')).toBeUndefined()
|
||||
expect(field(dialog, '排车单号').get('input').attributes('readonly')).toBeDefined()
|
||||
expect(dialog.find('textarea[aria-label="本次操作备注"]').exists()).toBe(false)
|
||||
await dialog.get('button[aria-label="展开全屏"]').trigger('click'); await flushPromises()
|
||||
dialog.vm.$emit('confirm'); await flushPromises()
|
||||
|
||||
@@ -3,9 +3,9 @@ import { expect, it } from 'vitest'
|
||||
import { createAppointmentForm } from '../appointmentForm'
|
||||
import FbaAppointmentFields from './FbaAppointmentFields.vue'
|
||||
|
||||
it('removes confirmation time and offset inputs only for the new appointment flow while retaining planned dates', async () => {
|
||||
it('keeps only the planned date for new appointments and allows time fields in other flows', async () => {
|
||||
const wrapper = mount(FbaAppointmentFields, {
|
||||
props: { model: createAppointmentForm(), continuous: true, hideConfirmationTimes: true, timezone: 'America/New_York' },
|
||||
props: { model: createAppointmentForm(), continuous: true, dateOnly: true, timezone: 'America/New_York' },
|
||||
global: { stubs: {
|
||||
ElForm: { template: '<form><slot /></form>' },
|
||||
ElFormItem: { props: ['label'], template: '<label>{{ label }}<slot /></label>' },
|
||||
@@ -16,10 +16,11 @@ it('removes confirmation time and offset inputs only for the new appointment flo
|
||||
})
|
||||
expect(wrapper.text()).toContain('外部约号')
|
||||
expect(wrapper.text()).toContain('计划送仓日期')
|
||||
expect(wrapper.text()).toContain('补充计划时段')
|
||||
expect(wrapper.findAll('[data-picker]')).toHaveLength(3)
|
||||
expect(wrapper.text()).not.toMatch(/对方确认开始时间|对方确认结束时间|确认开始时差|确认结束时差|夏令时/)
|
||||
await wrapper.setProps({ hideConfirmationTimes: false })
|
||||
expect(wrapper.findAll('[data-picker]')).toHaveLength(1)
|
||||
expect(wrapper.text()).not.toMatch(/补充计划时段|计划开始时段|计划结束时段|对方确认开始时间|对方确认结束时间|确认开始时差|确认结束时差|夏令时/)
|
||||
await wrapper.setProps({ dateOnly: false })
|
||||
expect(wrapper.text()).toContain('计划开始时段')
|
||||
expect(wrapper.text()).toContain('计划结束时段')
|
||||
expect(wrapper.text()).toContain('对方确认开始时间')
|
||||
expect(wrapper.findAll('[data-picker]')).toHaveLength(5)
|
||||
wrapper.unmount()
|
||||
|
||||
@@ -3,7 +3,7 @@ import InlineHelp from '@/shared/components/InlineHelp.vue'
|
||||
import { AppointmentConfirmationCargo, type PlanningSourceLookup } from '@/modules/appointment-calendar'
|
||||
import { ref, watchEffect } from 'vue'
|
||||
import type { AppointmentForm } from '../appointmentForm'
|
||||
const props = defineProps<{ model: AppointmentForm; appointmentNo?: string; timezone?: string | null; disabled?: boolean; continuous?: boolean; hideConfirmationTimes?: boolean; sources?: PlanningSourceLookup[] }>()
|
||||
const props = defineProps<{ model: AppointmentForm; appointmentNo?: string; timezone?: string | null; disabled?: boolean; continuous?: boolean; dateOnly?: boolean; sources?: PlanningSourceLookup[] }>()
|
||||
const emit = defineEmits<{ confirmationReady: [ready: boolean] }>()
|
||||
const cargo = ref<InstanceType<typeof AppointmentConfirmationCargo> | null>(null)
|
||||
watchEffect(() => {
|
||||
@@ -13,7 +13,7 @@ watchEffect(() => {
|
||||
defineExpose({ canLeave: () => cargo.value?.canLeave() ?? Promise.resolve(true) })
|
||||
</script>
|
||||
<template>
|
||||
<p v-if="!hideConfirmationTimes && model.appointmentStatus === 'SUCCESS' && timezone && /^(UTC|GMT|Etc\/(GMT|UTC)|[+-])/.test(timezone)" role="alert">目的仓当前只有固定时差,尚未设置地区时区。请到“仓库管理”编辑本次目的 FBA 仓,按实际地址设置地区时区(如 America/New_York),再重新打开预约;固定时差无法判断夏令时。</p>
|
||||
<p v-if="!dateOnly && model.appointmentStatus === 'SUCCESS' && timezone && /^(UTC|GMT|Etc\/(GMT|UTC)|[+-])/.test(timezone)" role="alert">目的仓当前只有固定时差,尚未设置地区时区。请到“仓库管理”编辑本次目的 FBA 仓,按实际地址设置地区时区(如 America/New_York),再重新打开预约;固定时差无法判断夏令时。</p>
|
||||
<AppointmentConfirmationCargo v-if="sources?.length" ref="cargo" :sources="sources" :active="model.appointmentStatus === 'SUCCESS'" :disabled="disabled" @ready="emit('confirmationReady', $event)" />
|
||||
<div v-if="continuous" class="density-dialog-sections">
|
||||
<section class="density-dialog-section" aria-label="预约来源信息"><h3 class="density-dialog-section__title">预约信息</h3><table class="density-table"><tbody><tr><th scope="row">系统单号</th><td>{{ appointmentNo || '保存后生成' }}</td><th scope="row">目的仓时区</th><td>{{ timezone || '待核实' }}</td></tr></tbody></table></section>
|
||||
@@ -22,13 +22,13 @@ defineExpose({ canLeave: () => cargo.value?.canLeave() ?? Promise.resolve(true)
|
||||
<tr><td><el-form-item label="计划送仓日期"><el-date-picker v-model="model.plannedDate" type="date" value-format="YYYY-MM-DD" placeholder="日期未定可留空" /></el-form-item></td><td><el-form-item label="预约状态"><el-input :model-value="({ SUCCESS: '已确认', CANCELLED: '已取消', FAILED: '未约成', PENDING: '待确认' })[model.appointmentStatus]" readonly /></el-form-item></td></tr>
|
||||
<template v-if="model.appointmentStatus === 'SUCCESS'">
|
||||
<tr><td colspan="2"><el-form-item label="外部约号" required><el-input v-model="model.confirmationNo" maxlength="128" placeholder="填写收货方提供的确认号" /></el-form-item></td></tr>
|
||||
<tr v-if="!hideConfirmationTimes"><td><el-form-item label="对方确认开始时间"><el-date-picker v-model="model.appointmentAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="目的仓当地时间" @change="model.startOffset = ''" /></el-form-item></td><td><el-form-item label="对方确认结束时间"><el-date-picker v-model="model.appointmentEndAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="未明确可留空" @change="model.endOffset = ''" /></el-form-item></td></tr>
|
||||
<tr v-if="!dateOnly"><td><el-form-item label="对方确认开始时间"><el-date-picker v-model="model.appointmentAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="目的仓当地时间" @change="model.startOffset = ''" /></el-form-item></td><td><el-form-item label="对方确认结束时间"><el-date-picker v-model="model.appointmentEndAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="未明确可留空" @change="model.endOffset = ''" /></el-form-item></td></tr>
|
||||
</template>
|
||||
<tr><td colspan="2"><el-form-item :label="model.appointmentStatus === 'FAILED' ? '原因' : '备注'" :required="model.appointmentStatus === 'FAILED'"><el-input v-model="model.appointmentRemark" aria-label="预约备注" maxlength="1000" /></el-form-item></td></tr>
|
||||
</tbody></table>
|
||||
<details><summary>{{ hideConfirmationTimes ? '补充计划时段' : '补充计划时段与夏令时信息' }}</summary><table class="density-table density-table--editable appointment-editor-table"><tbody>
|
||||
<details v-if="!dateOnly"><summary>补充计划时段与夏令时信息</summary><table class="density-table density-table--editable appointment-editor-table"><tbody>
|
||||
<tr><td><el-form-item label="计划开始时段"><el-date-picker v-model="model.plannedAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="只知道日期时不必填写" /></el-form-item></td><td><el-form-item label="计划结束时段"><el-date-picker v-model="model.plannedEndAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" /></el-form-item></td></tr>
|
||||
<template v-if="!hideConfirmationTimes && model.appointmentStatus === 'SUCCESS'"><tr><td colspan="2"><span class="density-dialog-note">当地时钟回拨、同一时间出现两次时,需按对方确认明确时差。</span><InlineHelp title="夏令时填写说明">仅重叠时刻需填写对方确认的时差;修改确认时间后须重新核对时差。</InlineHelp></td></tr><tr><td><el-form-item label="确认开始时差"><el-input v-model="model.startOffset" placeholder="仅重叠时刻需填,如 -07:00" /></el-form-item></td><td><el-form-item label="确认结束时差"><el-input v-model="model.endOffset" placeholder="仅重叠时刻需填,如 -08:00" /></el-form-item></td></tr></template>
|
||||
<template v-if="model.appointmentStatus === 'SUCCESS'"><tr><td colspan="2"><span class="density-dialog-note">当地时钟回拨、同一时间出现两次时,需按对方确认明确时差。</span><InlineHelp title="夏令时填写说明">仅重叠时刻需填写对方确认的时差;修改确认时间后须重新核对时差。</InlineHelp></td></tr><tr><td><el-form-item label="确认开始时差"><el-input v-model="model.startOffset" placeholder="仅重叠时刻需填,如 -07:00" /></el-form-item></td><td><el-form-item label="确认结束时差"><el-input v-model="model.endOffset" placeholder="仅重叠时刻需填,如 -08:00" /></el-form-item></td></tr></template>
|
||||
</tbody></table></details></el-form>
|
||||
</section>
|
||||
</div>
|
||||
@@ -39,15 +39,15 @@ defineExpose({ canLeave: () => cargo.value?.canLeave() ?? Promise.resolve(true)
|
||||
<el-form-item label="预约状态"><el-input :model-value="({ SUCCESS: '已确认', CANCELLED: '已取消', FAILED: '未约成', PENDING: '待确认' })[model.appointmentStatus]" readonly /></el-form-item>
|
||||
<template v-if="model.appointmentStatus === 'SUCCESS'">
|
||||
<el-form-item label="外部约号" required><el-input v-model="model.confirmationNo" maxlength="128" placeholder="填写收货方提供的确认号" /></el-form-item>
|
||||
<el-form-item v-if="!hideConfirmationTimes" label="对方确认开始时间"><el-date-picker v-model="model.appointmentAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="目的仓当地时间" @change="model.startOffset = ''" /></el-form-item>
|
||||
<el-form-item v-if="!hideConfirmationTimes" label="对方确认结束时间"><el-date-picker v-model="model.appointmentEndAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="未明确可留空" @change="model.endOffset = ''" /></el-form-item>
|
||||
<el-form-item v-if="!dateOnly" label="对方确认开始时间"><el-date-picker v-model="model.appointmentAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="目的仓当地时间" @change="model.startOffset = ''" /></el-form-item>
|
||||
<el-form-item v-if="!dateOnly" label="对方确认结束时间"><el-date-picker v-model="model.appointmentEndAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="未明确可留空" @change="model.endOffset = ''" /></el-form-item>
|
||||
</template>
|
||||
<el-form-item :label="model.appointmentStatus === 'FAILED' ? '原因' : '备注'" :required="model.appointmentStatus === 'FAILED'"><el-input v-model="model.appointmentRemark" aria-label="预约备注" maxlength="1000" /></el-form-item>
|
||||
<details class="appointment-fields__extra"><summary>{{ hideConfirmationTimes ? '补充计划时段' : '补充计划时段与夏令时信息' }}</summary>
|
||||
<details v-if="!dateOnly" class="appointment-fields__extra"><summary>补充计划时段与夏令时信息</summary>
|
||||
<div class="appointment-fields__extra-grid">
|
||||
<el-form-item label="计划开始时段"><el-date-picker v-model="model.plannedAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="只知道日期时不必填写" /></el-form-item>
|
||||
<el-form-item label="计划结束时段"><el-date-picker v-model="model.plannedEndAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" /></el-form-item>
|
||||
<template v-if="!hideConfirmationTimes && model.appointmentStatus === 'SUCCESS'"><p>当地时钟回拨、同一时间出现两次时,需按对方确认明确时差。</p>
|
||||
<template v-if="model.appointmentStatus === 'SUCCESS'"><p>当地时钟回拨、同一时间出现两次时,需按对方确认明确时差。</p>
|
||||
<el-form-item label="确认开始时差"><el-input v-model="model.startOffset" placeholder="仅重叠时刻需填,如 -07:00" /></el-form-item>
|
||||
<el-form-item label="确认结束时差"><el-input v-model="model.endOffset" placeholder="仅重叠时刻需填,如 -08:00" /></el-form-item>
|
||||
</template>
|
||||
|
||||
@@ -91,7 +91,7 @@ function feePreview(overrides: Partial<InboundFeePreview> = {}): InboundFeePrevi
|
||||
}
|
||||
|
||||
function savedOuterMeasurement() {
|
||||
return { actualPackageCount: 22, actualGrossWeightKg: 570.5, actualVolumeCbm: 3.6894,
|
||||
return { measureReviewStatus: 'APPROVED', actualPackageCount: 22, actualGrossWeightKg: 570.5, actualVolumeCbm: 3.6894,
|
||||
items: [{ id: '9001', itemNo: 'B1', packageCount: 22, lengthCm: 62, widthCm: 52, heightCm: 52,
|
||||
outerLengthCm: 63, outerWidthCm: 53, outerHeightCm: 53,
|
||||
calculatedOuterLengthCm: 63, calculatedOuterWidthCm: 53, calculatedOuterHeightCm: 53,
|
||||
@@ -266,6 +266,22 @@ describe('UnifiedOrdersView downstream fulfillment detail', () => {
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it.each(['NOT_SUBMITTED', 'PENDING', 'REJECTED'])('blocks fee publication before measurement approval: %s', async (measureReviewStatus) => {
|
||||
const wrapper = mountDetail()
|
||||
await flushPromises()
|
||||
const vm = wrapper.vm as unknown as FeeConfirmVm & { feeConfirmOpen: boolean }
|
||||
vm.measurementDetail = { ...savedOuterMeasurement(), measureReviewStatus }
|
||||
|
||||
await vm.openFeeConfirmDialog()
|
||||
expect(vm.feeConfirmOpen).toBe(false)
|
||||
await vm.executeConfirmAndSendFee()
|
||||
expect(previewInboundFee).not.toHaveBeenCalled()
|
||||
expect(calculateInboundFee).not.toHaveBeenCalled()
|
||||
expect(confirmInboundFee).not.toHaveBeenCalled()
|
||||
expect(sendInboundFee).not.toHaveBeenCalled()
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it.each([3.894, 4.25])('defaults billing to the saved outer volume and sends the confirmed value %s', async (confirmedVolume) => {
|
||||
const fee = { unitPrice: 13.6, chargeableQuantity: 650.298, chargeUnit: 'KG', roundingScale: 4, rateCode: 'RATE', feeStatus: 'CALCULATED' }
|
||||
vi.mocked(getLatestInboundFee).mockResolvedValue(fee as never)
|
||||
@@ -359,7 +375,7 @@ describe('UnifiedOrdersView downstream fulfillment detail', () => {
|
||||
await flushPromises()
|
||||
const vm = wrapper.vm as unknown as FeeConfirmVm
|
||||
vm.measurementDetail = {
|
||||
actualPackageCount: 10, actualGrossWeightKg: 100, actualVolumeCbm: 0.81,
|
||||
measureReviewStatus: 'APPROVED', actualPackageCount: 10, actualGrossWeightKg: 100, actualVolumeCbm: 0.81,
|
||||
items: [{ id: '9001', itemNo: 'B1', packageCount: 10, lengthCm: 50, widthCm: 40, heightCm: 30,
|
||||
outerLengthCm: null, outerWidthCm: null, outerHeightCm: null,
|
||||
calculatedOuterLengthCm: 54, calculatedOuterWidthCm: 44, calculatedOuterHeightCm: 34,
|
||||
|
||||
@@ -1741,6 +1741,10 @@ async function openFeeConfirmDialog() {
|
||||
ElMessage.warning('当前单据缺少入仓信息,暂不能发送费用')
|
||||
return
|
||||
}
|
||||
if (measurementDetail.value?.measureReviewStatus !== 'APPROVED') {
|
||||
ElMessage.warning('测量数据尚未审核通过,请先完成测量审核后再发送客户')
|
||||
return
|
||||
}
|
||||
if (!hasActualMeasurementData.value) {
|
||||
ElMessage.warning('暂无实际测量数据,不能确认并发送费用')
|
||||
return
|
||||
@@ -1866,6 +1870,10 @@ async function executeConfirmAndSendFee() {
|
||||
if (confirmFeeSending.value) return
|
||||
const summary = detail.value?.summary
|
||||
if (!summary?.inboundId) return
|
||||
if (measurementDetail.value?.measureReviewStatus !== 'APPROVED') {
|
||||
ElMessage.warning('测量数据尚未审核通过,请先完成测量审核后再发送客户')
|
||||
return
|
||||
}
|
||||
const selectedWeightKg = Number(feeConfirmWeight.value)
|
||||
const selectedVolumeCbm = Number(feeConfirmVolume.value)
|
||||
const selectedUnitPrice = Number(feeConfirmUnitPrice.value)
|
||||
|
||||
@@ -1325,8 +1325,8 @@ describe('approved Admin density ownership', () => {
|
||||
expect(sectionStyles.get('.inbound-density.admin-density-dialog .measurement-section')).toEqual({
|
||||
'padding-block': 'var(--admin-space-4, 10px)',
|
||||
})
|
||||
expect(layout).toContain('.measurement-document-number { display: flex; flex: 1;')
|
||||
expect(layout).toContain('.measurement-platform-fields { display: flex; flex: 1 1 520px;')
|
||||
expect(layout).toContain('.measurement-platform-fields > .measurement-goods-name { flex: 1 1 340px; }')
|
||||
expect(layout).toContain('.measurement-platform-fields > .measurement-expected-pallets { flex: 0 0 180px; }')
|
||||
for (const style of dialog.styles) {
|
||||
expect(style.content).not.toMatch(/\.measurement-(summary-section|metrics|declaration|items-section)\b/)
|
||||
|
||||
Reference in New Issue
Block a user