优化
This commit is contained in:
@@ -3,7 +3,7 @@ import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import {
|
||||
ArrowLeft, BarChart3, Building2, Eye,
|
||||
CircleDollarSign, Container, FileCheck2, FileText, Pencil, Power,
|
||||
FileCheck2, FileText, Pencil, Power,
|
||||
Ship, UserRound, UsersRound,
|
||||
} from '@lucide/vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
@@ -20,13 +20,10 @@ import {
|
||||
type PlatformTenantStatus,
|
||||
} from '@/modules/iam'
|
||||
import {
|
||||
getPlatformTenantOrderStatistics,
|
||||
listPlatformFirstLegOrders,
|
||||
type PlatformFirstLegOrderResource,
|
||||
type PlatformFirstLegOrderStatus,
|
||||
} from '@/modules/first-leg-orders'
|
||||
import { getFinanceCustomerAccountSummary } from '@/modules/finance'
|
||||
import { getCustomerContainerTransitSummary } from '@/modules/container-loading'
|
||||
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'
|
||||
@@ -41,11 +38,9 @@ import CustomerBillingRulesPanel from '@/modules/customers/components/CustomerBi
|
||||
import CustomerOverseasStorageFeePanel from '@/modules/customers/CustomerOverseasStorageFeePanel.vue'
|
||||
import { CustomerStatementsPanel } from '@/modules/finance'
|
||||
import { CustomerAuditLogsPanel } from '@/modules/audit'
|
||||
import CustomerOperatingMetricCard from '@/modules/customers/components/CustomerOperatingMetricCard.vue'
|
||||
|
||||
type DetailTab = 'basic' | 'business' | 'orders' | 'statements' | 'billing' | 'logs'
|
||||
type PreviewState = { url: string; kind: 'image' | 'pdf'; loading: boolean; failed: boolean }
|
||||
type MetricState<T> = { loading: boolean; failed: boolean; data: T | null }
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -58,9 +53,6 @@ const loadError = ref<ApiErrorDetails | null>(null)
|
||||
const customer = ref<PlatformTenantResource | null>(null)
|
||||
const qualifications = ref<PlatformTenantQualification[]>([])
|
||||
const members = ref<PlatformTenantMember[]>([])
|
||||
const orderStatistics = ref<MetricState<Awaited<ReturnType<typeof getPlatformTenantOrderStatistics>>>>({ loading: false, failed: false, data: null })
|
||||
const financeStatistics = ref<MetricState<Awaited<ReturnType<typeof getFinanceCustomerAccountSummary>>>>({ loading: false, failed: false, data: null })
|
||||
const transitStatistics = ref<MetricState<Awaited<ReturnType<typeof getCustomerContainerTransitSummary>>>>({ loading: false, failed: false, data: null })
|
||||
const customerOrders = ref<PlatformFirstLegOrderResource[]>([])
|
||||
const customerOrderTotal = ref(0)
|
||||
const orderPage = ref(1)
|
||||
@@ -80,7 +72,7 @@ const tabList = ref<HTMLElement | null>(null)
|
||||
|
||||
const tabs: Array<{ key: DetailTab; label: string; icon: string }> = [
|
||||
{ key: 'basic', label: '基本信息', icon: '📋' },
|
||||
{ key: 'business', label: '业务概况', icon: '📊' },
|
||||
// { key: 'business', label: '业务概况', icon: '📊' },
|
||||
{ key: 'orders', label: '运单记录', icon: '📦' },
|
||||
{ key: 'statements', label: '财务账单', icon: '🧾' },
|
||||
{ key: 'billing', label: '计费规则', icon: '⚖️' },
|
||||
@@ -139,25 +131,6 @@ function transportLabel(order: PlatformFirstLegOrderResource) {
|
||||
return '陆运'
|
||||
}
|
||||
function formattedDate(value?: string | null) { return value ? value.replace('T', ' ').slice(0, 16) : '—' }
|
||||
function decimalText(value: number | string | null | undefined) {
|
||||
const raw = String(value ?? '').trim()
|
||||
if (!/^-?\d+(\.\d+)?$/.test(raw)) return '—'
|
||||
const normalized = raw.replace(/^(-?)0+(?=\d)/, '$1').replace(/\.0+$/, '').replace(/(\.\d*?)0+$/, '$1')
|
||||
return normalized === '-0' ? '0' : normalized
|
||||
}
|
||||
function cnyWan(value: number | string | null | undefined) {
|
||||
const raw = decimalText(value)
|
||||
if (raw === '—') return raw
|
||||
const negative = raw.startsWith('-')
|
||||
const [integer, fraction = ''] = (negative ? raw.slice(1) : raw).split('.')
|
||||
const digits = `${integer}${fraction}`.replace(/^0+(?=\d)/, '') || '0'
|
||||
const scale = fraction.length + 4
|
||||
const padded = digits.padStart(scale + 1, '0')
|
||||
const whole = padded.slice(0, -scale) || '0'
|
||||
const decimals = padded.slice(-scale).replace(/0+$/, '')
|
||||
const result = `${negative && digits !== '0' ? '-' : ''}${whole}${decimals ? `.${decimals}` : ''}`
|
||||
return result
|
||||
}
|
||||
|
||||
async function loadCustomerOrders(page = orderPage.value, requestedTenantId = tenantId.value) {
|
||||
if (!access.can('platform:order:list')) return
|
||||
@@ -178,30 +151,6 @@ async function loadCustomerOrders(page = orderPage.value, requestedTenantId = te
|
||||
}
|
||||
}
|
||||
|
||||
async function loadOperatingStatistics(requestedTenantId: string, serial: number) {
|
||||
const tasks: Promise<void>[] = []
|
||||
const active = () => !disposed && serial === detailRequestSerial && requestedTenantId === tenantId.value
|
||||
if (access.can('platform:order:list')) {
|
||||
orderStatistics.value = { loading: true, failed: false, data: null }
|
||||
tasks.push(getPlatformTenantOrderStatistics(requestedTenantId).then((data) => {
|
||||
if (active()) orderStatistics.value = { loading: false, failed: false, data }
|
||||
}).catch(() => { if (active()) orderStatistics.value = { loading: false, failed: true, data: null } }))
|
||||
}
|
||||
if (access.can('platform:finance:customer-account:list')) {
|
||||
financeStatistics.value = { loading: true, failed: false, data: null }
|
||||
tasks.push(getFinanceCustomerAccountSummary(requestedTenantId).then((data) => {
|
||||
if (active()) financeStatistics.value = { loading: false, failed: false, data }
|
||||
}).catch(() => { if (active()) financeStatistics.value = { loading: false, failed: true, data: null } }))
|
||||
}
|
||||
if (access.can('platform:firstleg:container:list')) {
|
||||
transitStatistics.value = { loading: true, failed: false, data: null }
|
||||
tasks.push(getCustomerContainerTransitSummary(requestedTenantId).then((data) => {
|
||||
if (active()) transitStatistics.value = { loading: false, failed: false, data }
|
||||
}).catch(() => { if (active()) transitStatistics.value = { loading: false, failed: true, data: null } }))
|
||||
}
|
||||
await Promise.all(tasks)
|
||||
}
|
||||
|
||||
function clearPreviews() {
|
||||
for (const preview of Object.values(previews.value)) if (preview.url) URL.revokeObjectURL(preview.url)
|
||||
previews.value = {}
|
||||
@@ -247,9 +196,6 @@ async function loadDetail() {
|
||||
customer.value = null
|
||||
qualifications.value = []
|
||||
members.value = []
|
||||
orderStatistics.value = { loading: false, failed: false, data: null }
|
||||
financeStatistics.value = { loading: false, failed: false, data: null }
|
||||
transitStatistics.value = { loading: false, failed: false, data: null }
|
||||
customerOrders.value = []
|
||||
customerOrderTotal.value = 0
|
||||
customerOrdersError.value = null
|
||||
@@ -267,7 +213,6 @@ async function loadDetail() {
|
||||
url: '', kind: qualificationName(item).toLowerCase().endsWith('.pdf') ? 'pdf' : 'image', loading: false, failed: false,
|
||||
} satisfies PreviewState]))
|
||||
|
||||
void loadOperatingStatistics(requestedTenantId, serial)
|
||||
if (access.can('platform:order:list')) {
|
||||
if (orderPage.value !== 1) skipNextOrderPageLoad = true
|
||||
orderPage.value = 1
|
||||
@@ -382,13 +327,13 @@ watch(orderPage, (page, previousPage) => {
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<template #summary>
|
||||
<!-- <template #summary>
|
||||
<section class="overview-strip" aria-label="客户概况">
|
||||
<CustomerOperatingMetricCard title="累计运单" :value="String(orderStatistics.data?.totalSubmittedOrders ?? '')" unit="票" :detail="`本月 ${orderStatistics.data?.currentMonthSubmittedOrders ?? 0} 票`" tone="blue" :loading="orderStatistics.loading" :failed="orderStatistics.failed" :unauthorized="!access.can('platform:order:list')"><template #icon><Ship :size="20" /></template></CustomerOperatingMetricCard>
|
||||
<CustomerOperatingMetricCard title="累计应收" :value="cnyWan(financeStatistics.data?.totalReceivable)" unit="万元" :detail="`未结清 ${cnyWan(financeStatistics.data?.outstandingReceivable)} 万元`" tone="green" :loading="financeStatistics.loading" :failed="financeStatistics.failed" :unauthorized="!access.can('platform:finance:customer-account:list')"><template #icon><CircleDollarSign :size="20" /></template></CustomerOperatingMetricCard>
|
||||
<CustomerOperatingMetricCard title="在途柜量" :value="decimalText(transitStatistics.data?.totalTeu)" unit="TEU" :detail="`${transitStatistics.data?.containerCount ?? 0} 柜${transitStatistics.data?.unknownContainerCount ? ` · 另有 ${transitStatistics.data.unknownContainerCount} 柜柜型未知,未计入TEU` : ''}`" tone="amber" :loading="transitStatistics.loading" :failed="transitStatistics.failed" :unauthorized="!access.can('platform:firstleg:container:list')"><template #icon><Container :size="20" /></template></CustomerOperatingMetricCard>
|
||||
</section>
|
||||
</template>
|
||||
</template> -->
|
||||
|
||||
<div ref="tabList" class="detail-tabs" role="tablist" aria-label="客户详情模块">
|
||||
<button v-for="tab in tabs" :id="`customer-tab-${tab.key}`" :key="tab.key" type="button" role="tab" :class="{ active: activeTab === tab.key }" :aria-label="tab.label" :aria-selected="activeTab === tab.key" :aria-controls="`customer-panel-${tab.key}`" :tabindex="activeTab === tab.key ? 0 : -1" @click="selectTab(tab.key, $event.currentTarget as HTMLElement)" @keydown="handleTabKeydown($event, tab.key)"><span class="detail-tab-icon" aria-hidden="true">{{ tab.icon }}</span><span>{{ tab.label }}</span></button>
|
||||
@@ -446,7 +391,7 @@ watch(orderPage, (page, previousPage) => {
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<section v-else-if="activeTab === 'business'" class="detail-panel module-panel">
|
||||
<!-- <section v-else-if="activeTab === 'business'" class="detail-panel module-panel">
|
||||
<div class="panel-title"><div><h2>业务概况</h2><p>基于当前已接入服务的客户业务摘要</p></div><BarChart3 :size="19" /></div>
|
||||
<div class="contract-note"><strong>趋势、港口与收入分析暂未接入</strong><p>原型中的近 6 个月趋势、运输方式分布、目的港和收入贡献依赖后续统计与财务服务;当前不展示模拟图表。</p></div>
|
||||
</section>
|
||||
@@ -468,7 +413,7 @@ watch(orderPage, (page, previousPage) => {
|
||||
</DataTable>
|
||||
</template>
|
||||
<div v-else class="module-empty"><Ship :size="30" /><strong>暂无运单查看权限</strong><p>请联系管理员申请头程订单查看权限。</p></div>
|
||||
</section>
|
||||
</section> -->
|
||||
|
||||
<section v-else-if="activeTab === 'statements'" class="detail-panel module-panel"><CustomerStatementsPanel :customer-id="tenantId" :customer-name="customer.tenantName" /></section>
|
||||
<section v-else-if="activeTab === 'logs'" class="detail-panel module-panel"><CustomerAuditLogsPanel :customer-id="tenantId" :customer-name="customer.tenantName" /></section>
|
||||
@@ -490,8 +435,6 @@ watch(orderPage, (page, previousPage) => {
|
||||
.detail-page-toolbar { min-width:0; flex:1; }
|
||||
.header-actions { display:flex; flex:0 0 auto; gap:8px; }
|
||||
.header-actions :deep(.el-button + .el-button) { margin-left:0; }
|
||||
.overview-strip { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); overflow:hidden; border:1px solid rgba(255,255,255,.5); border-radius:12px; background:rgba(255,255,255,.95); box-shadow:0 3px 12px rgba(15,23,42,.05); }
|
||||
.overview-strip :deep(.operating-metric-card:last-child) { border-right:0; }
|
||||
.detail-tabs { display:flex; gap:0; overflow-x:auto; overflow-y:hidden; border-bottom:1px solid rgba(228,228,231,.5); border-radius:20px 20px 0 0; background:rgba(255,255,255,.95); box-shadow:0 -4px 16px rgba(0,0,0,.04); scrollbar-width:thin; }
|
||||
.detail-tabs button { display:inline-flex; min-width:max-content; min-height:0; flex:0 0 auto; align-items:center; justify-content:center; gap:8px; padding:16px 28px; border:0; border-bottom:3px solid transparent; background:transparent; color:#71717a; font-size:15px; font-weight:600; white-space:nowrap; cursor:pointer; transition:all .3s cubic-bezier(.4,0,.2,1); }
|
||||
.detail-tab-icon { font-size:18px; line-height:1; }
|
||||
@@ -522,6 +465,6 @@ table { width:100%; min-width:720px; border-collapse:collapse; font-size:12px; }
|
||||
.contract-note { padding:16px 18px; border-radius:10px; background:#f8fafc; }.contract-note strong { color:#344054; font-size:12px; }.contract-note p { margin:5px 0 0; color:var(--cc-muted); font-size:11px; line-height:1.7; }
|
||||
.qualification-preview { display:grid; min-height:320px; max-height:72vh; overflow:auto; place-items:center; border-radius:10px; background:#f8fafc; }.qualification-preview img { display:block; max-width:100%; height:auto; }.qualification-preview iframe { width:100%; min-height:68vh; border:0; background:#fff; }
|
||||
@media (max-width:960px) { .basic-layout { grid-template-columns:1fr; }.contact-panel,.user-panel { grid-column:auto; }.qualification-panel { order:3; }.user-panel { order:4; } }
|
||||
@media (max-width:720px) { .detail-header { align-items:flex-start; flex-wrap:wrap; }.detail-page-toolbar { width:100%; flex-basis:100%; }.company-mark { width:40px; height:40px; }.header-actions { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); width:100%; }.header-actions :deep(.el-tag) { grid-column:1 / -1; justify-self:start; }.header-actions :deep(.el-button) { min-height:44px; }.overview-strip { grid-template-columns:1fr; }.overview-strip :deep(.operating-metric-card:last-child) { border-bottom:0; }.detail-panel { padding:16px; }.info-grid { grid-template-columns:1fr; }.info-grid > div.wide { grid-column:auto; } }
|
||||
@media (max-width:720px) { .detail-header { align-items:flex-start; flex-wrap:wrap; }.detail-page-toolbar { width:100%; flex-basis:100%; }.company-mark { width:40px; height:40px; }.header-actions { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); width:100%; }.header-actions :deep(.el-tag) { grid-column:1 / -1; justify-self:start; }.header-actions :deep(.el-button) { min-height:44px; }.detail-panel { padding:16px; }.info-grid { grid-template-columns:1fr; }.info-grid > div.wide { grid-column:auto; } }
|
||||
@media (max-width:420px) { .company-mark { display:none; }.header-actions { grid-template-columns:1fr; }.header-actions :deep(.el-tag) { grid-column:auto; }.qualification-list button { grid-template-columns:36px minmax(0,1fr) auto; }.qualification-list .file-date,.qualification-list button > svg { display:none; } }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user