topfans/frontend/composables/useDashboardData.js

121 lines
3.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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-awarerefresh(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,
}
}