优化
This commit is contained in:
@@ -39,11 +39,24 @@ describe('OceanBookingDetailView interaction contract', () => {
|
||||
it('provides guidance for every free-text booking field', () => {
|
||||
for (const placeholder of [
|
||||
'请输入提单号(可选)', '请输入船名', '请输入航次', '例如:CNNGB', '例如:宁波港',
|
||||
'例如:USLAX', '例如:洛杉矶港', '请输入最终交货地点', '例如:CY-CY',
|
||||
'例如:PREPAID', '请输入订舱备注(可选)',
|
||||
'例如:USLAX', '例如:洛杉矶港', '请输入最终交货地点', '请输入订舱备注(可选)',
|
||||
]) expect(source).toContain(`placeholder="${placeholder}"`)
|
||||
})
|
||||
|
||||
it('uses compact choices and keeps capacity snapshots behind one specification control', () => {
|
||||
expect(source).toContain("const serviceTermOptions = ['CY-CY', 'CY-DOOR', 'DOOR-CY', 'DOOR-DOOR']")
|
||||
expect(source).toContain("const freightTermOptions = ['PREPAID', 'COLLECT']")
|
||||
expect(source).toContain("const containerTypeOptions = ['20GP', '40GP', '40HC', '40NR', '45HC']")
|
||||
expect(source).toContain('<el-select v-model="form.data.serviceTerm"')
|
||||
expect(source).toContain('<el-select v-model="form.data.freightTerm"')
|
||||
expect(source).toContain('<el-select v-model="container.containerType"')
|
||||
expect(source).toContain('<el-select v-model="container.containerKind"')
|
||||
expect(source).toContain(':aria-label="`设置箱型 ${index + 1} 的容量与限重`"')
|
||||
expect(source).toContain('function containerSpecificationLabel(')
|
||||
expect(source).not.toContain('<el-input v-model="form.data.serviceTerm"')
|
||||
expect(source).not.toContain('<el-input v-model="container.containerType"')
|
||||
})
|
||||
|
||||
it('captures channel, warehouse, capacity snapshots and uses the release DTO', () => {
|
||||
expect(source).toContain("listPlatformWarehouses({ page: 1, size: 200, warehouseType: 'DOMESTIC', status: 'ACTIVE' })")
|
||||
expect(source).toContain("warehouse.status === 'ACTIVE' && warehouse.warehouseType === 'DOMESTIC'")
|
||||
@@ -78,7 +91,7 @@ describe('OceanBookingDetailView interaction contract', () => {
|
||||
expect(source).toContain('function removeContainer(index: number) { if (form.data.containerRequirements.length > 1)')
|
||||
expect(source).toContain('<table class="container-table">')
|
||||
expect(source).toContain('.container-table-wrap{overflow-x:auto')
|
||||
expect(source).toContain('.container-table td :deep(.el-input),.container-table td :deep(.el-input-number){width:100%;min-width:0}')
|
||||
expect(source).toContain('.container-table td :deep(.el-select),.container-table td :deep(.el-input-number){width:100%;min-width:0}')
|
||||
expect(source).toContain(':controls="false" placeholder="请输入数量"')
|
||||
expect(source).not.toContain('class="container-row"')
|
||||
})
|
||||
@@ -95,11 +108,11 @@ describe('OceanBookingDetailView interaction contract', () => {
|
||||
expect(source).toContain('aria-label="航线船期"')
|
||||
expect(source).toContain('aria-label="条款与柜量"')
|
||||
expect(source).toContain('grid-template-columns:minmax(0,1.4fr) repeat(2,minmax(0,1fr))')
|
||||
expect(source).toContain('.booking-group__body--fields{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));column-gap:12px}')
|
||||
expect(source).toContain('.booking-group__body--fields,.booking-terms{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));column-gap:12px}')
|
||||
expect(source).toContain('class="booking-field--wide" label="交货地点"')
|
||||
expect(source).toContain('@media(max-width:720px){.booking-groups,.party-grid{grid-template-columns:1fr}')
|
||||
expect(source).toContain('@media(max-width:520px){.booking-group__body--fields{grid-template-columns:1fr}')
|
||||
expect(source).toContain('min-width:580px')
|
||||
expect(source).toContain('@media(max-width:520px){.booking-group__body--fields,.booking-terms{grid-template-columns:1fr}')
|
||||
expect(source).toContain('min-width:520px')
|
||||
})
|
||||
|
||||
it('keeps the three real booking parties in their own three-column section', () => {
|
||||
@@ -108,4 +121,12 @@ describe('OceanBookingDetailView interaction contract', () => {
|
||||
expect(source).toContain("{ value: 'NOTIFY_PARTY', label: '通知人' }")
|
||||
expect(source).toContain('.party-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr))')
|
||||
})
|
||||
|
||||
it('shows client-side validation failures instead of rendering an empty error alert', () => {
|
||||
expect(source).toContain(':title="actionError.message"')
|
||||
expect(source).toContain("actionError.value = { message: '请完善客户、装柜仓库和物流渠道等必填信息' }")
|
||||
expect(source).toContain("actionError.value = { message: '发货人、收货人和通知人名称均为必填项' }")
|
||||
expect(source).toContain('规格(必填)')
|
||||
expect(source).toContain('placeholder="单位名称(必填)"')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ArrowLeft, Plus, Save, Trash2 } from '@lucide/vue'
|
||||
import { ArrowLeft, Plus, Save, SlidersHorizontal, Trash2 } from '@lucide/vue'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { getApiErrorDetails } from '@/shared/api/http'
|
||||
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
||||
@@ -41,6 +41,13 @@ const partyTypes = [
|
||||
{ value: 'CONSIGNEE', label: '收货人' },
|
||||
{ value: 'NOTIFY_PARTY', label: '通知人' },
|
||||
] as const
|
||||
const serviceTermOptions = ['CY-CY', 'CY-DOOR', 'DOOR-CY', 'DOOR-DOOR']
|
||||
const freightTermOptions = ['PREPAID', 'COLLECT']
|
||||
const containerTypeOptions = ['20GP', '40GP', '40HC', '40NR', '45HC']
|
||||
const containerKindOptions = [
|
||||
{ value: 'FCL', label: '整箱 FCL' },
|
||||
{ value: 'LCL', label: '拼箱 LCL' },
|
||||
]
|
||||
const actionLabels: Record<OceanBookingAction, string> = {
|
||||
submit: '提交', confirm: '确认订舱', release: '确认放舱', fail: '标记失败', cancel: '取消订舱',
|
||||
'change-request': '申请改船', 'change-confirm': '确认改船', 'change-fail': '改船失败',
|
||||
@@ -209,7 +216,11 @@ async function load() {
|
||||
}
|
||||
async function save() {
|
||||
if (!canSave.value) return
|
||||
if (!await formRef.value?.validate().catch(() => false)) return
|
||||
actionError.value = null
|
||||
if (!await formRef.value?.validate().catch(() => false)) {
|
||||
actionError.value = { message: '请完善客户、装柜仓库和物流渠道等必填信息' }
|
||||
return
|
||||
}
|
||||
if (form.data.parties.some((party) => !party.partyName.trim())) {
|
||||
actionError.value = { message: '发货人、收货人和通知人名称均为必填项' }
|
||||
return
|
||||
@@ -312,6 +323,10 @@ function validateContainerRequirements() {
|
||||
}
|
||||
return true
|
||||
}
|
||||
function containerSpecificationLabel(container: OceanBookingDraftData['containerRequirements'][number]) {
|
||||
if (!(Number(container.capacityCbm) > 0) || !(Number(container.maxWeightKg) > 0)) return '设置规格'
|
||||
return `${container.capacityCbm} m³ / ${container.maxWeightKg} kg`
|
||||
}
|
||||
function addContainer() { form.data.containerRequirements.push(createBlankContainerRequirement()) }
|
||||
function removeContainer(index: number) { if (form.data.containerRequirements.length > 1) form.data.containerRequirements.splice(index, 1) }
|
||||
|
||||
@@ -332,7 +347,7 @@ onMounted(() => { void load(); void loadOptions() })
|
||||
|
||||
<LoadErrorState v-if="error" title="订舱详情加载失败" :message="error.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState>
|
||||
<template v-else>
|
||||
<el-alert v-if="actionError" type="error" :closable="false" show-icon><ApiErrorMeta :details="actionError" /></el-alert>
|
||||
<el-alert v-if="actionError" type="error" :closable="false" show-icon :title="actionError.message"><ApiErrorMeta :details="actionError" /></el-alert>
|
||||
<section class="surface-panel">
|
||||
<div class="section-title">
|
||||
<div><h2>订舱信息</h2><p v-if="detail && !editable">当前状态仅可查看,草稿状态才允许编辑。</p></div>
|
||||
@@ -370,11 +385,13 @@ onMounted(() => { void load(); void loadOptions() })
|
||||
</section>
|
||||
<section class="booking-group" aria-label="条款与柜量">
|
||||
<h3>条款与柜量</h3><div class="booking-group__body">
|
||||
<el-form-item label="服务条款"><el-input v-model="form.data.serviceTerm" placeholder="例如:CY-CY" /></el-form-item>
|
||||
<el-form-item label="运费条款"><el-input v-model="form.data.freightTerm" placeholder="例如:PREPAID" /></el-form-item>
|
||||
<div class="booking-terms">
|
||||
<el-form-item label="服务条款"><el-select v-model="form.data.serviceTerm" clearable placeholder="请选择服务条款"><el-option v-for="value in serviceTermOptions" :key="value" :label="value" :value="value" /></el-select></el-form-item>
|
||||
<el-form-item label="运费条款"><el-select v-model="form.data.freightTerm" clearable placeholder="请选择运费条款"><el-option v-for="value in freightTermOptions" :key="value" :label="value" :value="value" /></el-select></el-form-item>
|
||||
</div>
|
||||
<div class="container-table-wrap" aria-label="箱型箱量">
|
||||
<table class="container-table">
|
||||
<thead><tr><th scope="col">操作</th><th scope="col">箱型</th><th scope="col">箱种</th><th scope="col">容量(m³)</th><th scope="col">限重(kg)</th><th scope="col">数量</th></tr></thead>
|
||||
<thead><tr><th scope="col">操作</th><th scope="col">箱型</th><th scope="col">箱种</th><th scope="col">规格(必填)</th><th scope="col">数量</th></tr></thead>
|
||||
<tbody>
|
||||
<tr v-for="(container, index) in form.data.containerRequirements" :key="index">
|
||||
<td class="container-table__action">
|
||||
@@ -383,10 +400,18 @@ onMounted(() => { void load(); void loadOptions() })
|
||||
</el-tooltip>
|
||||
<span v-else aria-hidden="true">—</span>
|
||||
</td>
|
||||
<td><el-input v-model="container.containerType" :disabled="!editable" placeholder="如 40HC" /></td>
|
||||
<td><el-input v-model="container.containerKind" :disabled="!editable" placeholder="如 FCL" /></td>
|
||||
<td><el-input-number v-model="container.capacityCbm" :disabled="!editable" :min="0.01" :precision="2" :controls="false" placeholder="请输入容量" /></td>
|
||||
<td><el-input-number v-model="container.maxWeightKg" :disabled="!editable" :min="0.01" :precision="2" :controls="false" placeholder="请输入限重" /></td>
|
||||
<td><el-select v-model="container.containerType" :disabled="!editable" placeholder="请选择箱型"><el-option v-for="value in containerTypeOptions" :key="value" :label="value" :value="value" /></el-select></td>
|
||||
<td><el-select v-model="container.containerKind" :disabled="!editable" placeholder="请选择箱种"><el-option v-for="item in containerKindOptions" :key="item.value" :label="item.label" :value="item.value" /></el-select></td>
|
||||
<td class="container-table__specification">
|
||||
<el-popover v-if="editable" placement="bottom" :width="300" trigger="click">
|
||||
<template #reference><el-button size="small" plain :type="Number(container.capacityCbm) > 0 && Number(container.maxWeightKg) > 0 ? 'default' : 'warning'" :aria-label="`设置箱型 ${index + 1} 的容量与限重`"><SlidersHorizontal :size="14" />{{ containerSpecificationLabel(container) }}</el-button></template>
|
||||
<div class="container-specification-fields">
|
||||
<label><span>容量(m³)</span><el-input-number v-model="container.capacityCbm" :min="0.01" :precision="2" :controls="false" placeholder="请输入容量" /></label>
|
||||
<label><span>限重(kg)</span><el-input-number v-model="container.maxWeightKg" :min="0.01" :precision="2" :controls="false" placeholder="请输入限重" /></label>
|
||||
</div>
|
||||
</el-popover>
|
||||
<span v-else>{{ containerSpecificationLabel(container) }}</span>
|
||||
</td>
|
||||
<td><el-input-number v-model="container.quantity" :disabled="!editable" :min="1" :max="999" :controls="false" placeholder="请输入数量" /></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -403,8 +428,8 @@ onMounted(() => { void load(); void loadOptions() })
|
||||
<h2>订舱关系方</h2>
|
||||
<div class="party-grid">
|
||||
<div v-for="(party, index) in form.data.parties" :key="party.partyType" class="subsection">
|
||||
<h3>{{ partyTypes[index]?.label || party.partyType }}</h3>
|
||||
<el-input v-model="party.partyName" :disabled="!editable" placeholder="单位名称" />
|
||||
<h3><span class="required-mark" aria-hidden="true">*</span>{{ partyTypes[index]?.label || party.partyType }}</h3>
|
||||
<el-input v-model="party.partyName" :disabled="!editable" :aria-label="`${partyTypes[index]?.label || party.partyType}单位名称(必填)`" placeholder="单位名称(必填)" />
|
||||
<el-input v-model="party.contactName" :disabled="!editable" placeholder="联系人" />
|
||||
<el-input v-model="party.contactPhone" :disabled="!editable" placeholder="联系电话" />
|
||||
<el-input v-model="party.address" :disabled="!editable" placeholder="地址" />
|
||||
@@ -445,5 +470,5 @@ onMounted(() => { void load(); void loadOptions() })
|
||||
|
||||
<style scoped>
|
||||
.channel-code{display:block;margin-left:8px;color:var(--cc-muted);font-size:11px}.field-error,.field-hint{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-top:5px;color:var(--cc-muted);font-size:12px;line-height:20px}.field-error{color:var(--el-color-danger)}
|
||||
.booking-detail{min-width:0}.detail-heading{display:flex;min-width:0;align-items:flex-start;gap:16px}.booking-page-toolbar{min-width:0;flex:1}.section-title p,.history-panel p{margin:5px 0 0;color:var(--cc-muted);font-size:13px}.heading-status{display:flex;align-items:center;gap:8px}.surface-panel{padding:20px}.surface-panel h2{margin:0;font-size:16px}.section-title{display:flex;justify-content:space-between;gap:12px;align-items:flex-start;margin-bottom:18px}.actions{display:flex;flex-wrap:wrap;gap:8px}.booking-groups{display:grid;grid-template-columns:minmax(0,1.4fr) repeat(2,minmax(0,1fr));gap:16px}.booking-group{min-width:0;overflow:hidden;border:1px solid #e8edf3;border-radius:8px;background:#fff}.booking-group>h3{margin:0;padding:14px 16px;border-bottom:1px solid #eef2f6;color:#1f2937;font-size:14px}.booking-group__body{padding:16px}.booking-group__body--fields{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));column-gap:12px}.booking-field--wide{grid-column:1/-1}.party-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;margin-top:16px}.subsection{display:grid;gap:10px;padding:14px;border:1px solid var(--cc-border);border-radius:6px}.subsection h3{margin:0;font-size:14px}.container-table-wrap{overflow-x:auto;margin-bottom:10px}.container-table{width:100%;min-width:580px;table-layout:fixed;border-collapse:collapse;font-size:12px}.container-table th,.container-table td{padding:7px 6px;border-bottom:1px solid #eef2f6;text-align:left;vertical-align:middle}.container-table th{background:#fafafa;color:var(--cc-muted);font-weight:650;white-space:nowrap}.container-table th:first-child,.container-table td:first-child{width:56px}.container-table th:nth-child(2),.container-table td:nth-child(2){width:112px}.container-table th:nth-child(3),.container-table td:nth-child(3){width:88px}.container-table th:nth-child(4),.container-table td:nth-child(4),.container-table th:nth-child(5),.container-table td:nth-child(5){width:118px}.container-table th:nth-child(6),.container-table td:nth-child(6){width:78px}.container-table td :deep(.el-input),.container-table td :deep(.el-input-number){width:100%;min-width:0}.container-table__action{text-align:center!important}.container-table__action :deep(.el-button){width:28px;height:28px}.add-container{margin-bottom:18px}.history-panel strong{font-size:14px}@media(max-width:1599px){.booking-groups{grid-template-columns:repeat(2,minmax(0,1fr))}.booking-group:last-child{grid-column:1/-1}}@media(max-width:720px){.booking-groups,.party-grid{grid-template-columns:1fr}.booking-group:last-child{grid-column:auto}.detail-heading{flex-direction:column}.booking-page-toolbar{width:100%}.section-title{flex-direction:column}.actions{width:100%}}@media(max-width:520px){.booking-group__body--fields{grid-template-columns:1fr}.booking-field--wide{grid-column:auto}}@media(max-width:420px){.surface-panel{padding:12px}.booking-group__body{padding:14px}.actions :deep(.el-button){min-height:44px}}
|
||||
.booking-detail{min-width:0}.detail-heading{display:flex;min-width:0;align-items:flex-start;gap:16px}.booking-page-toolbar{min-width:0;flex:1}.section-title p,.history-panel p{margin:5px 0 0;color:var(--cc-muted);font-size:13px}.heading-status{display:flex;align-items:center;gap:8px}.surface-panel{padding:20px}.surface-panel h2{margin:0;font-size:16px}.section-title{display:flex;justify-content:space-between;gap:12px;align-items:flex-start;margin-bottom:18px}.actions{display:flex;flex-wrap:wrap;gap:8px}.booking-groups{display:grid;grid-template-columns:minmax(0,1.4fr) repeat(2,minmax(0,1fr));gap:16px}.booking-group{min-width:0;overflow:hidden;border:1px solid #e8edf3;border-radius:8px;background:#fff}.booking-group>h3{margin:0;padding:14px 16px;border-bottom:1px solid #eef2f6;color:#1f2937;font-size:14px}.booking-group__body{padding:16px}.booking-group__body--fields,.booking-terms{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));column-gap:12px}.booking-field--wide{grid-column:1/-1}.booking-terms :deep(.el-select){width:100%}.party-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;margin-top:16px}.subsection{display:grid;gap:10px;padding:14px;border:1px solid var(--cc-border);border-radius:6px}.subsection h3{margin:0;font-size:14px}.required-mark{margin-right:4px;color:var(--el-color-danger)}.container-table-wrap{overflow-x:auto;margin-bottom:10px}.container-table{width:100%;min-width:520px;table-layout:fixed;border-collapse:collapse;font-size:12px}.container-table th,.container-table td{padding:7px 6px;border-bottom:1px solid #eef2f6;text-align:left;vertical-align:middle}.container-table th{background:#fafafa;color:var(--cc-muted);font-weight:650;white-space:nowrap}.container-table th:first-child,.container-table td:first-child{width:56px}.container-table th:nth-child(2),.container-table td:nth-child(2){width:110px}.container-table th:nth-child(3),.container-table td:nth-child(3){width:110px}.container-table th:nth-child(4),.container-table td:nth-child(4){width:180px}.container-table th:nth-child(5),.container-table td:nth-child(5){width:78px}.container-table td :deep(.el-select),.container-table td :deep(.el-input-number){width:100%;min-width:0}.container-table__action{text-align:center!important}.container-table__action :deep(.el-button){width:28px;height:28px}.container-table__specification :deep(.el-button){max-width:100%;overflow:hidden}.container-table__specification :deep(.el-button span){overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.container-specification-fields{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}.container-specification-fields label{display:grid;gap:6px;color:var(--cc-muted);font-size:12px}.container-specification-fields :deep(.el-input-number){width:100%}.add-container{margin-bottom:18px}.history-panel strong{font-size:14px}@media(max-width:1599px){.booking-groups{grid-template-columns:repeat(2,minmax(0,1fr))}.booking-group:last-child{grid-column:1/-1}}@media(max-width:720px){.booking-groups,.party-grid{grid-template-columns:1fr}.booking-group:last-child{grid-column:auto}.detail-heading{flex-direction:column}.booking-page-toolbar{width:100%}.section-title{flex-direction:column}.actions{width:100%}}@media(max-width:520px){.booking-group__body--fields,.booking-terms{grid-template-columns:1fr}.booking-field--wide{grid-column:auto}}@media(max-width:420px){.surface-panel{padding:12px}.booking-group__body{padding:14px}.actions :deep(.el-button){min-height:44px}}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user