fix(fleet): preserve drafts before dialog close and respect dashboard permissions
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import DashboardView from './DashboardView.vue'
|
||||
import { getTodayNewOrderCount } from '../api/dashboard'
|
||||
import { getTodayNewOrderCount, getActiveCustomerCount, getDashboardOperationalMetrics } from '../api/dashboard'
|
||||
|
||||
const access = vi.hoisted(() => ({ permissions: new Set<string>() }))
|
||||
vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: (permission: string) => access.permissions.has(permission) }) }))
|
||||
|
||||
vi.mock('../api/dashboard', () => ({
|
||||
getTodayNewOrderCount: vi.fn().mockResolvedValue(12),
|
||||
@@ -18,7 +21,28 @@ vi.mock('../api/dashboard', () => ({
|
||||
enableAutoUnmount(afterEach)
|
||||
|
||||
describe('DashboardView', () => {
|
||||
beforeEach(() => vi.clearAllMocks())
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
access.permissions = new Set(['platform:order:list', 'platform:customer:list', 'platform:firstleg:container:list', 'platform:firstleg:customs:list'])
|
||||
})
|
||||
|
||||
it('makes no metric requests for a minimal pool sales account and gives an authorized-menu hint', async () => {
|
||||
access.permissions = new Set(['platform:fleet:capacity-offer:list', 'platform:fleet:capacity-offer:inquiry-create', 'platform:fleet:capacity-offer:inquiry-edit', 'platform:fleet:capacity-offer:inquiry-cancel'])
|
||||
const wrapper = mount(DashboardView); await flushPromises()
|
||||
expect(getTodayNewOrderCount).not.toHaveBeenCalled(); expect(getActiveCustomerCount).not.toHaveBeenCalled(); expect(getDashboardOperationalMetrics).not.toHaveBeenCalled()
|
||||
expect(wrapper.findAll('.dashboard-metric')).toHaveLength(0)
|
||||
expect(wrapper.text()).toContain('请从左侧菜单进入已授权业务')
|
||||
expect(wrapper.text()).not.toContain('指标加载失败'); expect(wrapper.text()).not.toContain('刷新数据')
|
||||
})
|
||||
|
||||
it('requests only granted metric sources and requires both container and customs permissions', async () => {
|
||||
access.permissions = new Set(['platform:customer:list', 'platform:firstleg:container:list'])
|
||||
const wrapper = mount(DashboardView); await flushPromises()
|
||||
expect(getActiveCustomerCount).toHaveBeenCalledOnce(); expect(getTodayNewOrderCount).not.toHaveBeenCalled(); expect(getDashboardOperationalMetrics).not.toHaveBeenCalled()
|
||||
expect(wrapper.findAll('.dashboard-metric p').map(node => node.text())).toEqual(['活跃客户'])
|
||||
await wrapper.findAll('button').find(button => button.text().trim() === '刷新数据')!.trigger('click'); await flushPromises()
|
||||
expect(getActiveCustomerCount).toHaveBeenCalledTimes(2); expect(getDashboardOperationalMetrics).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('renders only the four live metrics', async () => {
|
||||
const wrapper = mount(DashboardView)
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
RefreshCw,
|
||||
} from '@lucide/vue'
|
||||
import AdminPage from '@/shared/components/AdminPage.vue'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import {
|
||||
getActiveCustomerCount,
|
||||
getDashboardOperationalMetrics,
|
||||
@@ -22,6 +23,11 @@ const todayNewOrderCount = ref<number | null>(null)
|
||||
const activeCustomerCount = ref<number | null>(null)
|
||||
const operationalMetrics = ref<DashboardOperationalMetrics | null>(null)
|
||||
const metricsUpdatedAt = ref<string | null>(null)
|
||||
const access = useAccessPreviewStore()
|
||||
const canReadOrders = computed(() => access.can('platform:order:list'))
|
||||
const canReadCustomers = computed(() => access.can('platform:customer:list'))
|
||||
const canReadOperations = computed(() => access.can('platform:firstleg:container:list') && access.can('platform:firstleg:customs:list'))
|
||||
const hasMetrics = computed(() => canReadOrders.value || canReadCustomers.value || canReadOperations.value)
|
||||
|
||||
function displayNumber(value: number | null | undefined, maximumFractionDigits = 0) {
|
||||
if (metricsLoading.value) return '…'
|
||||
@@ -30,17 +36,18 @@ function displayNumber(value: number | null | undefined, maximumFractionDigits =
|
||||
}
|
||||
|
||||
const metrics = computed(() => [
|
||||
{ label: '今日新运单', value: displayNumber(todayNewOrderCount.value), unit: '票', note: '按今日创建时间统计', tone: 'blue' },
|
||||
{ label: '今日新运单', value: displayNumber(todayNewOrderCount.value), unit: '票', note: '按今日创建时间统计', tone: 'blue', visible: canReadOrders.value },
|
||||
{
|
||||
label: '在途柜量', value: displayNumber(operationalMetrics.value?.inTransitTeu, 2), unit: 'TEU',
|
||||
note: operationalMetrics.value
|
||||
? `已开船未到仓 · ${operationalMetrics.value.inTransitContainerCount} 柜${operationalMetrics.value.unknownContainerTypeCount ? ` · ${operationalMetrics.value.unknownContainerTypeCount} 柜箱型未知` : ''}`
|
||||
: '已开船且尚未到仓',
|
||||
tone: 'green',
|
||||
visible: canReadOperations.value,
|
||||
},
|
||||
{ label: '活跃客户', value: displayNumber(activeCustomerCount.value), unit: '家', note: '客户状态为启用', tone: 'purple' },
|
||||
{ label: '待清关票数', value: displayNumber(operationalMetrics.value?.pendingCustomsOrderCount), unit: '票', note: '已进入清关且尚未完成', tone: 'rose' },
|
||||
])
|
||||
{ label: '活跃客户', value: displayNumber(activeCustomerCount.value), unit: '家', note: '客户状态为启用', tone: 'purple', visible: canReadCustomers.value },
|
||||
{ label: '待清关票数', value: displayNumber(operationalMetrics.value?.pendingCustomsOrderCount), unit: '票', note: '已进入清关且尚未完成', tone: 'rose', visible: canReadOperations.value },
|
||||
].filter(metric => metric.visible))
|
||||
|
||||
async function toggleFullscreen() {
|
||||
if (!document.fullscreenElement) await dashboardRef.value?.requestFullscreen()
|
||||
@@ -55,17 +62,17 @@ async function loadMetrics() {
|
||||
metricsLoading.value = true
|
||||
metricsError.value = false
|
||||
const [orders, customers, operations] = await Promise.allSettled([
|
||||
getTodayNewOrderCount(),
|
||||
getActiveCustomerCount(),
|
||||
getDashboardOperationalMetrics(),
|
||||
canReadOrders.value ? getTodayNewOrderCount() : Promise.resolve(null),
|
||||
canReadCustomers.value ? getActiveCustomerCount() : Promise.resolve(null),
|
||||
canReadOperations.value ? getDashboardOperationalMetrics() : Promise.resolve(null),
|
||||
])
|
||||
todayNewOrderCount.value = orders.status === 'fulfilled' ? orders.value : null
|
||||
activeCustomerCount.value = customers.status === 'fulfilled' ? customers.value : null
|
||||
operationalMetrics.value = operations.status === 'fulfilled' ? operations.value : null
|
||||
metricsError.value = [orders, customers, operations].some(result => result.status === 'rejected')
|
||||
metricsUpdatedAt.value = operations.status === 'fulfilled' && operations.value.asOf
|
||||
metricsUpdatedAt.value = operations.status === 'fulfilled' && operations.value?.asOf
|
||||
? operations.value.asOf
|
||||
: dayjs().format('YYYY-MM-DDTHH:mm:ss')
|
||||
: hasMetrics.value ? dayjs().format('YYYY-MM-DDTHH:mm:ss') : null
|
||||
metricsLoading.value = false
|
||||
}
|
||||
|
||||
@@ -80,10 +87,10 @@ onBeforeUnmount(() => document.removeEventListener('fullscreenchange', syncFulls
|
||||
<section ref="dashboardRef" class="dashboard-view">
|
||||
<AdminPage as="div" spacing="normal">
|
||||
<section class="dashboard-context" aria-label="数据看板说明">
|
||||
<p>{{ metricsError ? '部分核心指标加载失败,可刷新重试。' : '核心指标来自实时业务数据。' }}</p>
|
||||
<p>{{ !hasMetrics ? '当前账号暂无可查看的首页指标,请从左侧菜单进入已授权业务。' : metricsError ? '部分核心指标加载失败,可刷新重试。' : '核心指标来自实时业务数据。' }}</p>
|
||||
<div class="dashboard-head-actions">
|
||||
<span>数据更新时间:{{ metricsUpdatedAt ? dayjs(metricsUpdatedAt).format('YYYY-MM-DD HH:mm') : '加载中' }}</span>
|
||||
<button type="button" class="dashboard-outline-button" :disabled="metricsLoading" @click="loadMetrics">
|
||||
<span v-if="hasMetrics">数据更新时间:{{ metricsUpdatedAt ? dayjs(metricsUpdatedAt).format('YYYY-MM-DD HH:mm') : '加载中' }}</span>
|
||||
<button v-if="hasMetrics" type="button" class="dashboard-outline-button" :disabled="metricsLoading" @click="loadMetrics">
|
||||
<RefreshCw :size="16" aria-hidden="true" />
|
||||
刷新数据
|
||||
</button>
|
||||
@@ -95,7 +102,7 @@ onBeforeUnmount(() => document.removeEventListener('fullscreenchange', syncFulls
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="dashboard-metrics" aria-label="核心业务指标">
|
||||
<div v-if="hasMetrics" class="dashboard-metrics" aria-label="核心业务指标">
|
||||
<article v-for="metric in metrics" :key="metric.label" class="dashboard-metric" :class="`is-${metric.tone}`">
|
||||
<span class="dashboard-metric-mark" aria-hidden="true" />
|
||||
<p>{{ metric.label }}</p>
|
||||
|
||||
Reference in New Issue
Block a user