优化
This commit is contained in:
@@ -10,6 +10,7 @@ import {
|
||||
listTenantQualifications,
|
||||
type PlatformTenantResource,
|
||||
} from '@/modules/iam'
|
||||
import { listPlatformFirstLegOrders, type PlatformFirstLegOrderResource } from '@/modules/first-leg-orders'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { useAuthStore } from '@/core/auth/auth'
|
||||
import CustomerDetailView from './CustomerDetailView.vue'
|
||||
@@ -32,6 +33,10 @@ vi.mock('@/modules/iam', () => ({
|
||||
qualificationFileName: vi.fn((documentUrl: string) => documentUrl.split('/').at(-1) || '资质文件'),
|
||||
}))
|
||||
|
||||
vi.mock('@/modules/first-leg-orders', () => ({
|
||||
listPlatformFirstLegOrders: vi.fn(),
|
||||
}))
|
||||
|
||||
class ResizeObserverStub implements ResizeObserver {
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
@@ -75,6 +80,17 @@ describe('CustomerDetailView', () => {
|
||||
phone: '13800138000', email: 'ops@example.com', memberType: 'OWNER', memberNo: null, status: 'ACTIVE', roleIds: [], roleNames: ['企业管理员'],
|
||||
}])
|
||||
vi.mocked(listPlatformTenantOrderCounts).mockResolvedValue([{ tenantId: tenant.id, orderCount: 12 }])
|
||||
vi.mocked(listPlatformFirstLegOrders).mockResolvedValue({
|
||||
records: [{
|
||||
id: 'order-1', tenantId: tenant.id, orderNo: 'BY20260807001', customerId: null, customerName: tenant.tenantName,
|
||||
status: 'PROCESSING', transportMode: 'SEA', tradeTerm: null, destinationCountryCode: 'US', destinationWarehouseId: null,
|
||||
destinationWarehouseName: '洛杉矶', expectedShipDate: '2026-08-08', customerRemark: null, pickupMethod: 'WAREHOUSE',
|
||||
receivingWarehouseId: 'warehouse-1', receivingWarehouseName: '深圳头程收货仓', receivingWarehouseAddress: '深圳市', receivingTime: null,
|
||||
channelCode: 'SEA_FCL', channelName: '海运整箱', taxMode: 'DDP', containerMode: 'FULL_CONTAINER', customsInspection: false,
|
||||
destinationType: 'FBA', amazonWarehouseCode: null, shipmentId: null, referenceId: null, expressCompany: null, expressTrackingNo: null,
|
||||
createdAt: '2026-08-07T10:00:00', updatedAt: '2026-08-07T10:00:00',
|
||||
} satisfies PlatformFirstLegOrderResource], page: 1, size: 10, total: 1, pages: 1,
|
||||
})
|
||||
vi.mocked(downloadTenantQualification).mockResolvedValue(new Blob(['image'], { type: 'image/png' }))
|
||||
})
|
||||
|
||||
@@ -93,6 +109,14 @@ describe('CustomerDetailView', () => {
|
||||
expect(document.body.textContent).toContain('已替换')
|
||||
expect(document.body.textContent).toContain('12票')
|
||||
|
||||
const ordersTab = [...document.querySelectorAll<HTMLButtonElement>('.detail-tabs button')]
|
||||
.find((button) => button.textContent?.trim() === '运单记录')
|
||||
ordersTab?.click()
|
||||
await flushPromises()
|
||||
expect(document.body.textContent).toContain('BY20260807001')
|
||||
expect(document.body.textContent).toContain('海运整箱 FCL')
|
||||
expect(listPlatformFirstLegOrders).toHaveBeenCalledWith({ tenantId: tenant.id, page: 1, size: 10 })
|
||||
|
||||
const billingTab = [...document.querySelectorAll<HTMLButtonElement>('.detail-tabs button')]
|
||||
.find((button) => button.textContent?.trim() === '计费规则')
|
||||
billingTab?.click()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import {
|
||||
ArrowLeft, BarChart3, Box, Building2, CircleDollarSign, ClipboardList, Eye,
|
||||
@@ -20,8 +20,15 @@ import {
|
||||
type PlatformTenantResource,
|
||||
type PlatformTenantStatus,
|
||||
} from '@/modules/iam'
|
||||
import {
|
||||
listPlatformFirstLegOrders,
|
||||
type PlatformFirstLegOrderResource,
|
||||
type PlatformFirstLegOrderStatus,
|
||||
} from '@/modules/first-leg-orders'
|
||||
import { getApiErrorDetails, getApiErrorMessage, type ApiErrorDetails } from '@/shared/api/http'
|
||||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
|
||||
import StatusBadge from '@/shared/components/StatusBadge.vue'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import CustomerBillingRulesPanel from '@/modules/customers/components/CustomerBillingRulesPanel.vue'
|
||||
|
||||
@@ -40,6 +47,12 @@ const customer = ref<PlatformTenantResource | null>(null)
|
||||
const qualifications = ref<PlatformTenantQualification[]>([])
|
||||
const members = ref<PlatformTenantMember[]>([])
|
||||
const orderCount = ref<number | null>(null)
|
||||
const customerOrders = ref<PlatformFirstLegOrderResource[]>([])
|
||||
const customerOrderTotal = ref(0)
|
||||
const orderPage = ref(1)
|
||||
const orderPageSize = ref(10)
|
||||
const customerOrdersLoading = ref(false)
|
||||
const customerOrdersError = ref<ApiErrorDetails | null>(null)
|
||||
const previews = ref<Record<string, PreviewState>>({})
|
||||
const previewOpen = ref(false)
|
||||
const previewName = ref('')
|
||||
@@ -59,6 +72,16 @@ const tabs: Array<{ key: DetailTab; label: string; icon: typeof ClipboardList }>
|
||||
const approvedQualifications = computed(() => qualifications.value.filter((item) => item.status === 'APPROVED').length)
|
||||
const enterpriseMembers = computed(() => members.value.filter((member) => ['OWNER', 'EMPLOYEE'].includes(member.memberType)))
|
||||
const activeEnterpriseMembers = computed(() => enterpriseMembers.value.filter((member) => member.status === 'ACTIVE').length)
|
||||
const orderColumns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 92, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'status', label: '状态', width: 106, slotName: 'status' },
|
||||
{ key: 'orderNo', label: '运单号', minWidth: 180, slotName: 'orderNo' },
|
||||
{ key: 'transport', label: '运输方式', minWidth: 150, slotName: 'transport' },
|
||||
{ key: 'destination', label: '目的地', minWidth: 170, slotName: 'destination' },
|
||||
{ key: 'receivingWarehouse', label: '平台收货仓', minWidth: 180, slotName: 'receivingWarehouse' },
|
||||
{ key: 'expectedShipDate', label: '预计出运', width: 128, slotName: 'expectedShipDate' },
|
||||
{ key: 'createdAt', label: '创建时间', width: 150, slotName: 'createdAt' },
|
||||
]
|
||||
|
||||
function displayText(value?: string | null) { return value?.trim() || '—' }
|
||||
function formatDate(value?: string | null) { return value ? value.replace('T', ' ').slice(0, 16) : '—' }
|
||||
@@ -89,6 +112,33 @@ function qualificationName(item: PlatformTenantQualification) {
|
||||
try { return qualificationFileName(item.documentUrl) }
|
||||
catch { return `${qualificationLabel(item.qualificationType)}文件` }
|
||||
}
|
||||
function orderStatusLabel(status: PlatformFirstLegOrderStatus) {
|
||||
return ({ DRAFT: '草稿', SUBMITTED: '待审核', APPROVED: '已审核', ACCEPTED: '已受理', PROCESSING: '处理中', COMPLETED: '已完成', CANCELLED: '已取消', REJECTED: '已驳回(旧契约)' } as Record<PlatformFirstLegOrderStatus, string>)[status] || status
|
||||
}
|
||||
function transportLabel(order: PlatformFirstLegOrderResource) {
|
||||
if (order.transportMode === 'SEA') return order.containerMode === 'FULL_CONTAINER' ? '海运整箱 FCL' : '海运拼箱 LCL'
|
||||
if (order.transportMode === 'AIR') return '空运'
|
||||
if (order.transportMode === 'RAIL') return '铁路运输'
|
||||
return '陆运'
|
||||
}
|
||||
function formattedDate(value?: string | null) { return value ? value.replace('T', ' ').slice(0, 16) : '—' }
|
||||
|
||||
async function loadCustomerOrders(page = orderPage.value) {
|
||||
if (!access.can('platform:order:list')) return
|
||||
customerOrdersLoading.value = true
|
||||
customerOrdersError.value = null
|
||||
try {
|
||||
const result = await listPlatformFirstLegOrders({ tenantId: tenantId.value, page, size: orderPageSize.value })
|
||||
customerOrders.value = result.records
|
||||
customerOrderTotal.value = result.total
|
||||
if (orderCount.value === null) orderCount.value = result.total
|
||||
} catch (error) {
|
||||
customerOrders.value = []
|
||||
customerOrdersError.value = getApiErrorDetails(error, '运单记录加载失败,请稍后重试')
|
||||
} finally {
|
||||
customerOrdersLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function clearPreviews() {
|
||||
for (const preview of Object.values(previews.value)) if (preview.url) URL.revokeObjectURL(preview.url)
|
||||
@@ -145,9 +195,18 @@ async function loadDetail() {
|
||||
|
||||
const optional: Promise<void>[] = []
|
||||
if (access.can('platform:order:list')) {
|
||||
orderPage.value = 1
|
||||
customerOrders.value = []
|
||||
customerOrderTotal.value = 0
|
||||
optional.push(listPlatformTenantOrderCounts([tenantId.value]).then((records) => {
|
||||
orderCount.value = Number(records.find((item) => String(item.tenantId) === tenantId.value)?.orderCount ?? 0)
|
||||
}).catch(() => { orderCount.value = null }))
|
||||
optional.push(loadCustomerOrders(1))
|
||||
} else {
|
||||
customerOrdersLoading.value = false
|
||||
customerOrdersError.value = null
|
||||
customerOrders.value = []
|
||||
customerOrderTotal.value = 0
|
||||
}
|
||||
await Promise.all(optional)
|
||||
} catch (error) {
|
||||
@@ -158,6 +217,10 @@ async function loadDetail() {
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
|
||||
function openOrder(order: PlatformFirstLegOrderResource) {
|
||||
void router.push({ name: 'unified-order-detail', params: { orderId: String(order.id) }, query: { tenantId: tenantId.value } })
|
||||
}
|
||||
|
||||
async function changeStatus() {
|
||||
if (!customer.value || statusChanging.value) return
|
||||
const next = customer.value.status === 'ACTIVE' ? 'DISABLED' : 'ACTIVE'
|
||||
@@ -190,6 +253,9 @@ function handleTabKeydown(event: KeyboardEvent, key: DetailTab) {
|
||||
}
|
||||
onMounted(loadDetail)
|
||||
onBeforeUnmount(clearPreviews)
|
||||
watch(orderPage, (page, previousPage) => {
|
||||
if (page !== previousPage) void loadCustomerOrders(page)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -294,8 +360,22 @@ onBeforeUnmount(clearPreviews)
|
||||
</section>
|
||||
|
||||
<section v-else-if="activeTab === 'orders'" class="detail-panel module-panel">
|
||||
<div class="panel-title"><div><h2>运单记录</h2><p>客户关联头程运单</p></div><Ship :size="19" /></div>
|
||||
<div class="module-empty"><Ship :size="30" /><strong>当前客户累计 {{ orderCount ?? '—' }} 票运单</strong><p>现有平台接口仅提供客户维度运单数量,尚未提供详情页专用明细查询契约。</p><el-button v-if="access.can('platform:order:list')" type="primary" plain @click="router.push('/first-leg-orders')">前往运单管理</el-button></div>
|
||||
<div class="panel-title"><div><h2>运单记录</h2><p>客户关联头程运单,共 {{ customerOrderTotal }} 票</p></div><Ship :size="19" /></div>
|
||||
<template v-if="access.can('platform:order:list')">
|
||||
<el-alert v-if="customerOrdersError" type="error" :closable="false" show-icon role="alert"><template #title><div class="api-error-alert-title"><span>{{ customerOrdersError.message }}</span><ApiErrorMeta :details="customerOrdersError" /></div></template><el-button link type="danger" @click="loadDetail">重新加载</el-button></el-alert>
|
||||
<DataTable v-else v-model:page="orderPage" v-model:page-size="orderPageSize" :data="customerOrders" :columns="orderColumns" :loading="customerOrdersLoading" :total="customerOrderTotal" :page-sizes="[10]" empty-text="当前客户暂无运单记录" row-key="id">
|
||||
<template #actions="{ row }"><el-button link type="primary" @click="openOrder(row as PlatformFirstLegOrderResource)"><Eye :size="15" />详情</el-button></template>
|
||||
<template #status="{ row }"><StatusBadge :status="orderStatusLabel((row as PlatformFirstLegOrderResource).status)" /></template>
|
||||
<template #orderNo="{ row }"><strong>{{ (row as PlatformFirstLegOrderResource).orderNo || '—' }}</strong></template>
|
||||
<template #transport="{ row }"><span>{{ transportLabel(row as PlatformFirstLegOrderResource) }}</span></template>
|
||||
<template #destination="{ row }"><div class="primary-cell"><strong>{{ (row as PlatformFirstLegOrderResource).destinationWarehouseName || '—' }}</strong><span>{{ (row as PlatformFirstLegOrderResource).destinationCountryCode || '—' }}</span></div></template>
|
||||
<template #receivingWarehouse="{ row }"><span>{{ (row as PlatformFirstLegOrderResource).receivingWarehouseName || '—' }}</span></template>
|
||||
<template #expectedShipDate="{ row }"><span>{{ formattedDate((row as PlatformFirstLegOrderResource).expectedShipDate).slice(0, 10) }}</span></template>
|
||||
<template #createdAt="{ row }"><span>{{ formattedDate((row as PlatformFirstLegOrderResource).createdAt) }}</span></template>
|
||||
<template #empty><el-empty description="当前客户暂无运单记录" /></template>
|
||||
</DataTable>
|
||||
</template>
|
||||
<div v-else class="module-empty"><Ship :size="30" /><strong>暂无运单查看权限</strong><p>请联系管理员申请头程订单查看权限。</p></div>
|
||||
</section>
|
||||
|
||||
<section v-else-if="activeTab === 'finance'" class="detail-panel module-panel"><div class="panel-title"><div><h2>财务账单</h2><p>客户应收、回款与对账记录</p></div><ReceiptText :size="19" /></div><div class="module-empty"><ReceiptText :size="30" /><strong>财务账单尚未接入</strong><p>当前阶段没有可用的客户账单 API,页面不会使用原型中的演示金额。</p></div></section>
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export {
|
||||
listPlatformFirstLegOrders,
|
||||
type PlatformFirstLegOrderResource,
|
||||
type PlatformFirstLegOrderStatus,
|
||||
} from './api/platformFirstLegOrders'
|
||||
Reference in New Issue
Block a user