mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-07 18:07:47 +08:00
refactor(workspace): enforce layered crate boundaries
This commit is contained in:
@@ -7,30 +7,12 @@
|
||||
ref="statsPanelRef"
|
||||
class="flex-1 min-w-0 flex flex-col"
|
||||
>
|
||||
<div class="mb-4 flex items-center justify-between gap-3">
|
||||
<Badge
|
||||
:variant="authStore.isAdmin ? 'default' : 'secondary'"
|
||||
class="uppercase tracking-[0.45em]"
|
||||
>
|
||||
{{ dashboardModeLabel }}
|
||||
</Badge>
|
||||
<Button
|
||||
type="button"
|
||||
:variant="autoRefreshEnabled ? 'default' : 'outline'"
|
||||
class="h-8 gap-1.5 px-3 text-xs"
|
||||
:aria-pressed="autoRefreshEnabled"
|
||||
:title="autoRefreshButtonTitle"
|
||||
@click="toggleDashboardAutoRefresh"
|
||||
>
|
||||
<RefreshCw
|
||||
class="h-3.5 w-3.5"
|
||||
:class="{
|
||||
'animate-spin': autoRefreshEnabled && autoRefreshLoading,
|
||||
}"
|
||||
/>
|
||||
<span>{{ autoRefreshButtonLabel }}</span>
|
||||
</Button>
|
||||
</div>
|
||||
<Badge
|
||||
:variant="authStore.isAdmin ? 'default' : 'secondary'"
|
||||
class="mb-4 self-start uppercase tracking-[0.45em]"
|
||||
>
|
||||
{{ dashboardModeLabel }}
|
||||
</Badge>
|
||||
|
||||
<!-- 主要统计卡片 -->
|
||||
<div class="grid grid-cols-2 gap-3 sm:gap-4 xl:grid-cols-4">
|
||||
@@ -956,7 +938,6 @@ import {
|
||||
Clock,
|
||||
Database,
|
||||
Shuffle,
|
||||
RefreshCw,
|
||||
} from "lucide-vue-next";
|
||||
import { formatTokens, formatCurrency } from "@/utils/format";
|
||||
import { parseDateLike } from "@/utils/date";
|
||||
@@ -971,16 +952,10 @@ import type {
|
||||
|
||||
const authStore = useAuthStore();
|
||||
|
||||
const DASHBOARD_AUTO_REFRESH_INTERVAL_MS = 1_000;
|
||||
|
||||
type DashboardStatCard = Omit<DashboardStat, "icon"> & {
|
||||
icon: Component;
|
||||
};
|
||||
|
||||
type DashboardLoadOptions = {
|
||||
silent?: boolean;
|
||||
};
|
||||
|
||||
const statsPanelRef = ref<HTMLElement | null>(null);
|
||||
const announcementsHeight = ref<number | null>(null);
|
||||
const announcementsTimelineRef = ref<HTMLElement | null>(null);
|
||||
@@ -1005,8 +980,6 @@ function checkScreenSize() {
|
||||
|
||||
let statsPanelObserver: ResizeObserver | null = null;
|
||||
let announcementsTimelineObserver: ResizeObserver | null = null;
|
||||
let dashboardAutoRefreshTimer: ReturnType<typeof setInterval> | null = null;
|
||||
let dashboardRefreshPromise: Promise<void> | null = null;
|
||||
|
||||
function updateAnnouncementsHeight() {
|
||||
if (typeof window === "undefined") return;
|
||||
@@ -1107,14 +1080,6 @@ const getStatIconColor = (_index: number): string => {
|
||||
|
||||
// 统计数据
|
||||
const stats = ref<DashboardStatCard[]>([]);
|
||||
const autoRefreshEnabled = ref(false);
|
||||
const autoRefreshLoading = ref(false);
|
||||
const autoRefreshButtonLabel = computed(() =>
|
||||
autoRefreshEnabled.value ? "自动刷新中" : "自动刷新",
|
||||
);
|
||||
const autoRefreshButtonTitle = computed(() =>
|
||||
autoRefreshEnabled.value ? "点击关闭自动刷新" : "点击开启自动刷新",
|
||||
);
|
||||
const todayStats = ref<{
|
||||
requests: number;
|
||||
tokens: number;
|
||||
@@ -1499,55 +1464,17 @@ const dailyUsageTrendChartOptions = computed<ChartOptions<"line">>(() => {
|
||||
};
|
||||
});
|
||||
|
||||
async function refreshDashboardContent(options: DashboardLoadOptions = {}) {
|
||||
if (dashboardRefreshPromise) {
|
||||
return dashboardRefreshPromise;
|
||||
}
|
||||
autoRefreshLoading.value = true;
|
||||
dashboardRefreshPromise = (async () => {
|
||||
await Promise.all([loadDashboardData(options), loadDailyStats(options)]);
|
||||
await nextTick();
|
||||
updateAnnouncementsHeight();
|
||||
updateTimelineLine();
|
||||
})().finally(() => {
|
||||
autoRefreshLoading.value = false;
|
||||
dashboardRefreshPromise = null;
|
||||
});
|
||||
return dashboardRefreshPromise;
|
||||
}
|
||||
|
||||
function startDashboardAutoRefresh() {
|
||||
if (typeof window === "undefined") return;
|
||||
stopDashboardAutoRefresh();
|
||||
dashboardAutoRefreshTimer = window.setInterval(() => {
|
||||
void refreshDashboardContent({ silent: true });
|
||||
}, DASHBOARD_AUTO_REFRESH_INTERVAL_MS);
|
||||
}
|
||||
|
||||
function stopDashboardAutoRefresh() {
|
||||
if (dashboardAutoRefreshTimer !== null) {
|
||||
clearInterval(dashboardAutoRefreshTimer);
|
||||
dashboardAutoRefreshTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function toggleDashboardAutoRefresh() {
|
||||
autoRefreshEnabled.value = !autoRefreshEnabled.value;
|
||||
if (autoRefreshEnabled.value) {
|
||||
startDashboardAutoRefresh();
|
||||
void refreshDashboardContent({ silent: true });
|
||||
} else {
|
||||
stopDashboardAutoRefresh();
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
checkScreenSize();
|
||||
setupResizeObserver();
|
||||
if (typeof window !== "undefined") {
|
||||
window.addEventListener("resize", handleWindowResize);
|
||||
}
|
||||
await Promise.all([refreshDashboardContent(), loadAnnouncements()]);
|
||||
await Promise.all([
|
||||
loadDashboardData(),
|
||||
loadDailyStats(),
|
||||
loadAnnouncements(),
|
||||
]);
|
||||
await nextTick();
|
||||
setupTimelineResizeObserver();
|
||||
updateAnnouncementsHeight();
|
||||
@@ -1555,7 +1482,6 @@ onMounted(async () => {
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopDashboardAutoRefresh();
|
||||
if (typeof window !== "undefined") {
|
||||
window.removeEventListener("resize", handleWindowResize);
|
||||
}
|
||||
@@ -1575,10 +1501,8 @@ onBeforeUnmount(() => {
|
||||
dailyStatsRequestId += 1;
|
||||
});
|
||||
|
||||
async function loadDashboardData(options: DashboardLoadOptions = {}) {
|
||||
if (!options.silent) {
|
||||
loading.value = true;
|
||||
}
|
||||
async function loadDashboardData() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const statsData = await dashboardApi.getStats({
|
||||
timezone: dailyTimeRange.value.timezone,
|
||||
@@ -1604,21 +1528,17 @@ async function loadDashboardData(options: DashboardLoadOptions = {}) {
|
||||
userMonthlyCost.value = statsData.monthly_cost;
|
||||
}
|
||||
} finally {
|
||||
if (!options.silent) {
|
||||
loading.value = false;
|
||||
}
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDailyStats(options: DashboardLoadOptions = {}) {
|
||||
async function loadDailyStats() {
|
||||
if (dailyStatsLoadPromise) {
|
||||
hasPendingDailyStatsLoad = true;
|
||||
return dailyStatsLoadPromise;
|
||||
}
|
||||
const requestId = ++dailyStatsRequestId;
|
||||
if (!options.silent) {
|
||||
loadingDaily.value = true;
|
||||
}
|
||||
loadingDaily.value = true;
|
||||
dailyStatsLoadPromise = (async () => {
|
||||
try {
|
||||
const response = await dashboardApi.getDailyStats(dailyTimeRange.value);
|
||||
@@ -1630,7 +1550,7 @@ async function loadDailyStats(options: DashboardLoadOptions = {}) {
|
||||
dailyStats.value = [];
|
||||
providerSummary.value = [];
|
||||
} finally {
|
||||
if (requestId === dailyStatsRequestId && !options.silent) {
|
||||
if (requestId === dailyStatsRequestId) {
|
||||
loadingDaily.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -817,6 +817,7 @@ onMounted(async () => {
|
||||
const heatmapPromise = loadHeatmapData().catch(err => {
|
||||
log.error('加载热力图数据失败:', err)
|
||||
})
|
||||
const adminUsersPromise = loadAdminUsers()
|
||||
|
||||
await loadRecords(
|
||||
{ page: currentPage.value, pageSize: pageSize.value },
|
||||
@@ -825,8 +826,7 @@ onMounted(async () => {
|
||||
)
|
||||
void (async () => {
|
||||
await refreshAdminAnalytics({ force: true, preserveOnFailure: false })
|
||||
await heatmapPromise
|
||||
await loadAdminUsers()
|
||||
await Promise.all([heatmapPromise, adminUsersPromise])
|
||||
})()
|
||||
} else {
|
||||
// 用户页面:loadStats 已包含记录加载,不需要单独调用 loadRecords
|
||||
|
||||
@@ -101,7 +101,6 @@ vi.mock('lucide-vue-next', async () => {
|
||||
Clock: Icon,
|
||||
Database: Icon,
|
||||
Shuffle: Icon,
|
||||
RefreshCw: Icon,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -168,3 +167,27 @@ describe('Dashboard ordinary user wallet card', () => {
|
||||
expect(root.textContent).toContain('套餐额度 $100.00 · 钱包余额 $10.00')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Dashboard refresh controls', () => {
|
||||
it('does not render or run automatic refresh', async () => {
|
||||
vi.useFakeTimers()
|
||||
dashboardApiMocks.getStats.mockResolvedValue({ stats: [] })
|
||||
|
||||
try {
|
||||
const root = mountDashboard()
|
||||
await settle()
|
||||
|
||||
expect(root.textContent).not.toContain('自动刷新')
|
||||
expect(dashboardApiMocks.getStats).toHaveBeenCalledTimes(1)
|
||||
expect(dashboardApiMocks.getDailyStats).toHaveBeenCalledTimes(1)
|
||||
|
||||
await vi.advanceTimersByTimeAsync(60_000)
|
||||
await settle()
|
||||
|
||||
expect(dashboardApiMocks.getStats).toHaveBeenCalledTimes(1)
|
||||
expect(dashboardApiMocks.getDailyStats).toHaveBeenCalledTimes(1)
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
const source = readFileSync(
|
||||
resolve(process.cwd(), 'src/views/shared/Usage.vue'),
|
||||
'utf8',
|
||||
)
|
||||
|
||||
describe('admin usage initial loading', () => {
|
||||
it('starts the user filter request before analytics completes', () => {
|
||||
const mountedBlock = source
|
||||
.split('onMounted(async () => {')[1]
|
||||
?.split('// 处理时间范围变化')[0]
|
||||
|
||||
expect(mountedBlock).toBeTruthy()
|
||||
expect(mountedBlock).toContain('const adminUsersPromise = loadAdminUsers()')
|
||||
expect(mountedBlock).toContain('Promise.all([heatmapPromise, adminUsersPromise])')
|
||||
expect(mountedBlock?.indexOf('const adminUsersPromise = loadAdminUsers()'))
|
||||
.toBeLessThan(mountedBlock?.indexOf('await loadRecords(') ?? -1)
|
||||
expect(mountedBlock).not.toContain('await loadAdminUsers()')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user