优化
This commit is contained in:
@@ -6,7 +6,7 @@ import { readFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { useAuthStore } from '@/core/auth/auth'
|
||||
import { createContainerFee, listApplicableFeeItems, listContainerCostDetails, listContainerFeePartnerOptions, listContainerFees } from '../api/containerFees'
|
||||
import { adjustContainerFee, createContainerFee, listApplicableFeeItems, listContainerCostDetails, listContainerFeePartnerOptions, listContainerFees } from '../api/containerFees'
|
||||
import ContainerFeesPanel from './ContainerFeesPanel.vue'
|
||||
|
||||
vi.mock('../api/containerFees', async (importOriginal) => {
|
||||
@@ -32,6 +32,7 @@ beforeEach(() => {
|
||||
vi.mocked(listContainerFees).mockReset().mockResolvedValue([]); vi.mocked(listApplicableFeeItems).mockReset().mockResolvedValue([])
|
||||
vi.mocked(listContainerFeePartnerOptions).mockReset().mockResolvedValue([])
|
||||
vi.mocked(listContainerCostDetails).mockReset().mockResolvedValue([]); vi.mocked(createContainerFee).mockReset()
|
||||
vi.mocked(adjustContainerFee).mockReset().mockResolvedValue({} as never)
|
||||
document.body.innerHTML = ''
|
||||
})
|
||||
|
||||
@@ -58,10 +59,22 @@ describe('ContainerFeesPanel contract', () => {
|
||||
expect(panel).toContain("row.reviewStatus === 'APPROVED'")
|
||||
expect(panel).toContain('adjustContainerFee')
|
||||
expect(panel).toContain('voidContainerFee')
|
||||
expect(panel).toContain('调整后版本递增并重新进入待审核状态')
|
||||
expect(panel).toContain('调整仅允许修改业务日期和金额')
|
||||
expect(panel).toContain('请先作废后新增')
|
||||
expect(panel).toContain("row.partnerName || '服务端未返回'")
|
||||
})
|
||||
|
||||
it('adjusts only business date and amount while preserving the approved snapshot', async () => {
|
||||
const wrapper = await mountPanel(['firstleg:inbound:fee-detail:view', 'firstleg:inbound:fee-detail:edit'])
|
||||
const item = { id: '2', loadingPlanId: '9', containerBookingId: '99', containerNo: null, feeItemId: '7', feeItemName: '拖车费', direction: 'PAYABLE' as const, partnerId: '41', partnerName: '承运商A', businessDate: '2026-08-12', amount: 100, currency: 'CNY' as const, settlementIncluded: true, reviewStatus: 'APPROVED' as const, sourceStatus: 'ACTIVE' as const, sourceVersion: 3, remark: '原备注', reviewerName: null, reviewedAt: null, reviewReason: null }
|
||||
const state = (wrapper.vm.$ as unknown as { setupState: { openAdjust(value: typeof item): void; save(): Promise<void>; form: { businessDate: string; amount: number } } }).setupState
|
||||
state.openAdjust(item); await flushPromises()
|
||||
state.form.businessDate = '2026-08-13'; state.form.amount = 120
|
||||
await state.save()
|
||||
expect(adjustContainerFee).toHaveBeenCalledWith('9', '11', '2', { expectedSourceVersion: 3, businessDate: '2026-08-13', amount: 120 })
|
||||
expect(wrapper.text()).toContain('已审核费用仍可调整日期与金额')
|
||||
})
|
||||
|
||||
it('shows only server allocations and supplier snapshots', () => {
|
||||
expect(panel).toContain('listContainerCostDetails')
|
||||
expect(panel).toContain('仅展示 Finance 已持久化结果')
|
||||
@@ -123,8 +136,7 @@ describe('ContainerFeesPanel contract', () => {
|
||||
{ id: '2', loadingPlanId: '9', containerBookingId: '99', containerNo: null, feeItemId: '2', feeItemName: '已审费用', direction: 'PAYABLE', partnerId: '41', partnerName: '承运商A', businessDate: '2026-08-12', amount: 20, currency: 'CNY', settlementIncluded: true, reviewStatus: 'APPROVED', sourceStatus: 'ACTIVE', sourceVersion: 1, remark: null, reviewerName: null, reviewedAt: null, reviewReason: null },
|
||||
])
|
||||
const wrapper = await mountPanel(['firstleg:inbound:fee-detail:view'])
|
||||
expect(wrapper.text()).not.toContain('新增费用')
|
||||
for (const label of ['通过', '驳回', '调整', '作废']) expect(wrapper.findAll('button').some((button) => button.text() === label)).toBe(false)
|
||||
for (const label of ['新增费用', '通过', '驳回', '调整', '作废']) expect(wrapper.findAll('button').some((button) => button.text() === label)).toBe(false)
|
||||
})
|
||||
|
||||
it('keeps receivable usable when counterparty permission is missing', async () => {
|
||||
|
||||
@@ -25,6 +25,7 @@ const partnersError = ref<ApiErrorDetails | null>(null)
|
||||
const feeItemsLoading = ref(false)
|
||||
const partnersLoading = ref(false)
|
||||
const editorOpen = ref(false)
|
||||
const editorError = ref<ApiErrorDetails | null>(null)
|
||||
const rejectOpen = ref(false)
|
||||
const saving = ref(false)
|
||||
const target = ref<ContainerFeeResource | null>(null)
|
||||
@@ -113,21 +114,38 @@ async function load() {
|
||||
finally { if (sequence === loadSequence) loading.value = false }
|
||||
}
|
||||
|
||||
function openCreate() { if (!canEdit.value || !canViewFeeItems.value || !accountId.value) return; resetForm(); editorOpen.value = true; void Promise.all([loadFeeItems(), loadPartners()]) }
|
||||
function openAdjust(item: ContainerFeeResource) { if (!canEdit.value || !canViewFeeItems.value || !accountId.value) return; resetForm(item); editorOpen.value = true; void Promise.all([loadFeeItems(), loadPartners()]) }
|
||||
function openCreate() { if (!canEdit.value || !canViewFeeItems.value || !accountId.value) return; resetForm(); editorError.value = null; editorOpen.value = true; void Promise.all([loadFeeItems(), loadPartners()]) }
|
||||
function openAdjust(item: ContainerFeeResource) { if (!canEdit.value || !accountId.value) return; resetForm(item); editorError.value = null; editorOpen.value = true }
|
||||
async function save() {
|
||||
if (!canEdit.value || !canViewFeeItems.value || !accountId.value) return
|
||||
if (form.direction === 'PAYABLE' && (!canViewPartners.value || !form.partnerId)) { ElMessage.warning(canViewPartners.value ? '应付费用必须选择往来单位' : '当前账号缺少往来单位查看权限,无法保存应付费用'); return }
|
||||
if (!canEdit.value || !accountId.value || (!target.value && !canViewFeeItems.value)) return
|
||||
if (!target.value && form.direction === 'PAYABLE' && (!canViewPartners.value || !form.partnerId)) { ElMessage.warning(canViewPartners.value ? '应付费用必须选择往来单位' : '当前账号缺少往来单位查看权限,无法保存应付费用'); return }
|
||||
if (!await formRef.value?.validate().catch(() => false)) return
|
||||
editorError.value = null
|
||||
if (target.value) {
|
||||
saving.value = true
|
||||
try {
|
||||
await adjustContainerFee(props.planId, accountId.value, target.value.id, {
|
||||
expectedSourceVersion: target.value.sourceVersion,
|
||||
businessDate: form.businessDate,
|
||||
amount: form.amount,
|
||||
})
|
||||
editorOpen.value = false; ElMessage.success('费用调整已提交审核'); await load()
|
||||
} catch (cause) {
|
||||
const details = getApiErrorDetails(cause, '费用调整失败')
|
||||
editorError.value = details.code === 'A0409'
|
||||
? { ...details, message: `${details.message};当前输入已保留,请刷新费用明细后重试。` }
|
||||
: details
|
||||
} finally { saving.value = false }
|
||||
return
|
||||
}
|
||||
const partner = partners.value.find((item) => item.id === String(form.partnerId))
|
||||
const payload: ContainerFeeSaveRequest = { ...form, partnerId: form.direction === 'PAYABLE' ? form.partnerId : null, partnerName: form.direction === 'PAYABLE' ? partner?.partnerName || null : null }
|
||||
if (form.direction === 'PAYABLE' && !payload.partnerName) { ElMessage.warning('请选择有效的往来单位'); return }
|
||||
saving.value = true
|
||||
try {
|
||||
if (target.value) await adjustContainerFee(props.planId, accountId.value, target.value.id, target.value.sourceVersion, payload)
|
||||
else await createContainerFee(props.planId, accountId.value, payload)
|
||||
editorOpen.value = false; ElMessage.success(target.value ? '费用调整已提交审核' : '费用已新增'); await load()
|
||||
} catch (cause) { ElMessage.error(getApiErrorDetails(cause, '费用保存失败').message) }
|
||||
await createContainerFee(props.planId, accountId.value, payload)
|
||||
editorOpen.value = false; ElMessage.success('费用已新增'); await load()
|
||||
} catch (cause) { editorError.value = getApiErrorDetails(cause, '费用保存失败') }
|
||||
finally { saving.value = false }
|
||||
}
|
||||
async function approve(item: ContainerFeeResource) { if (!canReview.value || !accountId.value) return; await approveContainerFee(props.planId, accountId.value, item.id).then(() => ElMessage.success('费用已审核通过')).then(load).catch((cause) => ElMessage.error(getApiErrorDetails(cause, '审核失败').message)) }
|
||||
@@ -162,7 +180,7 @@ onMounted(() => void load())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="fees-panel surface-panel" aria-label="柜次费用维护">
|
||||
<section class="fees-panel" aria-label="柜次费用维护">
|
||||
<header class="fees-header">
|
||||
<div><h2>柜次费用</h2><p>费用与财务分摊均来自服务端真实记录</p></div>
|
||||
<div class="fees-header__actions">
|
||||
@@ -174,7 +192,7 @@ onMounted(() => void load())
|
||||
<template v-else>
|
||||
<el-alert v-if="customerOptions.length === 0" type="warning" :closable="false" show-icon title="装柜明细缺少客户归属,无法安全维护费用" />
|
||||
<el-alert v-else-if="!accountId" type="info" :closable="false" show-icon title="该柜次包含多个客户,请先选择费用归属客户" />
|
||||
<el-alert v-else-if="!canViewFeeItems" type="warning" :closable="false" show-icon title="当前账号缺少费用项目查看权限,无法新增或调整费用" />
|
||||
<el-alert v-else-if="!canViewFeeItems" type="warning" :closable="false" show-icon title="当前账号缺少费用项目查看权限,无法新增费用;已审核费用仍可调整日期与金额" />
|
||||
<LoadErrorState v-else-if="error" compact title="柜次费用加载失败" :message="error.message" @retry="load"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState>
|
||||
<DataTable v-else :data="fees" :columns="columns" :loading="loading" preference-key="container-fees" row-key="id" empty-text="当前客户暂无柜次费用">
|
||||
<template #toolbar-start><div class="fees-table-title"><strong>费用明细</strong><span>{{ fees.length }} 条</span></div></template>
|
||||
@@ -187,20 +205,52 @@ onMounted(() => void load())
|
||||
<div v-if="accountId" class="allocation-section"><header><div><h3>财务体积分摊</h3><p>{{ selectedCustomer?.label }} · 仅展示 Finance 已持久化结果</p></div></header><el-empty v-if="!loading && costs.length === 0" description="尚无与该柜次关联的财务分摊结果" :image-size="64" /><article v-for="cost in costs" :key="cost.id" class="cost-block"><div class="cost-summary"><strong>{{ cost.feeItemName || '柜次成本' }}</strong><span>往来单位:{{ cost.supplierName || '服务端未返回' }}</span><span>金额:¥{{ money(cost.amount) }}</span></div><DataTable :data="cost.allocations" :columns="allocationColumns" :preference-key="`container-cost-${cost.id}`" row-key="id" empty-text="服务端未返回分摊明细"><template #volume="{ row }">{{ row.volumeCbm.toFixed(3) }}</template><template #rate="{ row }">{{ (row.allocationRate * 100).toFixed(2) }}%</template><template #allocated="{ row }">{{ money(row.allocatedAmount) }}</template></DataTable></article></div>
|
||||
</template>
|
||||
|
||||
<AppDialog v-model="editorOpen" :title="target ? '调整已审核费用' : '新增柜次费用'" confirm-text="保存" :confirm-loading="saving" @confirm="save"><el-alert v-if="target" type="info" :closable="false" show-icon title="调整后版本递增并重新进入待审核状态" /><el-alert v-if="!canViewPartners" type="warning" :closable="false" show-icon title="当前账号缺少往来单位查看权限,仅可维护应收费用" /><el-alert v-else-if="partnersError" type="warning" :closable="false" show-icon :title="partnersError.message" /><el-alert v-if="feeItemsError" type="warning" :closable="false" show-icon :title="feeItemsError.message" /><el-form ref="formRef" :model="form" :rules="rules" label-position="top" class="fee-form"><el-form-item label="方向" prop="direction"><el-segmented v-model="form.direction" :options="[{ label: '应收', value: 'RECEIVABLE' }, { label: '应付', value: 'PAYABLE', disabled: !canViewPartners || !!partnersError }]" /></el-form-item><el-form-item label="费用项目" prop="feeItemId"><el-select v-model="form.feeItemId" filterable :loading="feeItemsLoading" :disabled="!canViewFeeItems" placeholder="请选择启用且适用的费用项目"><el-option v-for="item in applicableItems" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item><el-form-item v-if="form.direction === 'PAYABLE'" label="往来单位" required><el-select v-model="form.partnerId" filterable :loading="partnersLoading" :disabled="!canViewPartners || !!partnersError" placeholder="请选择启用的往来单位"><el-option v-for="partner in partners" :key="partner.id" :label="partner.partnerName" :value="partner.id" /></el-select></el-form-item><el-form-item label="业务日期" prop="businessDate"><el-date-picker v-model="form.businessDate" type="date" value-format="YYYY-MM-DD" /></el-form-item><el-form-item label="金额(CNY)" prop="amount"><el-input-number v-model="form.amount" :min="0" :precision="2" controls-position="right" /></el-form-item><el-form-item label="参与结算"><el-switch v-model="form.settlementIncluded" /></el-form-item><el-form-item label="备注"><el-input v-model="form.remark" type="textarea" :rows="3" maxlength="500" show-word-limit /></el-form-item></el-form></AppDialog>
|
||||
<AppDialog v-model="editorOpen" :title="target ? '调整已审核费用' : '新增柜次费用'" confirm-text="保存" :confirm-loading="saving" @confirm="save">
|
||||
<div v-if="target" class="adjust-snapshot">
|
||||
<p>调整仅允许修改业务日期和金额;如需变更方向、费用项目、往来单位或其他信息,请先作废后新增。</p>
|
||||
<el-descriptions :column="3" border size="small">
|
||||
<el-descriptions-item label="方向">{{ target.direction === 'RECEIVABLE' ? '应收' : '应付' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="费用项目">{{ target.feeItemName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="往来单位">{{ target.partnerName || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="币种">{{ target.currency }}</el-descriptions-item>
|
||||
<el-descriptions-item label="参与结算">{{ target.settlementIncluded ? '是' : '否' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="备注">{{ target.remark || '—' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
<template v-else>
|
||||
<el-alert v-if="!canViewPartners" type="warning" :closable="false" show-icon title="当前账号缺少往来单位查看权限,仅可维护应收费用" />
|
||||
<el-alert v-else-if="partnersError" type="warning" :closable="false" show-icon :title="partnersError.message" />
|
||||
<el-alert v-if="feeItemsError" type="warning" :closable="false" show-icon :title="feeItemsError.message" />
|
||||
</template>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-position="top" class="fee-form">
|
||||
<template v-if="!target">
|
||||
<el-form-item label="方向" prop="direction"><el-segmented v-model="form.direction" :options="[{ label: '应收', value: 'RECEIVABLE' }, { label: '应付', value: 'PAYABLE', disabled: !canViewPartners || !!partnersError }]" /></el-form-item>
|
||||
<el-form-item label="费用项目" prop="feeItemId"><el-select v-model="form.feeItemId" filterable :loading="feeItemsLoading" :disabled="!canViewFeeItems" placeholder="请选择启用且适用的费用项目"><el-option v-for="item in applicableItems" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item>
|
||||
<el-form-item v-if="form.direction === 'PAYABLE'" label="往来单位" required><el-select v-model="form.partnerId" filterable :loading="partnersLoading" :disabled="!canViewPartners || !!partnersError" placeholder="请选择启用的往来单位"><el-option v-for="partner in partners" :key="partner.id" :label="partner.partnerName" :value="partner.id" /></el-select></el-form-item>
|
||||
</template>
|
||||
<el-form-item label="业务日期" prop="businessDate"><el-date-picker v-model="form.businessDate" type="date" value-format="YYYY-MM-DD" /></el-form-item>
|
||||
<el-form-item label="金额(CNY)" prop="amount"><el-input-number v-model="form.amount" :min="0" :precision="2" controls-position="right" /></el-form-item>
|
||||
<template v-if="!target">
|
||||
<el-form-item label="参与结算"><el-switch v-model="form.settlementIncluded" /></el-form-item>
|
||||
<el-form-item label="备注"><el-input v-model="form.remark" type="textarea" :rows="3" maxlength="500" show-word-limit /></el-form-item>
|
||||
</template>
|
||||
</el-form>
|
||||
<LoadErrorState v-if="editorError" compact title="费用保存失败" :message="editorError.message" :show-retry="false"><template #details><ApiErrorMeta :details="editorError" /></template></LoadErrorState>
|
||||
</AppDialog>
|
||||
<AppDialog v-model="rejectOpen" title="驳回柜次费用" confirm-text="确认驳回" :confirm-loading="saving" @confirm="reject"><el-input v-model="rejection" type="textarea" :rows="4" maxlength="500" show-word-limit placeholder="请输入驳回原因" /></AppDialog>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.fees-panel { min-width: 0; overflow: hidden; border: 1px solid var(--cc-border); background: #fff; }
|
||||
.fees-header, .allocation-section > header { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 12px 14px; }
|
||||
.fees-panel { min-width: 0; overflow: hidden; padding-top: 16px; border-top: 1px solid var(--cc-border); }
|
||||
.fees-header, .allocation-section > header { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 0 0 12px; }
|
||||
.fees-header h2, .allocation-section h3 { margin: 0; font-size: 16px; }.fees-header p, .allocation-section p { margin: 4px 0 0; color: var(--cc-muted); font-size: 12px; }
|
||||
.fees-header__actions, .customer-context { display: flex; align-items: center; gap: 10px; }
|
||||
.customer-context > span { color: var(--cc-muted); font-size: 13px; white-space: nowrap; }.customer-context :deep(.el-select) { width: 260px; }
|
||||
.fees-table-title { display: flex; align-items: baseline; gap: 8px; }.fees-table-title strong { font-size: 14px; }.fees-table-title span { color: var(--cc-muted); font-size: 12px; }
|
||||
.fees-panel :deep(.data-table__toolbar) { min-height: 48px; padding: 6px 12px; }
|
||||
.fees-panel :deep(.el-table th.el-table__cell) { padding: 7px 0; }.fees-panel :deep(.el-table td.el-table__cell) { padding: 6px 0; }
|
||||
.row-actions { display: flex; flex-wrap: wrap; }.allocation-section { border-top: 1px solid var(--cc-border); }.cost-block { border-top: 1px solid var(--cc-border); }.cost-summary { display: flex; flex-wrap: wrap; gap: 10px 18px; padding: 10px 14px; background: #f8fafc; font-size: 13px; }.cost-summary span { color: var(--cc-muted); }.fee-form { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 16px; margin-top: 14px; }.fee-form :deep(.el-form-item:last-child) { grid-column: 1 / -1; }
|
||||
.row-actions { display: flex; flex-wrap: wrap; }.allocation-section { padding-top: 16px; border-top: 1px solid var(--cc-border); }.cost-block { border-top: 1px solid var(--cc-border); }.cost-summary { display: flex; flex-wrap: wrap; gap: 10px 18px; padding: 10px 14px; background: #f8fafc; font-size: 13px; }.cost-summary span { color: var(--cc-muted); }.fee-form { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 16px; margin-top: 14px; }.fee-form :deep(.el-form-item:last-child) { grid-column: 1 / -1; }
|
||||
.adjust-snapshot p { margin: 0 0 12px; color: var(--cc-muted); font-size: 13px; line-height: 1.5; }
|
||||
@media (max-width: 640px) { .fees-header { align-items: stretch; flex-direction: column; }.fees-header .el-button { width: 100%; }.fee-form { grid-template-columns: 1fr; }.fee-form :deep(.el-form-item:last-child) { grid-column: auto; }.customer-context { align-items: stretch; flex-direction: column; } }
|
||||
</style>
|
||||
|
||||
@@ -24,7 +24,7 @@ const props = defineProps<{
|
||||
|
||||
const emit = defineEmits<{ back: []; retry: []; edit: []; editWarehouseRemark: [item: LoadingItem]; confirmPreload: []; attachCustoms: []; completeCustoms: [] }>()
|
||||
|
||||
const actionColumn: DataTableColumn = { key: 'actions', label: '操作', fixed: 'left', width: 106, slotName: 'actions', hideable: false, movable: false }
|
||||
const actionColumn: DataTableColumn = { key: 'actions', label: '操作', fixed: 'left', width: 132, slotName: 'actions', hideable: false, movable: false }
|
||||
const baseItemColumns: DataTableColumn[] = [
|
||||
{ key: 'sequence', prop: 'loadSequence', label: '装柜顺序', width: 92 },
|
||||
{ key: 'inboundCode', prop: 'inboundCode', label: '入仓码', minWidth: 170, className: 'identifier-column', showOverflowTooltip: true },
|
||||
@@ -113,7 +113,7 @@ function formatNumber(value?: number | null, digits = 3) {
|
||||
|
||||
<template>
|
||||
<section v-loading="loading" class="loading-detail-content" :aria-label="title" :aria-busy="loading">
|
||||
<section class="detail-overview surface-panel" aria-label="清单概览">
|
||||
<section class="detail-overview" aria-label="清单概览">
|
||||
<header class="detail-page-header">
|
||||
<el-button link type="primary" class="detail-page-header__back" @click="emit('back')"><ArrowLeft :size="16" />返回列表</el-button>
|
||||
<div class="detail-page-header__identity">
|
||||
@@ -122,8 +122,8 @@ function formatNumber(value?: number | null, digits = 3) {
|
||||
</div>
|
||||
<div v-if="detail && summary" class="detail-page-header__actions">
|
||||
<template v-if="canEditItemRemark">
|
||||
<el-button type="primary" @click="emit('edit')"><Pencil :size="16" />{{ preloadSummary?.containerNo ? '修改集装箱号 / 航次' : '录入集装箱号' }}</el-button>
|
||||
<el-button type="success" :loading="confirmSaving" :disabled="!preloadSummary?.readiness.canConfirm" @click="emit('confirmPreload')"><Check :size="16" />确认预装</el-button>
|
||||
<el-button @click="emit('edit')"><Pencil :size="16" />{{ preloadSummary?.containerNo ? '修改集装箱号 / 航次' : '录入集装箱号' }}</el-button>
|
||||
<el-button type="primary" :loading="confirmSaving" :disabled="!preloadSummary?.readiness.canConfirm" @click="emit('confirmPreload')"><Check :size="16" />确认预装</el-button>
|
||||
</template>
|
||||
<template v-else-if="mode === 'confirmed'">
|
||||
<el-button v-if="canManageCustoms && !detail.customsDocument?.completedAt" type="primary" @click="emit('attachCustoms')"><FileUp :size="16" />{{ detail.customsDocument ? '更换报关资料' : '上传报关资料' }}</el-button>
|
||||
@@ -154,10 +154,12 @@ function formatNumber(value?: number | null, digits = 3) {
|
||||
|
||||
<div v-else-if="detail && summary" class="detail-layout">
|
||||
<DetailPageLayout :ariaLabel="title">
|
||||
<el-alert v-if="preloadSummary?.readiness.confirmationStatus === 'CONFIRMED'" type="success" :closable="false" show-icon title="整份预装计划已确认" :description="preloadSummary.readiness.pdaTaskStatus ? `PDA 任务状态:${pdaTaskStatusLabels[preloadSummary.readiness.pdaTaskStatus] || '未知状态'}` : 'PDA 前置条件已满足,等待服务端生成任务。'" />
|
||||
<el-alert v-else-if="preloadSummary && !preloadSummary.readiness.canConfirm" type="warning" :closable="false" show-icon title="当前暂不能确认预装" :description="blockingReasonText || '请刷新详情获取最新确认条件。'" />
|
||||
<div v-if="preloadSummary && preloadSummary.readiness.confirmationStatus !== 'CONFIRMED' && !preloadSummary.readiness.canConfirm" class="readiness-note" role="status">
|
||||
<strong>暂不能确认预装:</strong>
|
||||
<span>{{ blockingReasonText || '请刷新详情获取最新确认条件' }}</span>
|
||||
</div>
|
||||
|
||||
<section class="detail-section surface-panel">
|
||||
<section class="detail-section">
|
||||
<DataTable
|
||||
:data="detail.items"
|
||||
:columns="itemColumns"
|
||||
@@ -185,7 +187,7 @@ function formatNumber(value?: number | null, digits = 3) {
|
||||
|
||||
<ContainerFeesPanel :plan-id="summary.planId" :container-booking-id="summary.containerBookingId" :customers="feeCustomers" />
|
||||
|
||||
<section class="history-section surface-panel" aria-label="操作历史">
|
||||
<section class="history-section" aria-label="操作历史">
|
||||
<header><h2>操作历史</h2><span>{{ detail.histories.length }} 条</span></header>
|
||||
<el-empty v-if="detail.histories.length === 0" description="暂无操作历史" :image-size="72" />
|
||||
<el-timeline v-else>
|
||||
@@ -204,25 +206,29 @@ function formatNumber(value?: number | null, digits = 3) {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.loading-detail-content, .detail-layout { display: grid; min-width: 0; gap: 16px; }
|
||||
.detail-overview { overflow: hidden; }
|
||||
.detail-page-header { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 16px; padding: 14px 16px; }
|
||||
.loading-detail-content { display: grid; min-width: 0; gap: 16px; padding: 16px; border: 1px solid var(--cc-border); border-radius: 8px; background: #fff; }
|
||||
.detail-layout { display: grid; min-width: 0; }
|
||||
.detail-overview { min-width: 0; }
|
||||
.detail-page-header { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 16px; padding: 8px 0 12px; }
|
||||
.detail-page-header__back { margin-right: 2px; }
|
||||
.detail-page-header__identity { min-width: 0; }
|
||||
.detail-page-header__identity h1 { margin: 0; color: var(--cc-text); font-size: 18px; line-height: 1.35; }
|
||||
.detail-page-header__identity p { margin: 3px 0 0; color: var(--cc-muted); font-size: 12px; }
|
||||
.detail-page-header__actions { display: flex; align-items: center; justify-content: flex-end; gap: 8px; }
|
||||
.detail-page-header__actions :deep(.el-button + .el-button) { margin-left: 0; }
|
||||
.detail-summary { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); overflow: hidden; border-top: 1px solid var(--cc-border); }
|
||||
.detail-summary > div { display: flex; min-width: 0; min-height: 68px; flex-direction: column; justify-content: center; gap: 5px; padding: 11px 14px; border-right: 1px solid var(--cc-border); border-bottom: 1px solid var(--cc-border); background: #fff; }
|
||||
.detail-summary > div:nth-child(5n) { border-right: 0; }
|
||||
.detail-summary span { color: var(--cc-muted); font-size: 12px; }
|
||||
.detail-summary strong { overflow: hidden; color: var(--cc-text); font-size: 14px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.detail-summary { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 6px 28px; padding: 10px 0 2px; border-top: 1px solid var(--cc-border); }
|
||||
.detail-summary > div { display: flex; min-width: 0; min-height: 26px; align-items: center; gap: 6px; }
|
||||
.detail-summary > div > span:first-child { flex: 0 0 auto; color: var(--cc-muted); font-size: 12px; white-space: nowrap; }
|
||||
.detail-summary > div > span:first-child::after { content: ":"; }
|
||||
.detail-summary strong { min-width: 0; color: var(--cc-text); font-size: 13px; overflow-wrap: anywhere; }
|
||||
.detail-summary :deep(.status-badge) { width: fit-content; }
|
||||
.detail-summary small { color: var(--cc-muted); font-size: 11px; }
|
||||
.detail-summary__wide { grid-column: span 2; }
|
||||
.detail-layout :deep(.detail-page-layout__content) { display: grid; min-width: 0; gap: 16px; }
|
||||
.detail-section, .history-section { min-width: 0; overflow: hidden; border: 1px solid var(--cc-border); background: #fff; }
|
||||
.detail-summary__wide { grid-column: auto; }
|
||||
.detail-layout :deep(.detail-page-layout__content) { display: grid; min-width: 0; gap: 20px; }
|
||||
.readiness-note { display: flex; min-width: 0; align-items: center; padding: 2px 4px; color: var(--cc-text); font-size: 12px; }
|
||||
.readiness-note strong { flex: 0 0 auto; color: var(--el-color-warning-dark-2); }
|
||||
.readiness-note span { min-width: 0; color: var(--cc-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.detail-section, .history-section { min-width: 0; overflow: hidden; padding: 16px 0 0; border-top: 1px solid var(--cc-border); }
|
||||
.detail-section__heading h2 { margin: 0; font-size: 15px; }
|
||||
.detail-section__heading h2 span { margin-left: 6px; color: var(--cc-muted); font-size: 12px; font-weight: 400; }
|
||||
.detail-section__heading p { margin: 2px 0 0; color: var(--cc-muted); font-size: 11px; line-height: 1.35; }
|
||||
@@ -233,7 +239,6 @@ function formatNumber(value?: number | null, digits = 3) {
|
||||
.detail-section :deep(.identifier-column .cell) { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.detail-section small { display: block; margin-top: 3px; color: var(--cc-muted); }
|
||||
.remark-cell { min-width: 0; line-height: 1.45; overflow-wrap: anywhere; }
|
||||
.history-section { padding: 14px 16px; }
|
||||
.history-section header { display: flex; align-items: baseline; gap: 10px; margin-bottom: 18px; }
|
||||
.history-section h2 { margin: 0; font-size: 16px; }
|
||||
.history-section header span { color: var(--cc-muted); font-size: 12px; }
|
||||
|
||||
@@ -120,7 +120,8 @@ describe('LoadingListsView', () => {
|
||||
expect(detailSource).toContain("PRELOAD_NOT_PUSHED: '尚未推送预装清单'")
|
||||
expect(detailSource).toContain("OCEAN_BOOKING_NOT_RELEASED: '关联订舱尚未放舱'")
|
||||
expect(detailSource).toContain("PRELOAD_ALREADY_CONFIRMED: '整份预装计划已经确认'")
|
||||
expect(detailSource).toContain("confirmationStatus === 'CONFIRMED'")
|
||||
expect(detailSource).toContain("confirmationStatus !== 'CONFIRMED' && !preloadSummary.readiness.canConfirm")
|
||||
expect(detailSource).toContain('confirmationStatusLabels[preloadSummary.readiness.confirmationStatus]')
|
||||
expect(detailSource).toContain("pdaTaskStatusLabels")
|
||||
expect(preloadDetailSource).toContain('@confirm-preload="openConfirmPreload"')
|
||||
})
|
||||
@@ -186,12 +187,18 @@ describe('LoadingListsView', () => {
|
||||
expect(detailSource).not.toContain("emit('editInternalRemark'")
|
||||
})
|
||||
|
||||
it('uses a compact desktop detail header, five-column summary and dense non-wrapping item table', () => {
|
||||
expect(detailSource).toContain('class="detail-overview surface-panel"')
|
||||
it('uses one white detail surface, a readable four-column summary and dense item table', () => {
|
||||
expect(detailSource).toContain('class="detail-overview"')
|
||||
expect(detailSource).not.toContain('class="detail-overview surface-panel"')
|
||||
expect(detailSource).toContain('class="detail-page-header"')
|
||||
expect(detailSource).toContain("@click=\"emit('back')\"")
|
||||
expect(detailSource).toContain('class="detail-page-header__actions"')
|
||||
expect(detailSource).toContain('grid-template-columns: repeat(5, minmax(0, 1fr))')
|
||||
expect(detailSource).toContain('grid-template-columns: repeat(4, minmax(0, 1fr))')
|
||||
expect(detailSource).toContain('min-height: 26px')
|
||||
expect(detailSource).toContain('> div > span:first-child::after { content: ":"; }')
|
||||
expect(detailSource).not.toContain('> div > span::after { content: ":"; }')
|
||||
expect(detailSource).toContain('background: #fff')
|
||||
expect(detailSource).toContain('overflow-wrap: anywhere')
|
||||
expect(detailSource).toContain("className: 'identifier-column'")
|
||||
expect(detailSource).toContain('showOverflowTooltip: true')
|
||||
expect(detailSource).toContain('white-space: nowrap')
|
||||
@@ -202,7 +209,12 @@ describe('LoadingListsView', () => {
|
||||
it('merges the page header and summary while spacing independent business sections', () => {
|
||||
expect(detailSource).toContain('<section v-if="detail && summary" class="detail-summary"')
|
||||
expect(detailSource).not.toContain('class="detail-summary surface-panel"')
|
||||
expect(detailSource).toContain('.detail-layout :deep(.detail-page-layout__content) { display: grid; min-width: 0; gap: 16px; }')
|
||||
expect(detailSource).toContain('.detail-layout :deep(.detail-page-layout__content) { display: grid; min-width: 0; gap: 20px; }')
|
||||
expect(detailSource).toContain('<section class="detail-section">')
|
||||
expect(detailSource).not.toContain('class="detail-section surface-panel"')
|
||||
expect(detailSource).not.toContain('class="history-section surface-panel"')
|
||||
expect(detailSource).toContain('padding: 16px 0 0')
|
||||
expect(detailSource).toContain("fixed: 'left', width: 132")
|
||||
})
|
||||
|
||||
it('does not expose unknown history action codes', () => {
|
||||
|
||||
Reference in New Issue
Block a user