优化优化

This commit is contained in:
2026-08-11 18:06:35 +08:00
parent 8a8c5a3faa
commit 03e1cd0c80
40 changed files with 2092 additions and 354 deletions
@@ -2,4 +2,22 @@ import { describe, expect, it } from 'vitest'
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
const source = readFileSync(resolve(process.cwd(), 'src/modules/customs/pages/CustomsCasesView.vue'), 'utf8')
describe('CustomsCasesView', () => { it('uses shared list and dialog components', () => { expect(source).toContain('QueryToolbar'); expect(source).toContain('DataTable'); expect(source).toContain('AppDialog'); expect(source).toContain("fixed: 'left'") }); it('supports create, detail, history and agent submission', () => { expect(source).toContain('createCustomsCase'); expect(source).toContain('getCustomsCase'); expect(source).toContain('submitCustomsCase'); expect(source).toContain('detail.history') }) })
describe('CustomsCasesView', () => {
it('uses shared list and dialog components', () => { expect(source).toContain('QueryToolbar'); expect(source).toContain('DataTable'); expect(source).toContain('AppDialog'); expect(source).toContain("fixed: 'left'") })
it('supports create, detail, history and agent submission', () => { expect(source).toContain('createCustomsCase'); expect(source).toContain('getCustomsCase'); expect(source).toContain('submitCustomsCase'); expect(source).toContain('detail.history') })
it('selects business resources instead of asking operators for customer and order ids', () => {
expect(source).toContain('listPlatformTenants')
expect(source).toContain('listPlatformUnifiedOrders')
expect(source).toContain('listContainers')
expect(source).toContain('selectOrder')
expect(source).toContain('重新加载选项')
expect(source).not.toContain('label="客户 ID"')
expect(source).not.toContain('label="订单 ID"')
expect(source).not.toContain('label="入库单 ID"')
})
it('only exposes active customers in create selectors', () => {
expect(source).toContain("customers.value.filter((customer) => customer.status === 'ACTIVE')")
expect(source).toContain('v-for="customer in activeCustomers"')
expect(source).not.toContain(':disabled="customer.status !== \'ACTIVE\'"')
})
})
+27 -4
View File
@@ -12,6 +12,9 @@ import ListPageLayout from '@/shared/components/ListPageLayout.vue'
import StatusBadge from '@/shared/components/StatusBadge.vue'
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
import { listPlatformUnifiedOrders, type UnifiedOrderResource } from '@/modules/unified-orders'
import { listContainers, type ContainerBookingResource } from '@/modules/container-loading'
import { createCustomsCase, getCustomsCase, listCustomsCases, submitCustomsCase } from '../api/platformCustoms'
import type { CustomsCaseCreateRequest, CustomsCaseDetail, CustomsCaseResource } from '../types'
@@ -20,8 +23,28 @@ const filters = reactive<QueryModel>({ orderId: '', inboundId: '', status: '' })
const columns = [{ key: 'actions', label: '操作', fixed: 'left', width: 175, slotName: 'actions', hideable: false, movable: false }, { prop: 'customsNo', label: '报关编号', minWidth: 145 }, { prop: 'orderId', label: '订单 ID', minWidth: 155 }, { prop: 'inboundId', label: '入库单 ID', minWidth: 155 }, { prop: 'checklistCode', label: '资料清单', minWidth: 125 }, { prop: 'checklistVersionNo', label: '版本', width: 90 }, { prop: 'status', label: '状态', width: 110, slotName: 'status' }, { prop: 'updatedAt', label: '更新时间', minWidth: 160 }]
async function load() { loading.value = true; error.value = null; try { const result = await listCustomsCases({ page: page.value, size: pageSize.value, orderId: String(filters.orderId || '') || undefined, inboundId: String(filters.inboundId || '') || undefined, status: String(filters.status || '') || undefined }); rows.value = result.records; total.value = result.total } catch (cause) { error.value = getApiErrorDetails(cause) } finally { loading.value = false } }
function search() { if (page.value === 1) void load(); else page.value = 1 }
const createOpen = ref(false); const createSaving = ref(false); const createError = ref<ApiErrorDetails | null>(null); const formRef = ref<FormInstance>(); const createForm = reactive<CustomsCaseCreateRequest>({ customerId: '', orderId: '', inboundId: '', containerBookingId: null, checklistVersionId: '', remark: '', expectedVersion: 0 }); const rules: FormRules = { customerId: [{ required: true, message: '请输入客户 ID', trigger: 'blur' }], orderId: [{ required: true, message: '请输入订单 ID', trigger: 'blur' }], inboundId: [{ required: true, message: '请输入入库单 ID', trigger: 'blur' }], checklistVersionId: [{ required: true, message: '请输入资料清单版本 ID', trigger: 'blur' }] }
function openCreate() { Object.assign(createForm, { customerId: '', orderId: '', inboundId: '', containerBookingId: null, checklistVersionId: '', remark: '', expectedVersion: 0 }); createError.value = null; createOpen.value = true }
const createOpen = ref(false); const createSaving = ref(false); const createError = ref<ApiErrorDetails | null>(null); const formRef = ref<FormInstance>(); const createForm = reactive<CustomsCaseCreateRequest>({ customerId: '', orderId: '', inboundId: '', containerBookingId: null, checklistVersionId: '', remark: '', expectedVersion: 0 })
const customers = ref<PlatformTenantResource[]>([]); const orders = ref<UnifiedOrderResource[]>([]); const containers = ref<ContainerBookingResource[]>([]); const optionsLoading = ref(false); const ordersLoading = ref(false); const optionsError = ref('')
const activeCustomers = computed(() => customers.value.filter((customer) => customer.status === 'ACTIVE'))
const rules: FormRules = { customerId: [{ required: true, message: '请选择客户', trigger: 'change' }], orderId: [{ required: true, message: '请选择订单', trigger: 'change' }], inboundId: [{ required: true, message: '所选订单尚未生成入库单', trigger: 'change' }], checklistVersionId: [{ required: true, message: '请输入资料清单版本 ID', trigger: 'blur' }] }
async function loadCreateOptions() {
optionsLoading.value = true; optionsError.value = ''
const [customerResult, containerResult] = await Promise.allSettled([listPlatformTenants({ page: 1, size: 200 }), listContainers({ page: 1, size: 200 })])
if (customerResult.status === 'fulfilled') customers.value = customerResult.value.records; else optionsError.value = '客户列表加载失败'
if (containerResult.status === 'fulfilled') containers.value = containerResult.value.records; else optionsError.value += `${optionsError.value ? ';' : ''}柜次列表加载失败`
optionsLoading.value = false
}
async function reloadCreateOptions() { await loadCreateOptions(); if (createForm.customerId) await selectCustomer(createForm.customerId) }
async function selectCustomer(customerId: string) {
createForm.customerId = customerId; createForm.orderId = ''; createForm.inboundId = ''; orders.value = []
if (!customerId) return
ordersLoading.value = true
try { const result = await listPlatformUnifiedOrders({ customerId, page: 1, size: 200 }); orders.value = result.records.filter((item) => item.inboundId != null) }
catch { optionsError.value = '该客户的订单列表加载失败,请重新加载选项' }
finally { ordersLoading.value = false }
}
function selectOrder(orderId: string) { createForm.orderId = orderId; createForm.inboundId = String(orders.value.find((item) => String(item.orderId) === orderId)?.inboundId || '') }
function openCreate() { Object.assign(createForm, { customerId: '', orderId: '', inboundId: '', containerBookingId: null, checklistVersionId: '', remark: '', expectedVersion: 0 }); createError.value = null; orders.value = []; createOpen.value = true; void loadCreateOptions() }
async function create() { if (!await formRef.value?.validate().catch(() => false)) return; createSaving.value = true; createError.value = null; try { await createCustomsCase({ ...createForm }); ElMessage.success('报关协同已创建'); createOpen.value = false; await load() } catch (cause) { createError.value = getApiErrorDetails(cause) } finally { createSaving.value = false } }
const detailOpen = ref(false); const detail = ref<CustomsCaseDetail | null>(null); const detailLoading = ref(false); const actionError = ref<ApiErrorDetails | null>(null); const submitForm = reactive({ customsAgentName: '', externalReference: '', note: '' }); const canSubmit = computed(() => detail.value && access.can('platform:firstleg:customs:submit') && !['SUBMITTED', 'RELEASED'].includes(detail.value.customsCase.status))
async function openDetail(id: string) { detailOpen.value = true; detailLoading.value = true; actionError.value = null; try { detail.value = await getCustomsCase(id) } catch (cause) { actionError.value = getApiErrorDetails(cause) } finally { detailLoading.value = false } }
@@ -29,6 +52,6 @@ async function submit() { if (!detail.value || !submitForm.customsAgentName.trim
watch([page, pageSize], () => void load()); onMounted(load)
</script>
<template><AdminPage as="div"><ListPageLayout aria-label="报关协同列表"><template #toolbar><QueryToolbar v-model="filters" class="surface-panel" ariaLabel="报关协同筛选" :fields="fields" @search="search" /></template><template v-if="error" #state><LoadErrorState title="报关协同加载失败" :message="error.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState></template><section v-if="!error" class="surface-panel table-panel"><DataTable v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :loading="loading" :total="total" row-key="id" preference-key="customs-cases" empty-text="暂无报关协同记录"><template #toolbar-actions><el-button v-if="access.can('platform:firstleg:customs:manage')" type="primary" @click="openCreate"><Plus :size="16" />新增报关</el-button></template><template #actions="{ row }"><el-button v-if="access.can('platform:firstleg:customs:view')" link type="primary" @click="openDetail(row.id)"><Eye :size="15" />查看</el-button></template><template #status="{ row }"><StatusBadge :status="row.status" /></template></DataTable></section></ListPageLayout>
<AppDialog v-model="createOpen" title="新增报关协同" width="720px" :confirm-loading="createSaving" @confirm="create"><el-alert v-if="createError" type="error" :closable="false"><ApiErrorMeta :details="createError" /></el-alert><el-form ref="formRef" :model="createForm" :rules="rules" label-position="top"><el-row :gutter="16"><el-col :xs="24" :sm="12"><el-form-item label="客户 ID" prop="customerId"><el-input v-model="createForm.customerId" /></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="订单 ID" prop="orderId"><el-input v-model="createForm.orderId" /></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="入库单 ID" prop="inboundId"><el-input v-model="createForm.inboundId" /></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="柜次 ID"><el-input v-model="createForm.containerBookingId" /></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="资料清单版本 ID" prop="checklistVersionId"><el-input v-model="createForm.checklistVersionId" /></el-form-item></el-col><el-col :xs="24"><el-form-item label="备注"><el-input v-model="createForm.remark" type="textarea" /></el-form-item></el-col></el-row></el-form></AppDialog>
<AppDialog v-model="detailOpen" title="报关协同详情" width="900px" :show-confirm="false"><LoadErrorState v-if="actionError && !detail" title="详情加载失败" :message="actionError.message" :loading="detailLoading" :show-retry="false"><template #details><ApiErrorMeta :details="actionError" /></template></LoadErrorState><template v-if="detail"><el-alert v-if="actionError" type="error" :closable="false"><ApiErrorMeta :details="actionError" /></el-alert><el-descriptions :column="2" border><el-descriptions-item label="报关编号">{{ detail.customsCase.customsNo }}</el-descriptions-item><el-descriptions-item label="状态"><StatusBadge :status="detail.customsCase.status" /></el-descriptions-item><el-descriptions-item label="资料清单">{{ detail.customsCase.checklistCode }} / {{ detail.customsCase.checklistVersionNo }}</el-descriptions-item><el-descriptions-item label="资料/回执">{{ detail.documents.length }} / {{ detail.receipts.length }}</el-descriptions-item></el-descriptions><el-form v-if="canSubmit" label-position="top" class="submit-form"><el-row :gutter="12"><el-col :xs="24" :sm="12"><el-form-item label="报关代理"><el-input v-model="submitForm.customsAgentName" /></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="外部参考号"><el-input v-model="submitForm.externalReference" /></el-form-item></el-col><el-col :xs="24"><el-form-item label="提交备注"><el-input v-model="submitForm.note" /></el-form-item></el-col></el-row><el-button type="primary" @click="submit"><Send :size="16" />登记线下提交</el-button></el-form><h3>状态历史</h3><el-timeline><el-timeline-item v-for="entry in detail.history" :key="entry.id" :timestamp="entry.createdAt">{{ entry.action }}:{{ entry.fromStatus || '—' }} → {{ entry.toStatus || '—' }}<small>{{ entry.note || '' }}</small></el-timeline-item></el-timeline></template></AppDialog></AdminPage></template>
<AppDialog v-model="createOpen" title="新增报关协同" width="720px" :confirm-loading="createSaving" @confirm="create"><el-alert v-if="createError" type="error" :closable="false"><ApiErrorMeta :details="createError" /></el-alert><el-alert v-if="optionsError" type="warning" :closable="false" show-icon :title="optionsError"><el-button link type="warning" :loading="optionsLoading || ordersLoading" @click="reloadCreateOptions">重新加载选项</el-button></el-alert><el-form ref="formRef" :model="createForm" :rules="rules" label-position="top"><el-row :gutter="16"><el-col :xs="24" :sm="12"><el-form-item label="客户" prop="customerId"><el-select :model-value="createForm.customerId" filterable placeholder="请选择客户" :loading="optionsLoading" style="width:100%" @update:model-value="selectCustomer(String($event || ''))"><el-option v-for="customer in activeCustomers" :key="String(customer.id)" :label="customer.tenantName" :value="String(customer.id)" /></el-select></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="订单 / 入库单" prop="orderId"><el-select :model-value="createForm.orderId" filterable placeholder="请先选择客户,再选择已入库订单" :loading="ordersLoading" :disabled="!createForm.customerId" style="width:100%" @update:model-value="selectOrder(String($event || ''))"><el-option v-for="order in orders" :key="String(order.orderId)" :label="`${order.orderNo || order.orderId} / ${order.inboundCode || order.inboundId}`" :value="String(order.orderId)" /></el-select></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="关联柜次(可选)"><el-select v-model="createForm.containerBookingId" filterable clearable placeholder="请选择柜次" :loading="optionsLoading" style="width:100%"><el-option v-for="container in containers" :key="container.id" :label="`${container.cabinetNo || container.bookingNo}${container.containerNo ? ` / ${container.containerNo}` : ''}`" :value="container.id" /></el-select></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="资料清单版本 ID" prop="checklistVersionId"><el-input v-model="createForm.checklistVersionId" placeholder="请输入已发布的资料清单版本 ID" /><small>当前服务端尚未提供资料清单版本查询接口。</small></el-form-item></el-col><el-col :xs="24"><el-form-item label="备注"><el-input v-model="createForm.remark" type="textarea" placeholder="请输入报关协同备注(可选)" /></el-form-item></el-col></el-row></el-form></AppDialog>
<AppDialog v-model="detailOpen" title="报关协同详情" width="900px" :show-confirm="false"><LoadErrorState v-if="actionError && !detail" title="详情加载失败" :message="actionError.message" :loading="detailLoading" :show-retry="false"><template #details><ApiErrorMeta :details="actionError" /></template></LoadErrorState><template v-if="detail"><el-alert v-if="actionError" type="error" :closable="false"><ApiErrorMeta :details="actionError" /></el-alert><el-descriptions :column="2" border><el-descriptions-item label="报关编号">{{ detail.customsCase.customsNo }}</el-descriptions-item><el-descriptions-item label="状态"><StatusBadge :status="detail.customsCase.status" /></el-descriptions-item><el-descriptions-item label="资料清单">{{ detail.customsCase.checklistCode }} / {{ detail.customsCase.checklistVersionNo }}</el-descriptions-item><el-descriptions-item label="资料/回执">{{ detail.documents.length }} / {{ detail.receipts.length }}</el-descriptions-item></el-descriptions><el-form v-if="canSubmit" label-position="top" class="submit-form"><el-row :gutter="12"><el-col :xs="24" :sm="12"><el-form-item label="报关代理"><el-input v-model="submitForm.customsAgentName" placeholder="请输入线下报关代理名称" /></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="外部参考号"><el-input v-model="submitForm.externalReference" placeholder="请输入代理回传的参考号(可选)" /></el-form-item></el-col><el-col :xs="24"><el-form-item label="提交备注"><el-input v-model="submitForm.note" placeholder="请输入本次提交说明(可选)" /></el-form-item></el-col></el-row><el-button type="primary" @click="submit"><Send :size="16" />登记线下提交</el-button></el-form><h3>状态历史</h3><el-timeline><el-timeline-item v-for="entry in detail.history" :key="entry.id" :timestamp="entry.createdAt">{{ entry.action }}:{{ entry.fromStatus || '—' }} → {{ entry.toStatus || '—' }}<small>{{ entry.note || '' }}</small></el-timeline-item></el-timeline></template></AppDialog></AdminPage></template>
<style scoped>.submit-form{margin-top:18px}.submit-form+.el-button{margin-bottom:12px}h3{margin:20px 0 12px}small{display:block;color:var(--cc-muted)}</style>