恢复账单明细入口并完善采购状态展示

This commit is contained in:
2026-10-30 18:50:56 +08:00
parent 119b4d6d66
commit 63193d04de
6 changed files with 52 additions and 12 deletions
@@ -126,7 +126,7 @@ function transportHarness(kind = 'trucking', failFirst = false, budgetCheck = as
const script = view.match(/<script setup lang="ts">([\s\S]*?)<\/script>/)[1]
const compiled = ts.transpileModule(script, { compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ESNext } }).outputText
.replace(/^import .*?;$/gm, '').replace(/^export \{\};$/gm, '')
const harness = runInNewContext(`${compiled}\n({ form, formRef, submit, detail, detailFields })`, {
const harness = runInNewContext(`${compiled}\n({ form, formRef, submit, detail, detailFields, configuration })`, {
defineProps: () => ({ kind, createPage: true, detailPage: false }),
withDefaults: props => props,
computed: getter => ({ get value() { return typeof getter === 'function' ? getter() : getter.get() } }),
@@ -176,13 +176,18 @@ test('truck retry submits the complete new contract with identical local times a
test('truck details render saved minute times and unknown pool demand without timezone conversion', () => {
const current = transportHarness()
current.detail.value = { summary: { vehicleType: null, pickupAt: '2026-10-20T09:30:00', deliveryAt: null }, originAddress: null, destinationAddress: null, remark: null }
current.detail.value = { summary: { vehicleType: null, palletCount: 3, packageCount: 0, pickupAt: '2026-10-20T09:30:00', deliveryAt: null }, originAddress: null, destinationAddress: null, remark: null }
const fields = new Map(current.detailFields.value)
assert.equal(fields.get('车辆类型'), '—')
assert.equal(fields.get('取货时间'), '2026-10-20 09:30')
assert.equal(fields.get('送达时间'), '—')
assert.equal(fields.has('柜型'), false)
assert.equal(fields.has('托盘数'), false)
assert.equal(fields.get('托数'), 3)
assert.equal(fields.has('件数'), false)
const columns = current.configuration.value.columns
assert.equal(columns.find(column => column.label === '托数').key, 'palletCount')
assert.equal(columns.some(column => column.key === 'packageCount'), false)
})
@@ -199,6 +204,12 @@ test('looseboard saves split contacts and both local minute times without obsole
assert.equal(payload.destinationContactPhone, '2')
assert.equal(payload.deliveryAt, '2026-10-20T15:30:00')
assert.equal(payload.idempotencyKey, current.calls[0].payload.idempotencyKey)
current.detail.value = { summary: { ...payload }, originAddress: null, destinationAddress: null, remark: null }
const fields = new Map(current.detailFields.value)
assert.equal(fields.get('件数'), 8)
assert.equal(fields.get('预计托数'), 3)
assert.equal(fields.has('托数'), false)
assert.equal(current.configuration.value.columns.some(column => column.key === 'palletCount'), false)
for (const field of ['palletRequired', 'transportType', 'transportMode', 'containerType', 'contactName', 'contactInfo', 'requestedDate']) assert.equal(Object.hasOwn(payload, field), false)
})
@@ -61,6 +61,7 @@ function transportColumns(looseboard: boolean): ColumnDefinition[] {
{ key: 'origin', label: originLabel, width: 150 },
{ key: 'destination', label: destinationLabel, width: 150 },
...(looseboard ? [{ key: 'productName', label: '商品名称', width: 150 }, { key: 'estimatedPalletCount', label: '预计托数', width: 100 }] : []),
...(!looseboard ? [{ key: 'palletCount', label: '托数', width: 80 }] : []),
{ key: 'totalWeightKg', label: '总重量(kg)', width: 120 },
{ key: 'budgetAmount', label: '预算总价', width: 110 },
...([
@@ -136,7 +137,7 @@ const detailFields = computed(() => {
] : [['车辆类型', vehicleTypeLabel(summary.vehicleType)]]),
[props.kind === 'trucking' ? '取货地' : '起运地', read(summary, 'origin')], [props.kind === 'trucking' ? '取货地址' : '起运地址', current.originAddress],
[props.kind === 'trucking' ? '送达地' : '目的地', read(summary, 'destination')], [props.kind === 'trucking' ? '送达地址' : '目的地址', current.destinationAddress],
['件数', read(summary, 'packageCount')], ['总重量(kg)', read(summary, 'totalWeightKg')],
[props.kind === 'trucking' ? '托数' : '件数', read(summary, props.kind === 'trucking' ? 'palletCount' : 'packageCount')], ['总重量(kg)', read(summary, 'totalWeightKg')],
['总体积(m³)', read(summary, 'totalVolumeCbm')], ['预算总价', money(read(summary, 'budgetAmount'), read(summary, 'currency'))],
[props.kind === 'trucking' ? '取货联系人' : '起运地联系人', summary.originContactName], [props.kind === 'trucking' ? '取货联系电话' : '起运地联系电话', summary.originContactPhone],
[props.kind === 'trucking' ? '送达联系人' : '目的地联系人', summary.destinationContactName], [props.kind === 'trucking' ? '送达联系电话' : '目的地联系电话', summary.destinationContactPhone],
@@ -294,7 +295,7 @@ onMounted(() => { if (props.detailPage) void loadDetail(); else if (!props.creat
<h2>{{ configuration.title }}下单</h2>
</div>
<el-button @click="returnToList">返回列表</el-button>
<el-button v-if="kind === 'looseboard'" @click="returnToList">返回列表</el-button>
</header>
<template v-if="mode === 'list'">
@@ -6,6 +6,7 @@ import { runInNewContext } from 'node:vm'
import ts from 'typescript'
import * as vue from 'vue'
import { parse, compileScript } from 'vue/compiler-sfc'
import { renderToString } from 'vue/server-renderer'
const { default: Schema } = createRequire(import.meta.resolve('element-plus'))('async-validator')
@@ -63,6 +64,27 @@ test('new requests get unique keys without restoring stored submissions', () =>
class ApiError extends Error { constructor(message, code, status) { super(message); this.code = code; this.status = status } }
const flush = async () => { await vue.nextTick(); await new Promise(resolve => setImmediate(resolve)); await vue.nextTick() }
test('shows only the active procurement rejection, hiding it after reupload, resubmission and confirmation', async () => {
const source = readFileSync(new URL('./views/ProcurementView.vue', import.meta.url), 'utf8')
const template = source.match(/<el-alert\b[^>]*title="采购结果驳回原因"[^>]*\/>/)?.[0]
assert.ok(template)
for (const [status, photosNeedUpdate, rejectionReason, visible] of [
['RECEIVING', true, '照片不清晰', true],
['RECEIVING', false, '照片不清晰', true],
['RECEIVING', false, null, false],
['WAIT_CUSTOMER_CONFIRM', true, '照片不清晰', false],
['CONFIRMED', true, '照片不清晰', false],
['RECEIVING', true, null, false],
]) {
const app = vue.createSSRApp({ render: vue.compile(template), setup: () => ({ workflow: { status, photosNeedUpdate, rejectionReason } }) })
app.component('ElAlert', { props: ['title', 'description'], setup: props => () => vue.h('aside', `${props.title} ${props.description}`) })
const html = await renderToString(app)
assert.equal(html.includes('采购结果驳回原因'), visible, `${status}, photosNeedUpdate=${photosNeedUpdate}`)
assert.equal(html.includes('照片不清晰'), visible)
}
})
function viewHarness({ createPage = false, detailPage = false, api = {}, storage = new Map() } = {}) {
const props = vue.reactive({ createPage, detailPage })
const route = vue.reactive({ params: { orderNo: detailPage ? 'PO-1' : '' } })
@@ -164,7 +164,7 @@ onBeforeUnmount(() => { active = false; ++requestSequence })
<header class="procurement-heading"><div><el-button link type="primary" @click="returnToList">← 返回列表</el-button><h2>{{ detail?.order.orderNo || '采购订单详情' }}</h2></div><div><el-tag v-if="detail" :type="statusTone(workflow?.status || detail.order.status)">{{ statusLabel(workflow?.status || detail.order.status, detail.order.procurementDirection) }}</el-tag><el-button :disabled="loading" @click="loadDetail">刷新</el-button></div></header>
<OrderFundingStatus v-if="detail" kind="procurement" :order-no="detail.order.orderNo" :status="detail.order.status" :can-retry="canCreate" @changed="loadDetail" />
<div v-if="error" class="procurement-error" role="alert"><span>{{ error }}</span><el-button :disabled="loading" @click="loadDetail">重新加载</el-button></div>
<template v-else-if="detail && workflow"><el-alert v-if="workflow.rejectionReason" title="采购结果驳回原因" :description="workflow.rejectionReason" type="warning" :closable="false" /><ProcurementDetailPanel :detail="detail" :workflow="workflow" /></template><el-skeleton v-else :rows="8" animated />
<template v-else-if="detail && workflow"><el-alert v-if="workflow.status === 'RECEIVING' && workflow.rejectionReason" title="采购结果驳回原因" :description="workflow.rejectionReason" type="warning" :closable="false" /><ProcurementDetailPanel :detail="detail" :workflow="workflow" /></template><el-skeleton v-else :rows="8" animated />
</section>
<section v-else class="procurement-create" aria-label="采购下单">
<header class="procurement-create-heading">