优化
This commit is contained in:
@@ -58,9 +58,9 @@
|
|||||||
| 财务账户、账单、收款与成本 | `modules/finance/` | `finance/`;费用来源与柜次成本快照联查 `firstleg/`,凭证联查 `file/` |
|
| 财务账户、账单、收款与成本 | `modules/finance/` | `finance/`;费用来源与柜次成本快照联查 `firstleg/`,凭证联查 `file/` |
|
||||||
| 货柜与智能排柜 | `modules/container-loading/` | `firstleg/loading/`、`firstleg/split/` |
|
| 货柜与智能排柜 | `modules/container-loading/` | `firstleg/loading/`、`firstleg/split/` |
|
||||||
| 预装详情、装柜清单与柜次费用 | `modules/loading-lists/pages/PreloadPlanDetailView.vue`、`modules/loading-lists/components/LoadingListDetailDrawer.vue`、`modules/loading-lists/components/ContainerFeesPanel.vue`、`modules/loading-lists/api/containerFees.ts` | `firstleg/loading/`、`firstleg` 柜次费用、`finance/` 成本分摊、`file/`、`warehouse/` |
|
| 预装详情、装柜清单与柜次费用 | `modules/loading-lists/pages/PreloadPlanDetailView.vue`、`modules/loading-lists/components/LoadingListDetailDrawer.vue`、`modules/loading-lists/components/ContainerFeesPanel.vue`、`modules/loading-lists/api/containerFees.ts` | `firstleg/loading/`、`firstleg` 柜次费用、`finance/` 成本分摊、`file/`、`warehouse/` |
|
||||||
| 海运订舱与船期 | `modules/ocean-bookings/`、`modules/port-schedules/` | `firstleg/oceanbooking/`、`firstleg/portschedule/`、`integration-service/` |
|
| 海运订舱与船期 | `modules/ocean-bookings/`、`modules/port-schedules/` | `firstleg/oceanbooking/`、`firstleg/portschedule/`、`integration-service/`;订舱代理联查 `modules/suppliers/` 与 `warehouse/src/main/resources/openapi/supplier-v1.yaml`,服务端契约见 `firstleg/src/main/resources/openapi/ocean-bookings-v1.yaml` |
|
||||||
| 拖车预约 | `modules/trucking/` | `firstleg/outbound/dispatch/`、`firstleg/` |
|
| 拖车预约 | `modules/trucking/` | `firstleg/outbound/dispatch/`、`firstleg/`;承运商联查 `modules/suppliers/` 与 `warehouse/src/main/resources/openapi/supplier-v1.yaml`,服务端契约见 `firstleg/src/main/resources/openapi/truck-appointments-v1.yaml` |
|
||||||
| 报关协同 | `modules/customs/` | `firstleg/customs/`、`file/` |
|
| 报关协同 | `modules/customs/` | `firstleg/customs/`、`file/`;报关代理联查 `modules/suppliers/` 与 `warehouse/src/main/resources/openapi/supplier-v1.yaml`,服务端契约见 `firstleg/src/main/resources/openapi/customs-v1.yaml` |
|
||||||
| 国内出仓确认 | `modules/outbound-dispatch/` | `firstleg/outbound/dispatch/` |
|
| 国内出仓确认 | `modules/outbound-dispatch/` | `firstleg/outbound/dispatch/` |
|
||||||
| 主数据与下载 | `modules/master-data/`、`modules/downloads/` | `warehouse/`、`file/` |
|
| 主数据与下载 | `modules/master-data/`、`modules/downloads/` | `warehouse/`、`file/` |
|
||||||
|
|
||||||
@@ -87,6 +87,7 @@
|
|||||||
- 供应商菜单、列表和详情固定只使用 `platform:supplier:list`,`platform:supplier:view` 必须停用/移除。修改 `modules/suppliers/`、`core/navigation/adminNavigation.ts` 或根路由时,不得恢复或兼容 `platform:supplier:view`。
|
- 供应商菜单、列表和详情固定只使用 `platform:supplier:list`,`platform:supplier:view` 必须停用/移除。修改 `modules/suppliers/`、`core/navigation/adminNavigation.ts` 或根路由时,不得恢复或兼容 `platform:supplier:view`。
|
||||||
- `core/navigation/adminNavigation.ts`、`app/router/index.ts` 和模块 `routes.ts` 属于共享装配文件;并行业务任务不得用局部模块结论覆盖菜单与权限总控已经确认的权限语义,发现重叠改动时保留并回传总控处理。
|
- `core/navigation/adminNavigation.ts`、`app/router/index.ts` 和模块 `routes.ts` 属于共享装配文件;并行业务任务不得用局部模块结论覆盖菜单与权限总控已经确认的权限语义,发现重叠改动时保留并回传总控处理。
|
||||||
- 认证统一经过 `src/core/auth/auth.ts` 和 `src/shared/api/http.ts`;不要在页面中复制令牌读写或手工拼装 `Authorization`。
|
- 认证统一经过 `src/core/auth/auth.ts` 和 `src/shared/api/http.ts`;不要在页面中复制令牌读写或手工拼装 `Authorization`。
|
||||||
|
- 订舱代理、拖车承运商和报关代理必须提交服务端契约要求的供应商 ID,并从公共供应商列表选择 `ACTIVE` 且类别/范围匹配的记录;名称只作为展示或一致性提示,不得把名称输入当作 ID 的替代。修改这些请求参数时,必须同步检查 API 封装、类型、表单、详情展示、Mock/Fixture、契约测试和 Firstleg/Warehouse OpenAPI,不能只改页面或只改后端。
|
||||||
- 当前登录和部分数据仍可能是模拟实现。接入 `baiyue-firstleg` Gateway 真实接口时删除对应模拟路径,不允许让生产逻辑静默回退到假数据。
|
- 当前登录和部分数据仍可能是模拟实现。接入 `baiyue-firstleg` Gateway 真实接口时删除对应模拟路径,不允许让生产逻辑静默回退到假数据。
|
||||||
- 删除、停用、改权限等高风险操作必须有明确确认、处理中、成功和失败反馈;失败时保留用户上下文,避免误以为已生效。
|
- 删除、停用、改权限等高风险操作必须有明确确认、处理中、成功和失败反馈;失败时保留用户上下文,避免误以为已生效。
|
||||||
- 日志、提示和示例数据不得包含真实账号、令牌、客户隐私或生产数据。
|
- 日志、提示和示例数据不得包含真实账号、令牌、客户隐私或生产数据。
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
|
import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
|
||||||
import type { CustomsCaseCreateRequest, CustomsCaseDetail, CustomsCasePage, CustomsCaseQuery, CustomsCaseResource } from '../types'
|
import type { CustomsCaseCreateRequest, CustomsCaseDetail, CustomsCasePage, CustomsCaseQuery, CustomsCaseResource, CustomsSubmitBatchRequest } from '../types'
|
||||||
const basePath = '/platform/v1/customs'
|
const basePath = '/platform/v1/customs'
|
||||||
const keys = ['id', 'tenantId', 'orderId', 'inboundId', 'containerBookingId', 'checklistVersionId'] as const
|
const keys = ['id', 'tenantId', 'orderId', 'inboundId', 'containerBookingId', 'checklistVersionId'] as const
|
||||||
function item(raw: CustomsCaseResource) { const value = { ...raw } as CustomsCaseResource & Record<string, unknown>; for (const key of keys) if (value[key] != null) value[key] = String(value[key]); value.version = Number(value.version ?? 0); return value }
|
function item(raw: CustomsCaseResource) { const value = { ...raw } as CustomsCaseResource & Record<string, unknown>; for (const key of keys) if (value[key] != null) value[key] = String(value[key]); value.version = Number(value.version ?? 0); return value }
|
||||||
@@ -8,4 +8,4 @@ function idem() { return `customs-${Date.now()}-${Math.random().toString(36).sli
|
|||||||
export async function listCustomsCases(query: CustomsCaseQuery) { const response = await http.get<ApiEnvelope<CustomsCasePage>, ApiEnvelope<CustomsCasePage>>(`${basePath}/cases`, { params: query }); const data = unwrapApiResponse(response); return { ...data, records: (data.records || []).map(item), page: Number(data.page), size: Number(data.size), total: Number(data.total), pages: Number(data.pages) } }
|
export async function listCustomsCases(query: CustomsCaseQuery) { const response = await http.get<ApiEnvelope<CustomsCasePage>, ApiEnvelope<CustomsCasePage>>(`${basePath}/cases`, { params: query }); const data = unwrapApiResponse(response); return { ...data, records: (data.records || []).map(item), page: Number(data.page), size: Number(data.size), total: Number(data.total), pages: Number(data.pages) } }
|
||||||
export async function getCustomsCase(id: string) { const response = await http.get<ApiEnvelope<CustomsCaseDetail>, ApiEnvelope<CustomsCaseDetail>>(`${basePath}/cases/${encodeURIComponent(id)}`); return detail(unwrapApiResponse(response)) }
|
export async function getCustomsCase(id: string) { const response = await http.get<ApiEnvelope<CustomsCaseDetail>, ApiEnvelope<CustomsCaseDetail>>(`${basePath}/cases/${encodeURIComponent(id)}`); return detail(unwrapApiResponse(response)) }
|
||||||
export async function createCustomsCase(request: CustomsCaseCreateRequest) { const response = await http.post<ApiEnvelope<CustomsCaseDetail>, ApiEnvelope<CustomsCaseDetail>>(`${basePath}/cases`, request, { headers: { 'Idempotency-Key': idem() } }); return detail(unwrapApiResponse(response)) }
|
export async function createCustomsCase(request: CustomsCaseCreateRequest) { const response = await http.post<ApiEnvelope<CustomsCaseDetail>, ApiEnvelope<CustomsCaseDetail>>(`${basePath}/cases`, request, { headers: { 'Idempotency-Key': idem() } }); return detail(unwrapApiResponse(response)) }
|
||||||
export async function submitCustomsCase(id: string, request: { customsAgentName: string; externalReference?: string; note?: string; expectedVersion: number }) { const response = await http.post<ApiEnvelope<unknown>, ApiEnvelope<unknown>>(`${basePath}/cases/${encodeURIComponent(id)}/submission-batches`, request, { headers: { 'Idempotency-Key': idem() } }); return unwrapApiResponse(response) }
|
export async function submitCustomsCase(id: string, request: CustomsSubmitBatchRequest) { const response = await http.post<ApiEnvelope<unknown>, ApiEnvelope<unknown>>(`${basePath}/cases/${encodeURIComponent(id)}/submission-batches`, request, { headers: { 'Idempotency-Key': idem() } }); return unwrapApiResponse(response) }
|
||||||
|
|||||||
@@ -14,11 +14,13 @@ import { customsCaseStatusOptions, customsHistoryActionLabel, customsStatusLabel
|
|||||||
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||||
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
|
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
|
||||||
|
import { listSuppliers, type Supplier } from '@/modules/suppliers'
|
||||||
import { getCustomsCase, listCustomsCases, submitCustomsCase } from '../api/platformCustoms'
|
import { getCustomsCase, listCustomsCases, submitCustomsCase } from '../api/platformCustoms'
|
||||||
import type { CustomsCaseDetail, CustomsCaseResource } from '../types'
|
import type { CustomsCaseDetail, CustomsCaseResource } from '../types'
|
||||||
|
|
||||||
const access = useAccessPreviewStore(); const rows = ref<CustomsCaseResource[]>([]); const loading = ref(false); const error = ref<ApiErrorDetails | null>(null); const page = ref(1); const pageSize = ref(20); const total = ref(0)
|
const access = useAccessPreviewStore(); const rows = ref<CustomsCaseResource[]>([]); const loading = ref(false); const error = ref<ApiErrorDetails | null>(null); const page = ref(1); const pageSize = ref(20); const total = ref(0)
|
||||||
const customers = ref<PlatformTenantResource[]>([]); const customersLoading = ref(false)
|
const customers = ref<PlatformTenantResource[]>([]); const customersLoading = ref(false)
|
||||||
|
const customsAgents = ref<Supplier[]>([]); const customsAgentsLoading = ref(false); const customsAgentsError = ref('')
|
||||||
const filters = ref<QueryModel>({ customerId: '', status: '' }); const fields = computed<QueryField[]>(() => [
|
const filters = ref<QueryModel>({ customerId: '', status: '' }); const fields = computed<QueryField[]>(() => [
|
||||||
{ key: 'customerId', type: 'select', label: '客户', placeholder: '全部客户', width: 220, options: customers.value.filter((customer) => customer.status === 'ACTIVE').map((customer) => ({ label: customer.tenantName, value: String(customer.id) })), props: { filterable: true, loading: customersLoading.value } },
|
{ key: 'customerId', type: 'select', label: '客户', placeholder: '全部客户', width: 220, options: customers.value.filter((customer) => customer.status === 'ACTIVE').map((customer) => ({ label: customer.tenantName, value: String(customer.id) })), props: { filterable: true, loading: customersLoading.value } },
|
||||||
{ key: 'status', type: 'select', label: '状态', options: customsCaseStatusOptions.map((option) => ({ ...option })) },
|
{ key: 'status', type: 'select', label: '状态', options: customsCaseStatusOptions.map((option) => ({ ...option })) },
|
||||||
@@ -26,13 +28,15 @@ const filters = ref<QueryModel>({ customerId: '', status: '' }); const fields =
|
|||||||
const columns = [{ key: 'actions', label: '操作', fixed: 'left', width: 175, slotName: 'actions', hideable: false, movable: false }, { prop: 'customsNo', label: '报关编号', minWidth: 145 }, { key: 'orderLink', label: '订单关联', minWidth: 120, formatter: (row: CustomsCaseResource) => associationLabel(row.orderId) }, { key: 'inboundLink', label: '入库单关联', minWidth: 120, formatter: (row: CustomsCaseResource) => associationLabel(row.inboundId) }, { prop: 'checklistCode', label: '资料清单', minWidth: 125 }, { prop: 'checklistVersionNo', label: '版本', width: 90 }, { prop: 'status', label: '状态', width: 110, slotName: 'status' }, { prop: 'updatedAt', label: '更新时间', minWidth: 160 }]
|
const columns = [{ key: 'actions', label: '操作', fixed: 'left', width: 175, slotName: 'actions', hideable: false, movable: false }, { prop: 'customsNo', label: '报关编号', minWidth: 145 }, { key: 'orderLink', label: '订单关联', minWidth: 120, formatter: (row: CustomsCaseResource) => associationLabel(row.orderId) }, { key: 'inboundLink', label: '入库单关联', minWidth: 120, formatter: (row: CustomsCaseResource) => associationLabel(row.inboundId) }, { prop: 'checklistCode', label: '资料清单', minWidth: 125 }, { prop: 'checklistVersionNo', label: '版本', width: 90 }, { prop: 'status', label: '状态', width: 110, slotName: 'status' }, { prop: 'updatedAt', label: '更新时间', minWidth: 160 }]
|
||||||
function associationLabel(value: string | null | undefined) { return value ? '已关联' : '未关联' }
|
function associationLabel(value: string | null | undefined) { return value ? '已关联' : '未关联' }
|
||||||
async function loadCustomers() { customersLoading.value = true; try { const result = await listPlatformTenants({ page: 1, size: 200, status: 'ACTIVE' }); customers.value = result.records.filter((customer) => customer.status === 'ACTIVE') } catch { customers.value = [] } finally { customersLoading.value = false } }
|
async function loadCustomers() { customersLoading.value = true; try { const result = await listPlatformTenants({ page: 1, size: 200, status: 'ACTIVE' }); customers.value = result.records.filter((customer) => customer.status === 'ACTIVE') } catch { customers.value = [] } finally { customersLoading.value = false } }
|
||||||
|
async function loadCustomsAgents() { customsAgentsLoading.value = true; customsAgentsError.value = ''; try { const result = await listSuppliers({ page: 1, size: 200, category: 'CUSTOMS', status: 'ACTIVE' }); customsAgents.value = result.records.filter((supplier) => supplier.category === 'CUSTOMS' && supplier.status === 'ACTIVE') } catch (cause) { customsAgents.value = []; customsAgentsError.value = getApiErrorDetails(cause, '报关代理加载失败').message } finally { customsAgentsLoading.value = false } }
|
||||||
async function load() { loading.value = true; error.value = null; try { const result = await listCustomsCases({ page: page.value, size: pageSize.value, customerId: String(filters.value.customerId || '') || undefined, status: String(filters.value.status || '') || undefined }); rows.value = result.records; total.value = result.total } catch (cause) { error.value = getApiErrorDetails(cause) } finally { loading.value = false } }
|
async function load() { loading.value = true; error.value = null; try { const result = await listCustomsCases({ page: page.value, size: pageSize.value, customerId: String(filters.value.customerId || '') || undefined, status: String(filters.value.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 }
|
function search() { if (page.value === 1) void load(); else page.value = 1 }
|
||||||
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?.customsCase.status === 'READY_TO_SUBMIT' && access.can('platform:firstleg:customs:submit'))
|
const detailOpen = ref(false); const detail = ref<CustomsCaseDetail | null>(null); const detailLoading = ref(false); const actionError = ref<ApiErrorDetails | null>(null); const submitForm = reactive({ customsAgentId: '', customsAgentName: '', externalReference: '', note: '' }); const canSubmit = computed(() => detail.value?.customsCase.status === 'READY_TO_SUBMIT' && access.can('platform:firstleg:customs:submit'))
|
||||||
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 } }
|
function selectCustomsAgent(supplierId: string) { const supplier = customsAgents.value.find((item) => item.id === supplierId); submitForm.customsAgentId = supplier?.id || ''; submitForm.customsAgentName = supplier?.name || '' }
|
||||||
async function submit() { if (!detail.value || !submitForm.customsAgentName.trim()) return; actionError.value = null; try { await submitCustomsCase(detail.value.customsCase.id, { ...submitForm, expectedVersion: detail.value.customsCase.version }); ElMessage.success('已登记线下提交批次'); detail.value = await getCustomsCase(detail.value.customsCase.id) } catch (cause) { actionError.value = getApiErrorDetails(cause); if (actionError.value.code === 'A0409') detail.value = await getCustomsCase(detail.value.customsCase.id) } }
|
async function openDetail(id: string) { detailOpen.value = true; detailLoading.value = true; actionError.value = null; selectCustomsAgent(''); try { detail.value = await getCustomsCase(id) } catch (cause) { actionError.value = getApiErrorDetails(cause) } finally { detailLoading.value = false } }
|
||||||
watch([page, pageSize], () => void load()); onMounted(() => { void load(); void loadCustomers() })
|
async function submit() { if (!detail.value || !submitForm.customsAgentId || !customsAgents.value.some((item) => item.id === submitForm.customsAgentId)) { actionError.value = { message: '请选择有效的合作中报关供应商' }; return } actionError.value = null; try { await submitCustomsCase(detail.value.customsCase.id, { ...submitForm, expectedVersion: detail.value.customsCase.version }); ElMessage.success('已登记线下提交批次'); detail.value = await getCustomsCase(detail.value.customsCase.id) } catch (cause) { actionError.value = getApiErrorDetails(cause); if (actionError.value.code === 'A0409') detail.value = await getCustomsCase(detail.value.customsCase.id) } }
|
||||||
|
watch([page, pageSize], () => void load()); onMounted(() => { void load(); void loadCustomers(); void loadCustomsAgents() })
|
||||||
</script>
|
</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" stripe row-key="id" preference-key="customs-cases" empty-text="暂无报关协同记录"><template #actions="{ row }"><el-button v-if="access.can('platform:firstleg:customs:view')" size="small" type="primary" plain @click="openDetail(row.id)"><Eye :size="15" />查看</el-button></template><template #status="{ row }"><StatusBadge :status="customsStatusLabel(row.status)" /></template></DataTable></section></ListPageLayout>
|
<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" stripe row-key="id" preference-key="customs-cases" empty-text="暂无报关协同记录"><template #actions="{ row }"><el-button v-if="access.can('platform:firstleg:customs:view')" size="small" type="primary" plain @click="openDetail(row.id)"><Eye :size="15" />查看</el-button></template><template #status="{ row }"><StatusBadge :status="customsStatusLabel(row.status)" /></template></DataTable></section></ListPageLayout>
|
||||||
<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="customsStatusLabel(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">{{ customsHistoryActionLabel(entry.action) }}:{{ customsStatusLabel(entry.fromStatus) }} → {{ customsStatusLabel(entry.toStatus) }}<small>{{ entry.note || '' }}</small></el-timeline-item></el-timeline></template></AppDialog></AdminPage></template>
|
<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="customsStatusLabel(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-alert v-if="customsAgentsError" type="warning" :closable="false" :title="customsAgentsError"><el-button link type="warning" :loading="customsAgentsLoading" @click="loadCustomsAgents">重新加载</el-button></el-alert><el-row :gutter="12"><el-col :xs="24" :sm="12"><el-form-item label="报关代理" required><el-select :model-value="submitForm.customsAgentId" filterable clearable placeholder="请选择报关代理" :loading="customsAgentsLoading" no-data-text="暂无可用报关供应商" style="width:100%" @update:model-value="selectCustomsAgent(String($event || ''))"><el-option v-for="supplier in customsAgents" :key="supplier.id" :label="supplier.name" :value="supplier.id" /></el-select></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" :disabled="!submitForm.customsAgentId" @click="submit"><Send :size="16" />登记线下提交</el-button></el-form><h3>提交批次</h3><el-empty v-if="!detail.submissionBatches.length" description="暂无提交批次" :image-size="56" /><el-timeline v-else><el-timeline-item v-for="batch in detail.submissionBatches" :key="batch.id" :timestamp="batch.createdAt">报关代理:{{ batch.customsAgentName || '服务端未返回名称快照' }}</el-timeline-item></el-timeline><h3>状态历史</h3><el-timeline><el-timeline-item v-for="entry in detail.history" :key="entry.id" :timestamp="entry.createdAt">{{ customsHistoryActionLabel(entry.action) }}:{{ customsStatusLabel(entry.fromStatus) }} → {{ customsStatusLabel(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)}:global(.el-dialog:has(.submit-form,.el-descriptions)){width:min(900px,calc(100vw - 28px))!important;max-height:90vh;overflow:hidden}:global(.el-dialog:has(.submit-form,.el-descriptions) .el-dialog__body){max-height:calc(90vh - 120px);overflow-y:auto}</style>
|
<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)}:global(.el-dialog:has(.submit-form,.el-descriptions)){width:min(900px,calc(100vw - 28px))!important;max-height:90vh;overflow:hidden}:global(.el-dialog:has(.submit-form,.el-descriptions) .el-dialog__body){max-height:calc(90vh - 120px);overflow-y:auto}</style>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export interface CustomsCaseResource { id: string; customsNo: string; tenantId?:
|
|||||||
export interface CustomsDocumentResource { id: string; itemCode: string; fileName?: string | null; status?: string | null; version?: number }
|
export interface CustomsDocumentResource { id: string; itemCode: string; fileName?: string | null; status?: string | null; version?: number }
|
||||||
export interface CustomsHistoryResource { id: string; action: string; fromStatus?: string | null; toStatus?: string | null; note?: string | null; operatorName?: string | null; createdAt: string }
|
export interface CustomsHistoryResource { id: string; action: string; fromStatus?: string | null; toStatus?: string | null; note?: string | null; operatorName?: string | null; createdAt: string }
|
||||||
export interface CustomsCaseDetail { customsCase: CustomsCaseResource; documents: CustomsDocumentResource[]; supplements: unknown[]; submissionBatches: Array<{ id: string; customsAgentName?: string; status?: string; createdAt?: string }>; receipts: Array<{ id: string; receiptType?: string; status?: string; createdAt?: string }>; history: CustomsHistoryResource[] }
|
export interface CustomsCaseDetail { customsCase: CustomsCaseResource; documents: CustomsDocumentResource[]; supplements: unknown[]; submissionBatches: Array<{ id: string; customsAgentName?: string; status?: string; createdAt?: string }>; receipts: Array<{ id: string; receiptType?: string; status?: string; createdAt?: string }>; history: CustomsHistoryResource[] }
|
||||||
|
export interface CustomsSubmitBatchRequest { customsAgentId: string; customsAgentName: string; externalReference?: string; note?: string; expectedVersion: number }
|
||||||
export interface CustomsCasePage { records: CustomsCaseResource[]; page: number; size: number; total: number; pages: number }
|
export interface CustomsCasePage { records: CustomsCaseResource[]; page: number; size: number; total: number; pages: number }
|
||||||
export interface CustomsCaseQuery { page: number; size: number; customerId?: string; orderId?: string; inboundId?: string; containerBookingId?: string; status?: string }
|
export interface CustomsCaseQuery { page: number; size: number; customerId?: string; orderId?: string; inboundId?: string; containerBookingId?: string; status?: string }
|
||||||
export interface CustomsCaseCreateRequest { customerId: string; orderId: string; inboundId: string; containerBookingId?: string | null; checklistVersionId: string; remark?: string | null; expectedVersion: number }
|
export interface CustomsCaseCreateRequest { customerId: string; orderId: string; inboundId: string; containerBookingId?: string | null; checklistVersionId: string; remark?: string | null; expectedVersion: number }
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { ElMessage, ElMessageBox, type UploadFile } from 'element-plus'
|
|||||||
import { ArrowLeft, CreditCard, Download, Eye, HandCoins, Plus, RotateCcw, XCircle } from '@lucide/vue'
|
import { ArrowLeft, CreditCard, Download, Eye, HandCoins, Plus, RotateCcw, XCircle } from '@lucide/vue'
|
||||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||||
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
|
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
|
||||||
import { listPlatformPartners, type PlatformPartnerResource } from '@/modules/master-data'
|
import { categoryLabels, listSuppliers, type Supplier } from '@/modules/suppliers'
|
||||||
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||||
import AdminPage from '@/shared/components/AdminPage.vue'
|
import AdminPage from '@/shared/components/AdminPage.vue'
|
||||||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||||
@@ -54,7 +54,7 @@ const createOpen = ref(false)
|
|||||||
const createBusy = ref(false)
|
const createBusy = ref(false)
|
||||||
const statementOptions = ref<FinanceStatementReadModel[]>([])
|
const statementOptions = ref<FinanceStatementReadModel[]>([])
|
||||||
const feeOptions = ref<FinanceFeePoolItem[]>([])
|
const feeOptions = ref<FinanceFeePoolItem[]>([])
|
||||||
const supplierOptions = ref<PlatformPartnerResource[]>([])
|
const supplierOptions = ref<Supplier[]>([])
|
||||||
const createForm = ref({ reconciliationStatementId: '', supplierId: '', feeSourceIds: [] as string[], dueDate: '' })
|
const createForm = ref({ reconciliationStatementId: '', supplierId: '', feeSourceIds: [] as string[], dueDate: '' })
|
||||||
const paymentOpen = ref(false)
|
const paymentOpen = ref(false)
|
||||||
const paymentBusy = ref(false)
|
const paymentBusy = ref(false)
|
||||||
@@ -208,10 +208,10 @@ async function openCreate() {
|
|||||||
} else {
|
} else {
|
||||||
const [fees, suppliers] = await Promise.all([
|
const [fees, suppliers] = await Promise.all([
|
||||||
listFinanceFeePool(customerId, 'PAYABLE'),
|
listFinanceFeePool(customerId, 'PAYABLE'),
|
||||||
listPlatformPartners({ page: 1, size: 200, status: 'ACTIVE' }),
|
listSuppliers({ page: 1, size: 200, status: 'ACTIVE' }),
|
||||||
])
|
])
|
||||||
feeOptions.value = fees.filter((item) => item.feeSourceId && item.status === 'AVAILABLE')
|
feeOptions.value = fees.filter((item) => item.feeSourceId && item.status === 'AVAILABLE')
|
||||||
supplierOptions.value = suppliers.records
|
supplierOptions.value = suppliers.records.filter((item) => item.status === 'ACTIVE')
|
||||||
}
|
}
|
||||||
} catch (cause) { error.value = getApiErrorDetails(cause, '结算来源加载失败') }
|
} catch (cause) { error.value = getApiErrorDetails(cause, '结算来源加载失败') }
|
||||||
}
|
}
|
||||||
@@ -489,7 +489,7 @@ onMounted(() => {
|
|||||||
<AppDialog v-model="createOpen" :title="`生成${title}`" width="min(560px, 94vw)" :confirm-loading="createBusy" @confirm="submitCreate">
|
<AppDialog v-model="createOpen" :title="`生成${title}`" width="min(560px, 94vw)" :confirm-loading="createBusy" @confirm="submitCreate">
|
||||||
<el-form label-position="top">
|
<el-form label-position="top">
|
||||||
<el-form-item v-if="kind === 'receivable'" label="已出账对账单"><el-select v-model="createForm.reconciliationStatementId" filterable><el-option v-for="item in statementOptions" :key="item.id" :label="`${item.billNo} · ${item.outstandingAmount} CNY`" :value="item.id" /></el-select></el-form-item>
|
<el-form-item v-if="kind === 'receivable'" label="已出账对账单"><el-select v-model="createForm.reconciliationStatementId" filterable><el-option v-for="item in statementOptions" :key="item.id" :label="`${item.billNo} · ${item.outstandingAmount} CNY`" :value="item.id" /></el-select></el-form-item>
|
||||||
<template v-else><el-form-item label="供应商"><el-select v-model="createForm.supplierId" filterable placeholder="请选择供应商"><el-option v-for="item in supplierOptions" :key="String(item.id)" :label="`${item.partnerName} · ${item.partnerCode}`" :value="String(item.id)" /></el-select></el-form-item><el-form-item label="到期日"><el-date-picker v-model="createForm.dueDate" type="date" value-format="YYYY-MM-DD" /></el-form-item><el-form-item label="应付费用来源"><el-select v-model="createForm.feeSourceIds" multiple filterable><el-option v-for="item in feeOptions" :key="item.feeSourceId!" :label="`${item.feeItemName} · ${item.amount} CNY`" :value="item.feeSourceId!" /></el-select></el-form-item></template>
|
<template v-else><el-form-item label="供应商"><el-select v-model="createForm.supplierId" filterable placeholder="请选择供应商"><el-option v-for="item in supplierOptions" :key="String(item.id)" :label="`${item.name} · ${categoryLabels[item.category]}`" :value="String(item.id)" /></el-select></el-form-item><el-form-item label="到期日"><el-date-picker v-model="createForm.dueDate" type="date" value-format="YYYY-MM-DD" /></el-form-item><el-form-item label="应付费用来源"><el-select v-model="createForm.feeSourceIds" multiple filterable><el-option v-for="item in feeOptions" :key="item.feeSourceId!" :label="`${item.feeItemName} · ${item.amount} CNY`" :value="item.feeSourceId!" /></el-select></el-form-item></template>
|
||||||
</el-form>
|
</el-form>
|
||||||
</AppDialog>
|
</AppDialog>
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vites
|
|||||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||||
import { useAuthStore } from '@/core/auth/auth'
|
import { useAuthStore } from '@/core/auth/auth'
|
||||||
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
|
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
|
||||||
import { listPlatformPartners, type PlatformPartnerResource } from '@/modules/master-data'
|
import { listSuppliers, type Supplier } from '@/modules/suppliers'
|
||||||
import { ApiResponseError } from '@/shared/api/http'
|
import { ApiResponseError } from '@/shared/api/http'
|
||||||
import {
|
import {
|
||||||
actionReconciliationStatement,
|
actionReconciliationStatement,
|
||||||
@@ -25,7 +25,10 @@ import FinanceCustomerStatementsView from './FinanceCustomerStatementsView.vue'
|
|||||||
import FinanceFormalSettlementPage from './FinanceFormalSettlementPage.vue'
|
import FinanceFormalSettlementPage from './FinanceFormalSettlementPage.vue'
|
||||||
|
|
||||||
vi.mock('@/modules/iam', () => ({ listPlatformTenants: vi.fn() }))
|
vi.mock('@/modules/iam', () => ({ listPlatformTenants: vi.fn() }))
|
||||||
vi.mock('@/modules/master-data', () => ({ listPlatformPartners: vi.fn() }))
|
vi.mock('@/modules/suppliers', () => ({
|
||||||
|
listSuppliers: vi.fn(),
|
||||||
|
categoryLabels: { BOOKING: '订舱', CUSTOMS: '报关', TRUCKING: '拖车', CERTIFICATE: '证书' },
|
||||||
|
}))
|
||||||
vi.mock('../api/finance', () => ({
|
vi.mock('../api/finance', () => ({
|
||||||
actionReconciliationStatement: vi.fn(), createFinanceSettlementPayment: vi.fn(), createPayableSettlement: vi.fn(),
|
actionReconciliationStatement: vi.fn(), createFinanceSettlementPayment: vi.fn(), createPayableSettlement: vi.fn(),
|
||||||
createReceivableSettlement: vi.fn(), getFinanceSettlement: vi.fn(), getReconciliationStatement: vi.fn(),
|
createReceivableSettlement: vi.fn(), getFinanceSettlement: vi.fn(), getReconciliationStatement: vi.fn(),
|
||||||
@@ -70,7 +73,7 @@ describe('Finance G3/G4 views', () => {
|
|||||||
document.body.innerHTML = ''
|
document.body.innerHTML = ''
|
||||||
const customer = { id: '10001', tenantName: '客户A', status: 'ACTIVE' } as PlatformTenantResource
|
const customer = { id: '10001', tenantName: '客户A', status: 'ACTIVE' } as PlatformTenantResource
|
||||||
vi.mocked(listPlatformTenants).mockReset().mockResolvedValue({ records: [customer], page: 1, size: 200, total: 1, pages: 1 })
|
vi.mocked(listPlatformTenants).mockReset().mockResolvedValue({ records: [customer], page: 1, size: 200, total: 1, pages: 1 })
|
||||||
vi.mocked(listPlatformPartners).mockReset().mockResolvedValue({ records: [{ id: '301', tenantId: '0', dataScopeId: '0', partnerCode: 'PARTNER-301', partnerName: '往来单位A', partnerType: 'CUSTOMER', types: ['CUSTOMER'], contactName: '', contactPhone: '', contactEmail: null, countryCode: 'CN', province: null, city: '', district: null, addressLine: '', postalCode: null, status: 'ACTIVE', remark: null } as PlatformPartnerResource], page: 1, size: 200, total: 1, pages: 1 })
|
vi.mocked(listSuppliers).mockReset().mockResolvedValue({ records: [{ id: '301', tenantId: '0', name: '拖车供应商A', category: 'TRUCKING', scope: 'DOMESTIC', region: '宁波', legalRepresentative: '', contactName: '', phone: '', email: null, address: null, businessLicenseNo: null, licenseFileId: null, status: 'ACTIVE', settlementPeriod: 'MONTHLY_30', remark: null, createdBy: null, createdAt: '2026-08-01', updatedBy: null, updatedAt: '2026-08-01', deleted: false, version: 0 } as Supplier], page: 1, size: 200, total: 1, pages: 1 })
|
||||||
vi.mocked(listFinanceStatements).mockReset().mockResolvedValue([statement])
|
vi.mocked(listFinanceStatements).mockReset().mockResolvedValue([statement])
|
||||||
vi.mocked(getReconciliationStatement).mockReset().mockResolvedValue(statementDetail)
|
vi.mocked(getReconciliationStatement).mockReset().mockResolvedValue(statementDetail)
|
||||||
vi.mocked(actionReconciliationStatement).mockReset().mockResolvedValue(statementDetail)
|
vi.mocked(actionReconciliationStatement).mockReset().mockResolvedValue(statementDetail)
|
||||||
@@ -106,8 +109,8 @@ describe('Finance G3/G4 views', () => {
|
|||||||
const wrapper = await mountWithAccess(FinanceFormalSettlementPage, ['platform:finance:settlement-ap:list', 'platform:finance:settlement-ap:create', 'platform:finance:payment:create'], { kind: 'payable' })
|
const wrapper = await mountWithAccess(FinanceFormalSettlementPage, ['platform:finance:settlement-ap:list', 'platform:finance:settlement-ap:create', 'platform:finance:payment:create'], { kind: 'payable' })
|
||||||
const vm = wrapper.vm as unknown as { load(model: Record<string, string>): Promise<void>; openCreate(): Promise<void>; submitCreate(): Promise<void>; openPayment(row: FinancePayableSettlement): void; submitPayment(): Promise<void>; createForm: { supplierId: string; dueDate: string; feeSourceIds: string[] }; paymentFile: File | null; paymentForm: { amount: number; transactionDate: string; method: 'BANK_TRANSFER'; referenceNo: string; remark: string } }
|
const vm = wrapper.vm as unknown as { load(model: Record<string, string>): Promise<void>; openCreate(): Promise<void>; submitCreate(): Promise<void>; openPayment(row: FinancePayableSettlement): void; submitPayment(): Promise<void>; createForm: { supplierId: string; dueDate: string; feeSourceIds: string[] }; paymentFile: File | null; paymentForm: { amount: number; transactionDate: string; method: 'BANK_TRANSFER'; referenceNo: string; remark: string } }
|
||||||
await vm.load({ customerId: '10001' }); await vm.openCreate()
|
await vm.load({ customerId: '10001' }); await vm.openCreate()
|
||||||
expect(listPlatformPartners).toHaveBeenCalledWith({ page: 1, size: 200, status: 'ACTIVE' })
|
expect(listSuppliers).toHaveBeenCalledWith({ page: 1, size: 200, status: 'ACTIVE' })
|
||||||
expect(document.body.textContent).toContain('往来单位A · PARTNER-301')
|
expect(document.body.textContent).toContain('拖车供应商A · 拖车')
|
||||||
Object.assign(vm.createForm, { supplierId: '301', dueDate: '2026-09-30', feeSourceIds: ['81'] })
|
Object.assign(vm.createForm, { supplierId: '301', dueDate: '2026-09-30', feeSourceIds: ['81'] })
|
||||||
await vm.submitCreate()
|
await vm.submitCreate()
|
||||||
expect(createPayableSettlement).toHaveBeenCalledWith({ customerId: '10001', supplierId: '301', feeSourceIds: ['81'], dueDate: '2026-09-30', currency: 'CNY' })
|
expect(createPayableSettlement).toHaveBeenCalledWith({ customerId: '10001', supplierId: '301', feeSourceIds: ['81'], dueDate: '2026-09-30', currency: 'CNY' })
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export { listSuppliers } from './api/platformSuppliers'
|
||||||
|
export { categoryLabels } from './types'
|
||||||
|
export type { Supplier, SupplierPage, SupplierQuery, SupplierCategory } from './types'
|
||||||
@@ -7,19 +7,22 @@ const source = readFileSync(resolve(process.cwd(), 'src/modules/trucking/compone
|
|||||||
describe('TruckAppointmentForm interaction contract', () => {
|
describe('TruckAppointmentForm interaction contract', () => {
|
||||||
it('uses searchable business selectors for shared resources', () => {
|
it('uses searchable business selectors for shared resources', () => {
|
||||||
expect(source).toContain('listPlatformTenants')
|
expect(source).toContain('listPlatformTenants')
|
||||||
expect(source).toContain('listPlatformPartners')
|
expect(source).toContain('listSuppliers')
|
||||||
expect(source).toContain('listContainers')
|
expect(source).toContain('listContainers')
|
||||||
expect(source).toContain('listOceanBookings')
|
expect(source).toContain('listOceanBookings')
|
||||||
expect(source).toContain('listAllPlatformWarehouses')
|
expect(source).toContain('listAllPlatformWarehouses')
|
||||||
expect(source).not.toContain('label="承运商 ID"')
|
expect(source).not.toContain('label="承运商 ID"')
|
||||||
expect(source).not.toContain('label="客户关联 ID"')
|
expect(source).not.toContain('label="客户关联 ID"')
|
||||||
expect(source).toContain("listPlatformTenants({ page: 1, size: 200, status: 'ACTIVE' })")
|
expect(source).toContain("listPlatformTenants({ page: 1, size: 200, status: 'ACTIVE' })")
|
||||||
|
expect(source).toContain("listSuppliers({ page: 1, size: 200, category: 'TRUCKING', status: 'ACTIVE' })")
|
||||||
expect(source).toContain('function customerLabel(customer: PlatformTenantResource) { return customer.tenantName }')
|
expect(source).toContain('function customerLabel(customer: PlatformTenantResource) { return customer.tenantName }')
|
||||||
expect(source).not.toContain('customer.tenantCode')
|
expect(source).not.toContain('customer.tenantCode')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('synchronizes ids and names and recovers failed option loading', () => {
|
it('synchronizes ids and names and recovers failed option loading', () => {
|
||||||
expect(source).toContain('selectCarrier')
|
expect(source).toContain('selectCarrier')
|
||||||
|
expect(source).toContain("item.category === 'TRUCKING' && item.status === 'ACTIVE'")
|
||||||
|
expect(source).toContain('请选择有效的合作中拖车供应商')
|
||||||
expect(source).toContain('draft.carrierName')
|
expect(source).toContain('draft.carrierName')
|
||||||
expect(source).toContain('selectCustomer')
|
expect(source).toContain('selectCustomer')
|
||||||
expect(source).toContain('draft.customerName')
|
expect(source).toContain('draft.customerName')
|
||||||
@@ -29,7 +32,7 @@ describe('TruckAppointmentForm interaction contract', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('hides inactive options while preserving resources already bound to an appointment', () => {
|
it('hides inactive options while preserving resources already bound to an appointment', () => {
|
||||||
expect(source).toContain("item.status === 'ACTIVE' || String(item.id) === String(draft.customerId)")
|
expect(source).toContain("item.status === 'ACTIVE'")
|
||||||
expect(source).toContain("item.status === 'ACTIVE' || String(item.id) === String(draft.warehouseId)")
|
expect(source).toContain("item.status === 'ACTIVE' || String(item.id) === String(draft.warehouseId)")
|
||||||
expect(source).toContain('v-for="item in selectableCustomers"')
|
expect(source).toContain('v-for="item in selectableCustomers"')
|
||||||
expect(source).toContain('v-for="item in selectableWarehouses"')
|
expect(source).toContain('v-for="item in selectableWarehouses"')
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type { FormInstance, FormRules } from 'element-plus'
|
|||||||
import { Eye, Save } from '@lucide/vue'
|
import { Eye, Save } from '@lucide/vue'
|
||||||
import type { SaveTruckAppointmentRequest, TruckAppointmentResource } from '../types'
|
import type { SaveTruckAppointmentRequest, TruckAppointmentResource } from '../types'
|
||||||
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
|
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
|
||||||
import { listPlatformPartners, type PlatformPartnerResource } from '@/modules/master-data'
|
import { listSuppliers, type Supplier } from '@/modules/suppliers'
|
||||||
import { listContainers, type ContainerBookingResource } from '@/modules/container-loading'
|
import { listContainers, type ContainerBookingResource } from '@/modules/container-loading'
|
||||||
import { listOceanBookings, type OceanBookingResource } from '@/modules/ocean-bookings'
|
import { listOceanBookings, type OceanBookingResource } from '@/modules/ocean-bookings'
|
||||||
import { listAllPlatformWarehouses, type PlatformWarehouseSummaryResource } from '@/modules/warehouses'
|
import { listAllPlatformWarehouses, type PlatformWarehouseSummaryResource } from '@/modules/warehouses'
|
||||||
@@ -27,7 +27,7 @@ const phoneRule = (label: string) => ({ validator: (_rule: unknown, value: unkno
|
|||||||
else callback(new Error(`${label}格式不正确`))
|
else callback(new Error(`${label}格式不正确`))
|
||||||
}, trigger: 'blur' })
|
}, trigger: 'blur' })
|
||||||
const rules: FormRules = {
|
const rules: FormRules = {
|
||||||
carrierId: [{ required: true, message: '请选择承运商', trigger: 'change' }],
|
carrierId: [{ required: true, message: '请选择承运商', trigger: 'change' }, { validator: (_rule, value, callback) => callback(selectableCarriers.value.some((item) => String(item.id) === String(value)) ? undefined : new Error('请选择有效的合作中拖车供应商')), trigger: 'change' }],
|
||||||
plannedLoadingAt: [{ required: true, message: '请选择预计装箱时间', trigger: 'change' }],
|
plannedLoadingAt: [{ required: true, message: '请选择预计装箱时间', trigger: 'change' }],
|
||||||
fleetPhone: [phoneRule('车队电话')], driverPhone: [phoneRule('司机电话')], storePhone: [phoneRule('门店电话')],
|
fleetPhone: [phoneRule('车队电话')], driverPhone: [phoneRule('司机电话')], storePhone: [phoneRule('门店电话')],
|
||||||
packageCount: [nonNegative('件数', true)], palletCount: [nonNegative('托盘数量', true)], transitMinutes: [nonNegative('在途分钟', true)],
|
packageCount: [nonNegative('件数', true)], palletCount: [nonNegative('托盘数量', true)], transitMinutes: [nonNegative('在途分钟', true)],
|
||||||
@@ -36,7 +36,7 @@ const rules: FormRules = {
|
|||||||
}
|
}
|
||||||
const relationText = computed(() => [props.resource?.customerName, props.resource?.outboundOrderId, props.resource?.containerBookingId].filter(Boolean).join(' / ') || '由服务端关联资源返回')
|
const relationText = computed(() => [props.resource?.customerName, props.resource?.outboundOrderId, props.resource?.containerBookingId].filter(Boolean).join(' / ') || '由服务端关联资源返回')
|
||||||
const customers = ref<PlatformTenantResource[]>([])
|
const customers = ref<PlatformTenantResource[]>([])
|
||||||
const carriers = ref<PlatformPartnerResource[]>([])
|
const carriers = ref<Supplier[]>([])
|
||||||
const containers = ref<ContainerBookingResource[]>([])
|
const containers = ref<ContainerBookingResource[]>([])
|
||||||
const bookings = ref<OceanBookingResource[]>([])
|
const bookings = ref<OceanBookingResource[]>([])
|
||||||
const warehouses = ref<PlatformWarehouseSummaryResource[]>([])
|
const warehouses = ref<PlatformWarehouseSummaryResource[]>([])
|
||||||
@@ -49,24 +49,24 @@ const selectableWarehouses = computed(() => warehouses.value.filter((item) => (
|
|||||||
item.status === 'ACTIVE' || String(item.id) === String(draft.warehouseId)
|
item.status === 'ACTIVE' || String(item.id) === String(draft.warehouseId)
|
||||||
)))
|
)))
|
||||||
const selectableCarriers = computed(() => carriers.value.filter((item) => (
|
const selectableCarriers = computed(() => carriers.value.filter((item) => (
|
||||||
item.status === 'ACTIVE' || String(item.id) === String(draft.carrierId)
|
item.status === 'ACTIVE'
|
||||||
)))
|
)))
|
||||||
function customerLabel(customer: PlatformTenantResource) { return customer.tenantName }
|
function customerLabel(customer: PlatformTenantResource) { return customer.tenantName }
|
||||||
function selectCustomer(customerId: string) { const customer = customers.value.find((item) => String(item.id) === customerId); draft.customerId = customerId || null; draft.customerName = customer ? customerLabel(customer) : null }
|
function selectCustomer(customerId: string) { const customer = customers.value.find((item) => String(item.id) === customerId); draft.customerId = customerId || null; draft.customerName = customer ? customerLabel(customer) : null }
|
||||||
function selectCarrier(carrierId: string) { draft.carrierId = carrierId || null; draft.carrierName = carriers.value.find((item) => String(item.id) === carrierId)?.partnerName || null }
|
function selectCarrier(carrierId: string) { const carrier = selectableCarriers.value.find((item) => String(item.id) === carrierId); draft.carrierId = carrier?.id || null; draft.carrierName = carrier?.name || null }
|
||||||
function selectContainer(containerId: string) { const item = containers.value.find((entry) => entry.id === containerId); draft.containerBookingId = containerId || null; if (item) { draft.loadingPlanId = item.planId; draft.warehouseId = item.warehouseId } }
|
function selectContainer(containerId: string) { const item = containers.value.find((entry) => entry.id === containerId); draft.containerBookingId = containerId || null; if (item) { draft.loadingPlanId = item.planId; draft.warehouseId = item.warehouseId } }
|
||||||
function selectBooking(bookingId: string) { const item = bookings.value.find((entry) => entry.id === bookingId); draft.oceanBookingId = bookingId || null; if (item?.customerId) selectCustomer(item.customerId) }
|
function selectBooking(bookingId: string) { const item = bookings.value.find((entry) => entry.id === bookingId); draft.oceanBookingId = bookingId || null; if (item?.customerId) selectCustomer(item.customerId) }
|
||||||
async function loadOptions() {
|
async function loadOptions() {
|
||||||
optionsLoading.value = true; optionsError.value = ''
|
optionsLoading.value = true; optionsError.value = ''
|
||||||
const results = await Promise.allSettled([
|
const results = await Promise.allSettled([
|
||||||
listPlatformTenants({ page: 1, size: 200, status: 'ACTIVE' }),
|
listPlatformTenants({ page: 1, size: 200, status: 'ACTIVE' }),
|
||||||
listPlatformPartners({ page: 1, size: 200, type: 'CARRIER', status: 'ACTIVE' }),
|
listSuppliers({ page: 1, size: 200, category: 'TRUCKING', status: 'ACTIVE' }),
|
||||||
listContainers({ page: 1, size: 200 }),
|
listContainers({ page: 1, size: 200 }),
|
||||||
listOceanBookings({ page: 1, size: 200 }),
|
listOceanBookings({ page: 1, size: 200 }),
|
||||||
listAllPlatformWarehouses(),
|
listAllPlatformWarehouses(),
|
||||||
])
|
])
|
||||||
if (results[0].status === 'fulfilled') customers.value = results[0].value.records.filter((customer) => customer.status === 'ACTIVE')
|
if (results[0].status === 'fulfilled') customers.value = results[0].value.records.filter((customer) => customer.status === 'ACTIVE')
|
||||||
if (results[1].status === 'fulfilled') carriers.value = results[1].value.records
|
if (results[1].status === 'fulfilled') carriers.value = results[1].value.records.filter((item) => item.category === 'TRUCKING' && item.status === 'ACTIVE')
|
||||||
if (results[2].status === 'fulfilled') containers.value = results[2].value.records
|
if (results[2].status === 'fulfilled') containers.value = results[2].value.records
|
||||||
if (results[3].status === 'fulfilled') bookings.value = results[3].value.records
|
if (results[3].status === 'fulfilled') bookings.value = results[3].value.records
|
||||||
if (results[4].status === 'fulfilled') warehouses.value = results[4].value
|
if (results[4].status === 'fulfilled') warehouses.value = results[4].value
|
||||||
@@ -93,7 +93,7 @@ watch(() => props.readonly, (readonly) => { if (!readonly && !customers.value.le
|
|||||||
<el-col :xs="24" :sm="12"><el-form-item label="仓库"><el-select v-model="draft.warehouseId" filterable clearable placeholder="请选择仓库" :loading="optionsLoading" style="width:100%"><el-option v-for="item in selectableWarehouses" :key="String(item.id)" :label="`${item.warehouseName}(${item.warehouseCode})`" :value="String(item.id)" /></el-select></el-form-item></el-col>
|
<el-col :xs="24" :sm="12"><el-form-item label="仓库"><el-select v-model="draft.warehouseId" filterable clearable placeholder="请选择仓库" :loading="optionsLoading" style="width:100%"><el-option v-for="item in selectableWarehouses" :key="String(item.id)" :label="`${item.warehouseName}(${item.warehouseCode})`" :value="String(item.id)" /></el-select></el-form-item></el-col>
|
||||||
<el-col v-if="resource" :xs="24" :sm="12"><el-form-item label="派车单号"><el-input :model-value="resource.appointmentNo || ''" disabled /></el-form-item></el-col>
|
<el-col v-if="resource" :xs="24" :sm="12"><el-form-item label="派车单号"><el-input :model-value="resource.appointmentNo || ''" disabled /></el-form-item></el-col>
|
||||||
<el-col :xs="24"><el-divider content-position="left">运输与时间计划</el-divider></el-col>
|
<el-col :xs="24"><el-divider content-position="left">运输与时间计划</el-divider></el-col>
|
||||||
<el-col :xs="24" :sm="12"><el-form-item label="承运商" prop="carrierId"><el-select :model-value="draft.carrierId" filterable placeholder="请选择承运商" :loading="optionsLoading" style="width:100%" @update:model-value="selectCarrier(String($event || ''))"><el-option v-if="draft.carrierId && !selectableCarriers.some((item) => String(item.id) === String(draft.carrierId))" :label="draft.carrierName || '当前承运商'" :value="String(draft.carrierId)" /><el-option v-for="item in selectableCarriers" :key="String(item.id)" :label="item.partnerName" :value="String(item.id)" /></el-select></el-form-item></el-col>
|
<el-col :xs="24" :sm="12"><el-form-item label="承运商" prop="carrierId"><el-select :model-value="draft.carrierId" filterable clearable placeholder="请选择承运商" :loading="optionsLoading" no-data-text="暂无可用拖车供应商" style="width:100%" @update:model-value="selectCarrier(String($event || ''))"><el-option v-if="draft.carrierId && !selectableCarriers.some((item) => String(item.id) === String(draft.carrierId))" :label="draft.carrierName || '当前承运商(需重新选择)'" :value="String(draft.carrierId)" disabled /><el-option v-for="item in selectableCarriers" :key="String(item.id)" :label="item.name" :value="String(item.id)" /></el-select></el-form-item></el-col>
|
||||||
<el-col :xs="24" :sm="12"><el-form-item label="车队联系人"><el-input v-model="draft.fleetContact" placeholder="请输入车队联系人" /></el-form-item></el-col>
|
<el-col :xs="24" :sm="12"><el-form-item label="车队联系人"><el-input v-model="draft.fleetContact" placeholder="请输入车队联系人" /></el-form-item></el-col>
|
||||||
<el-col :xs="24" :sm="12"><el-form-item label="车队电话" prop="fleetPhone"><el-input v-model="draft.fleetPhone" placeholder="请输入车队联系电话" maxlength="32" show-word-limit /></el-form-item></el-col>
|
<el-col :xs="24" :sm="12"><el-form-item label="车队电话" prop="fleetPhone"><el-input v-model="draft.fleetPhone" placeholder="请输入车队联系电话" maxlength="32" show-word-limit /></el-form-item></el-col>
|
||||||
<el-col :xs="24" :sm="12"><el-form-item label="司机"><el-input v-model="draft.driverName" placeholder="请输入司机姓名" /></el-form-item></el-col>
|
<el-col :xs="24" :sm="12"><el-form-item label="司机"><el-input v-model="draft.driverName" placeholder="请输入司机姓名" /></el-form-item></el-col>
|
||||||
|
|||||||
Reference in New Issue
Block a user