优化
This commit is contained in:
@@ -5,6 +5,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
downloadTenantQualification,
|
||||
getPlatformTenant,
|
||||
listPlatformTenantMembers,
|
||||
listPlatformTenantOrderCounts,
|
||||
listTenantQualifications,
|
||||
type PlatformTenantResource,
|
||||
@@ -25,6 +26,7 @@ vi.mock('@/modules/iam', () => ({
|
||||
changePlatformTenantStatus: vi.fn(),
|
||||
downloadTenantQualification: vi.fn(),
|
||||
getPlatformTenant: vi.fn(),
|
||||
listPlatformTenantMembers: vi.fn(),
|
||||
listPlatformTenantOrderCounts: vi.fn(),
|
||||
listTenantQualifications: vi.fn(),
|
||||
qualificationFileName: vi.fn((documentUrl: string) => documentUrl.split('/').at(-1) || '资质文件'),
|
||||
@@ -68,19 +70,26 @@ describe('CustomerDetailView', () => {
|
||||
id: 'qualification-1', tenantId: tenant.id, qualificationType: 'BUSINESS_LICENSE', certificateNo: tenant.unifiedSocialCreditCode,
|
||||
documentUrl: '/files/license.png', status: 'SUPERSEDED', reviewComment: '已替换', validTo: null,
|
||||
}])
|
||||
vi.mocked(listPlatformTenantMembers).mockResolvedValue([{
|
||||
membershipId: 'member-1', userId: 'user-1', authUserId: 'auth-1', username: 'merchant_admin', displayName: '张三',
|
||||
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(downloadTenantQualification).mockResolvedValue(new Blob(['image'], { type: 'image/png' }))
|
||||
})
|
||||
|
||||
it('renders all six modules without inventing unavailable tenant users', async () => {
|
||||
it('renders enterprise users and includes the owner in customer statistics', async () => {
|
||||
mount(CustomerDetailView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
|
||||
await flushPromises()
|
||||
|
||||
expect([...document.querySelectorAll('.detail-tabs button')].map((button) => button.textContent?.trim())).toEqual([
|
||||
'基本信息', '业务概况', '运单记录', '财务账单', '操作日志', '计费规则',
|
||||
])
|
||||
expect(document.body.textContent).toContain('企业成员数据暂不可用')
|
||||
expect(document.body.textContent).not.toContain('0 人正常使用')
|
||||
expect(document.body.textContent).toContain('企业用户1人')
|
||||
expect(document.body.textContent).toContain('1 人正常使用')
|
||||
expect(document.body.textContent).toContain('merchant_admin')
|
||||
expect(document.body.textContent).toContain('企业管理员')
|
||||
expect(listPlatformTenantMembers).toHaveBeenCalledWith(tenant.id)
|
||||
expect(document.body.textContent).toContain('已替换')
|
||||
expect(document.body.textContent).toContain('12票')
|
||||
|
||||
|
||||
@@ -11,10 +11,12 @@ import {
|
||||
changePlatformTenantStatus,
|
||||
downloadTenantQualification,
|
||||
getPlatformTenant,
|
||||
listPlatformTenantMembers,
|
||||
listPlatformTenantOrderCounts,
|
||||
listTenantQualifications,
|
||||
qualificationFileName,
|
||||
type PlatformTenantQualification,
|
||||
type PlatformTenantMember,
|
||||
type PlatformTenantResource,
|
||||
type PlatformTenantStatus,
|
||||
} from '@/modules/iam'
|
||||
@@ -36,6 +38,7 @@ const statusChanging = ref(false)
|
||||
const loadError = ref<ApiErrorDetails | null>(null)
|
||||
const customer = ref<PlatformTenantResource | null>(null)
|
||||
const qualifications = ref<PlatformTenantQualification[]>([])
|
||||
const members = ref<PlatformTenantMember[]>([])
|
||||
const orderCount = ref<number | null>(null)
|
||||
const previews = ref<Record<string, PreviewState>>({})
|
||||
const previewOpen = ref(false)
|
||||
@@ -54,6 +57,8 @@ 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)
|
||||
|
||||
function displayText(value?: string | null) { return value?.trim() || '—' }
|
||||
function formatDate(value?: string | null) { return value ? value.replace('T', ' ').slice(0, 16) : '—' }
|
||||
@@ -126,12 +131,14 @@ async function loadDetail() {
|
||||
loadError.value = null
|
||||
clearPreviews()
|
||||
try {
|
||||
const [tenantResult, qualificationResult] = await Promise.all([
|
||||
const [tenantResult, qualificationResult, memberResult] = await Promise.all([
|
||||
getPlatformTenant(tenantId.value),
|
||||
listTenantQualifications(tenantId.value),
|
||||
listPlatformTenantMembers(tenantId.value),
|
||||
])
|
||||
customer.value = tenantResult
|
||||
qualifications.value = qualificationResult
|
||||
members.value = memberResult
|
||||
previews.value = Object.fromEntries(qualificationResult.map((item) => [String(item.id), {
|
||||
url: '', kind: qualificationName(item).toLowerCase().endsWith('.pdf') ? 'pdf' : 'image', loading: false, failed: false,
|
||||
} satisfies PreviewState]))
|
||||
@@ -146,6 +153,7 @@ async function loadDetail() {
|
||||
} catch (error) {
|
||||
customer.value = null
|
||||
qualifications.value = []
|
||||
members.value = []
|
||||
loadError.value = getApiErrorDetails(error, '客户详情加载失败,请稍后重试')
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
@@ -219,7 +227,7 @@ onBeforeUnmount(clearPreviews)
|
||||
<div v-loading="loading" class="detail-content">
|
||||
<section class="overview-strip" aria-label="客户概况">
|
||||
<article><span class="metric-icon is-blue"><Ship :size="20" /></span><div><p>累计运单</p><strong>{{ orderCount ?? '—' }}<small>票</small></strong><span>{{ access.can('platform:order:list') ? '来自头程订单服务' : '暂无运单查看权限' }}</span></div></article>
|
||||
<article><span class="metric-icon is-green"><UsersRound :size="20" /></span><div><p>企业用户</p><strong>—<small>人</small></strong><span>企业成员接口待接入</span></div></article>
|
||||
<article><span class="metric-icon is-green"><UsersRound :size="20" /></span><div><p>企业用户</p><strong>{{ enterpriseMembers.length }}<small>人</small></strong><span>{{ activeEnterpriseMembers }} 人正常使用</span></div></article>
|
||||
<article><span class="metric-icon is-amber"><FileCheck2 :size="20" /></span><div><p>资质文件</p><strong>{{ qualifications.length }}<small>份</small></strong><span>{{ approvedQualifications }} 份已通过</span></div></article>
|
||||
</section>
|
||||
|
||||
@@ -263,14 +271,25 @@ onBeforeUnmount(clearPreviews)
|
||||
</section>
|
||||
<section class="detail-panel user-panel">
|
||||
<div class="panel-title"><div><h2>企业用户</h2><p>当前客户租户下的登录账号</p></div><UsersRound :size="19" /></div>
|
||||
<div class="section-empty"><UsersRound :size="22" /><strong>企业成员数据暂不可用</strong><span>当前平台用户接口仅返回平台内部账号,不能据此判断该客户是否已有企业用户</span></div>
|
||||
<div v-if="enterpriseMembers.length" class="user-table-wrap">
|
||||
<table><thead><tr><th>账号</th><th>成员类型</th><th>联系方式</th><th>角色</th><th>状态</th></tr></thead>
|
||||
<tbody><tr v-for="member in enterpriseMembers" :key="String(member.membershipId)">
|
||||
<td><div class="user-identity"><span>{{ (member.displayName || member.username || '?').slice(0, 1) }}</span><div><strong>{{ displayText(member.username) }}</strong><small>{{ displayText(member.displayName) }}</small></div></div></td>
|
||||
<td>{{ member.memberType === 'OWNER' ? '企业管理员' : '企业员工' }}</td>
|
||||
<td><div class="member-contact"><span>{{ displayText(member.phone) }}</span><small>{{ displayText(member.email) }}</small></div></td>
|
||||
<td><div v-if="member.roleNames?.length" class="role-list"><el-tag v-for="role in member.roleNames" :key="role" size="small" effect="plain">{{ role }}</el-tag></div><span v-else>—</span></td>
|
||||
<td><el-tag :type="member.status === 'ACTIVE' ? 'success' : 'info'" effect="light" size="small">{{ member.status === 'ACTIVE' ? '启用' : '停用' }}</el-tag></td>
|
||||
</tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div v-else class="section-empty"><UsersRound :size="22" /><strong>暂无企业用户</strong><span>当前客户尚未创建企业成员账号</span></div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<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="business-summary"><div><Ship :size="20" /><span>累计运单</span><strong>{{ orderCount ?? '—' }} 票</strong></div><div><UsersRound :size="20" /><span>活跃企业用户</span><strong>— 人</strong></div><div><FileCheck2 :size="20" /><span>有效资质</span><strong>{{ approvedQualifications }} 份</strong></div></div>
|
||||
<div class="business-summary"><div><Ship :size="20" /><span>累计运单</span><strong>{{ orderCount ?? '—' }} 票</strong></div><div><UsersRound :size="20" /><span>活跃企业用户</span><strong>{{ activeEnterpriseMembers }} 人</strong></div><div><FileCheck2 :size="20" /><span>有效资质</span><strong>{{ approvedQualifications }} 份</strong></div></div>
|
||||
<div class="contract-note"><strong>趋势、港口与收入分析暂未接入</strong><p>原型中的近 6 个月趋势、运输方式分布、目的港和收入贡献依赖后续统计与财务服务;当前不展示模拟图表。</p></div>
|
||||
</section>
|
||||
|
||||
@@ -338,6 +357,7 @@ onBeforeUnmount(clearPreviews)
|
||||
.user-table-wrap { overflow-x:auto; }
|
||||
table { width:100%; min-width:720px; border-collapse:collapse; font-size:12px; }th { padding:10px 12px; background:#f8fafc; color:#667085; text-align:left; font-weight:650; }td { padding:12px; border-top:1px solid #edf0f4; color:#344054; }
|
||||
.user-identity { display:flex; align-items:center; gap:9px; }.user-identity > span { display:grid; width:34px; height:34px; flex:0 0 auto; place-items:center; border-radius:50%; background:#eef1ff; color:var(--cc-primary); font-weight:700; }.user-identity strong,.user-identity small { display:block; }.user-identity small { margin-top:3px; color:var(--cc-muted); font-size:10px; }.role-list { display:flex; flex-wrap:wrap; gap:5px; }
|
||||
.member-contact span,.member-contact small { display:block; }.member-contact small { margin-top:3px; color:var(--cc-muted); font-size:10px; }
|
||||
.module-panel { min-height:360px; }.module-empty { min-height:260px; }.module-empty > svg { color:#8797b2; }.module-empty p { max-width:520px; font-size:12px; }
|
||||
.business-summary { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); overflow:hidden; border:1px solid var(--cc-border); border-radius:12px; }.business-summary > div { display:grid; min-height:130px; place-items:center; align-content:center; gap:8px; border-right:1px solid var(--cc-border); }.business-summary > div:last-child { border-right:0; }.business-summary svg { color:var(--cc-primary); }.business-summary span { color:var(--cc-muted); font-size:11px; }.business-summary strong { color:#25324a; font-size:20px; }
|
||||
.contract-note { margin-top:18px; 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; }
|
||||
|
||||
Reference in New Issue
Block a user