From ea62b609aff5ed0238bba85ab40726dea2652bab Mon Sep 17 00:00:00 2001 From: zheng020 Date: Tue, 2 Jun 2026 21:41:58 +0800 Subject: [PATCH] =?UTF-8?q?feat(dashboard):=20useDashboardData=20composabl?= =?UTF-8?q?e=EF=BC=887=E6=8E=A5=E5=8F=A3=E5=B9=B6=E5=8F=91+section?= =?UTF-8?q?=E7=BA=A7refresh=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/composables/useDashboardData.js | 120 +++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 frontend/composables/useDashboardData.js diff --git a/frontend/composables/useDashboardData.js b/frontend/composables/useDashboardData.js new file mode 100644 index 0000000..da91cfe --- /dev/null +++ b/frontend/composables/useDashboardData.js @@ -0,0 +1,120 @@ +import { ref, computed } from 'vue' +import { dashboardApi } from '@/utils/api' + +/** + * 数据看板聚合 composable + * - 7 个接口并发(Promise.allSettled,单失败不阻塞) + * - 按 section 维度暴露 loading/error/data + * - 30 分钟内 refresh 不重发请求(lastFetched 缓存) + * + * @param {object} options + * @param {number|null} options.starId 顶粉星城 ID + */ +export function useDashboardData({ starId = null } = {}) { + const loading = ref({ + overall: false, + today: false, + curve: false, + exhibition: false, + likeIncome: false, + topAssets: false, + levels: false, + upgrades: false, + }) + + const error = ref({ + today: null, + curve: null, + exhibition: null, + likeIncome: null, + topAssets: null, + levels: null, + upgrades: null, + }) + + const data = ref({ + today: null, + curve: null, + exhibition: null, + likeIncome: null, + topAssets: null, + levels: null, + upgrades: null, + }) + + const lastFetched = ref(0) + const STALE_MS = 30 * 60 * 1000 + + const isReady = computed(() => + Object.values(data.value).every((v) => v !== null && v !== undefined) + ) + + // 内部辅助:单 section 加载,data 解包在 boundary 处进行 + async function loadSection(section, fetcher) { + loading.value[section] = true + error.value[section] = null + try { + const result = await fetcher(starId) + data.value[section] = result?.data ?? result + } catch (e) { + error.value[section] = e?.message || '加载失败' + data.value[section] = null + } finally { + loading.value[section] = false + } + } + + // 全量加载 + async function loadAll(force = false) { + if (!force && Date.now() - lastFetched.value < STALE_MS) return + loading.value.overall = true + try { + await Promise.allSettled([ + loadSection('today', dashboardApi.getTodayOverview), + loadSection('curve', dashboardApi.get7DayIncomeCurve), + loadSection('exhibition', dashboardApi.getExhibitionSummary), + loadSection('likeIncome', dashboardApi.getLikeIncomeByLevel), + loadSection('topAssets', dashboardApi.getTopAssets), + loadSection('levels', dashboardApi.getLevelDistribution), + loadSection('upgrades', dashboardApi.getUpgradeProgress), + ]) + lastFetched.value = Date.now() + } finally { + loading.value.overall = false + } + } + + // 局部刷新:refresh('curve') 只刷一个;refresh() 全量 cache-aware;refresh(true) 全量强制 + async function refresh(section, force = false) { + if (section) { + const fetcherMap = { + today: dashboardApi.getTodayOverview, + curve: dashboardApi.get7DayIncomeCurve, + exhibition: dashboardApi.getExhibitionSummary, + likeIncome: dashboardApi.getLikeIncomeByLevel, + topAssets: dashboardApi.getTopAssets, + levels: dashboardApi.getLevelDistribution, + upgrades: dashboardApi.getUpgradeProgress, + } + if (!fetcherMap[section]) return + return loadSection(section, fetcherMap[section]) + } + return loadAll(force) + } + + function dispose() { + // ref 状态由 Vue 自动 GC;保留接口给未来清理(如取消 in-flight 请求) + } + + loadAll() + + return { + loading, + error, + data, + refresh, + isReady, + lastFetched, + dispose, + } +}