优化构架
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import test from 'node:test'
|
||||
|
||||
const source = readFileSync(new URL('./DashboardView.vue', import.meta.url), 'utf8')
|
||||
|
||||
test('loads real dashboard data allowed by the current permissions', () => {
|
||||
assert.match(source, /auth\.hasPermission\('company:profile:view'\).*tenant\.loadCompanyContext\(\)/)
|
||||
assert.match(source, /auth\.hasPermission\('company:member:list'\).*tenant\.loadMembers\(\)/)
|
||||
assert.match(source, /auth\.hasPermission\('order:first-leg:list'\).*orders\.load\(\)/)
|
||||
assert.match(source, /onMounted\(\(\) => void loadDashboardData\(\)\)/)
|
||||
})
|
||||
|
||||
test('isolates recoverable dashboard failures without hiding successful sections', () => {
|
||||
assert.match(source, /Promise\.allSettled/)
|
||||
assert.match(source, /Partial<Record<DashboardDataSource, string>>/)
|
||||
assert.match(source, /:aria-busy="loading"/)
|
||||
assert.match(source, /role="alert"/)
|
||||
assert.match(source, /部分业务数据加载失败/)
|
||||
assert.match(source, /ordersUnavailable/)
|
||||
assert.match(source, /companyUnavailable/)
|
||||
assert.match(source, /订单数据暂不可用,请重新加载后查看。/)
|
||||
assert.match(source, /企业资料暂不可用,请重新加载后查看。/)
|
||||
assert.match(source, /@click="loadDashboardData"/)
|
||||
assert.doesNotMatch(source, /<template v-else>\s*<section class="quick-order-band">/)
|
||||
})
|
||||
@@ -0,0 +1,121 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { AlertTriangle, ArrowRight, ClipboardCheck, Clock3, FilePlus2, Users } from '@lucide/vue'
|
||||
import { useFirstLegOrdersStore } from '@/stores/firstLegOrders'
|
||||
import { useTenantAdministrationStore } from '@/stores/tenantAdministration'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { getFirstLegStatusLabel, getFirstLegStatusTone } from '@/utils/firstLegOrder'
|
||||
|
||||
const orders = useFirstLegOrdersStore()
|
||||
const tenant = useTenantAdministrationStore()
|
||||
const auth = useAuthStore()
|
||||
const loading = ref(false)
|
||||
type DashboardDataSource = 'company' | 'members' | 'orders'
|
||||
const loadErrors = ref<Partial<Record<DashboardDataSource, string>>>({})
|
||||
const dataSourceLabels: Record<DashboardDataSource, string> = {
|
||||
company: '企业资料',
|
||||
members: '企业成员',
|
||||
orders: '头程订单',
|
||||
}
|
||||
const companyApproved = computed(() => tenant.company.auditStatus === 'ACTIVE')
|
||||
const companyUnavailable = computed(() => Boolean(loadErrors.value.company))
|
||||
const membersUnavailable = computed(() => Boolean(loadErrors.value.members))
|
||||
const ordersUnavailable = computed(() => Boolean(loadErrors.value.orders))
|
||||
const loadErrorItems = computed(() =>
|
||||
(Object.keys(loadErrors.value) as DashboardDataSource[]).map((key) => ({
|
||||
key,
|
||||
label: dataSourceLabels[key],
|
||||
message: loadErrors.value[key] ?? '请求失败',
|
||||
})),
|
||||
)
|
||||
const stats = computed(() => [
|
||||
...(auth.hasPermission('order:first-leg:list')
|
||||
? [
|
||||
{ label: '进行中订单', value: ordersUnavailable.value ? '—' : String(orders.activeCount), icon: Clock3 },
|
||||
{ label: '待平台审核', value: ordersUnavailable.value ? '—' : String(orders.pendingCount), icon: ClipboardCheck },
|
||||
]
|
||||
: []),
|
||||
...(auth.hasPermission('company:member:list')
|
||||
? [{ label: '企业成员', value: membersUnavailable.value ? '—' : String(tenant.activeMemberCount), icon: Users }]
|
||||
: []),
|
||||
])
|
||||
const recentOrders = computed(() => orders.orders.slice(0, 3))
|
||||
|
||||
function errorMessage(error: unknown) {
|
||||
return error instanceof Error ? error.message : '请求失败,请稍后重试'
|
||||
}
|
||||
|
||||
async function loadDashboardData() {
|
||||
loading.value = true
|
||||
loadErrors.value = {}
|
||||
const requests: Array<{ source: DashboardDataSource; request: Promise<unknown> }> = []
|
||||
if (auth.hasPermission('company:profile:view')) requests.push({ source: 'company', request: tenant.loadCompanyContext() })
|
||||
if (auth.hasPermission('company:member:list')) requests.push({ source: 'members', request: tenant.loadMembers() })
|
||||
if (auth.hasPermission('order:first-leg:list')) requests.push({ source: 'orders', request: orders.load() })
|
||||
try {
|
||||
const results = await Promise.allSettled(requests.map(({ request }) => request))
|
||||
const nextErrors: Partial<Record<DashboardDataSource, string>> = {}
|
||||
results.forEach((result, index) => {
|
||||
const request = requests[index]
|
||||
if (request && result.status === 'rejected') nextErrors[request.source] = errorMessage(result.reason)
|
||||
})
|
||||
loadErrors.value = nextErrors
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => void loadDashboardData())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-loading="loading" class="client-dashboard" :aria-busy="loading">
|
||||
<div v-if="loadErrorItems.length" class="dashboard-load-error" role="alert">
|
||||
<AlertTriangle :size="20" />
|
||||
<div>
|
||||
<strong>部分业务数据加载失败</strong>
|
||||
<ul>
|
||||
<li v-for="item in loadErrorItems" :key="item.key">
|
||||
<span>{{ item.label }}:</span>{{ item.message }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<el-button :loading="loading" @click="loadDashboardData">重新加载</el-button>
|
||||
</div>
|
||||
<section class="quick-order-band">
|
||||
<div>
|
||||
<span>头程物流</span>
|
||||
<h2>{{ auth.hasPermission('order:first-leg:create') ? '创建新的头程订单' : '跟进企业头程订单' }}</h2>
|
||||
<p v-if="companyUnavailable">企业资料暂不可用,恢复后可查看下单资格。</p>
|
||||
<p v-else>{{ companyApproved ? '企业资料已审核,可正常提交并跟进订单。' : '企业资料待平台审核,当前可以维护并保存订单草稿。' }}</p>
|
||||
</div>
|
||||
<el-button v-if="auth.hasPermission('order:first-leg:list')" type="primary" size="large" @click="$router.push('/orders/first-leg')"><FilePlus2 :size="18" /> {{ auth.hasPermission('order:first-leg:create') ? '开始下单' : '查看订单' }}</el-button>
|
||||
</section>
|
||||
<section v-if="stats.length" class="client-stats">
|
||||
<article v-for="item in stats" :key="item.label"><component :is="item.icon" :size="20" /><div><span>{{ item.label }}</span><strong>{{ item.value }}</strong></div></article>
|
||||
</section>
|
||||
<section class="client-content-grid">
|
||||
<article v-if="auth.hasPermission('order:first-leg:list')" class="client-card">
|
||||
<header><div><span class="section-label">ORDERS</span><h3>最近订单</h3></div><el-button text @click="$router.push('/orders/first-leg')">查看全部 <ArrowRight :size="16" /></el-button></header>
|
||||
<div v-if="ordersUnavailable" class="dashboard-section-error" role="status">订单数据暂不可用,请重新加载后查看。</div>
|
||||
<div v-else-if="recentOrders.length" class="recent-orders"><button v-for="order in recentOrders" :key="order.id" type="button" @click="$router.push('/orders/first-leg')"><span><strong>{{ order.orderNo || '未提交草稿' }}</strong><small>{{ order.origin }} → {{ order.destination }}</small></span><el-tag :type="getFirstLegStatusTone(order.status)" effect="light">{{ getFirstLegStatusLabel(order.status) }}</el-tag></button></div>
|
||||
<div v-else class="client-empty">暂无订单,提交第一笔头程订单后将在这里展示。</div>
|
||||
</article>
|
||||
<article v-if="auth.hasPermission('company:profile:view')" class="client-card">
|
||||
<header><div><span class="section-label">ACCOUNT</span><h3>企业资料</h3></div></header>
|
||||
<div v-if="companyUnavailable" class="dashboard-section-error" role="status">企业资料暂不可用,请重新加载后查看。</div>
|
||||
<template v-else>
|
||||
<dl class="profile-summary"><div><dt>企业名称</dt><dd>{{ tenant.company.companyName || '-' }}</dd></div><div><dt>审核状态</dt><dd><el-tag :type="companyApproved ? 'success' : 'warning'" effect="light">{{ companyApproved ? '已通过' : '待审核' }}</el-tag></dd></div><div><dt>当前成员</dt><dd>{{ auth.user?.name }}</dd></div></dl>
|
||||
<el-button plain @click="$router.push('/company/profile')">{{ companyApproved ? '查看企业资料' : '完善企业资料' }}</el-button>
|
||||
</template>
|
||||
</article>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.dashboard-load-error ul { display: grid; gap: 3px; margin: 0; padding: 0; list-style: none; }
|
||||
.dashboard-load-error li { overflow-wrap: anywhere; color: var(--muted); font-size: 12px; line-height: 1.5; }
|
||||
.dashboard-load-error li span { color: #9a5a09; font-weight: 650; }
|
||||
.dashboard-section-error { display: grid; min-height: 160px; place-items: center; padding: 24px; border-top: 1px solid var(--border); color: #8a5a22; font-size: 13px; line-height: 1.6; text-align: center; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user