refactor(workspace): enforce layered crate boundaries

This commit is contained in:
elky
2026-07-15 23:47:19 +08:00
parent a728c090a9
commit 8616fe6ee2
969 changed files with 40187 additions and 27240 deletions
+17 -97
View File
@@ -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;
}
}
+2 -2
View File
@@ -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()')
})
})