topfans/frontend/composables/useDashboardData.js
2026-06-09 00:37:42 +08:00

164 lines
4.9 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, IS_MOCK_API } from '@/utils/api'
import { getAssetCoverRealUrl } from '@/utils/assetImageHelper'
import * as mock from '@/utils/mock/dashboard'
// 仅当 USE_MOCK_API=true 时使用 mock 短路
// 原因:标准基座 + Vite HMR 在 App 端会让 api.js 内部的 async 包装 + signal 透传
// 导致 useDashboardData.loadSection 里的 await 永远不 resolve。
// 走 mock 时直接调本地工厂200-600ms 内必定 resolve。
// 后端就绪USE_MOCK_API=false时走真实 dashboardRequest。
const MOCK_FACTORIES = {
today: mock.mockTodayOverview,
curve: mock.mock7DayIncomeCurve,
exhibition: mock.mockExhibitionSummary,
likeIncome: mock.mockLikeIncomeByLevel,
topAssets: mock.mockTopAssets,
levels: mock.mockLevelDistribution,
upgrades: mock.mockUpgradeProgress,
}
// section 名 → fetcher 映射模块级单例loadAll 与 refresh 复用
const SECTION_FETCHERS = {
today: dashboardApi.getTodayOverview,
curve: dashboardApi.get7DayIncomeCurve,
exhibition: dashboardApi.getExhibitionSummary,
likeIncome: dashboardApi.getLikeIncomeByLevel,
topAssets: dashboardApi.getTopAssets,
levels: dashboardApi.getLevelDistribution,
upgrades: dashboardApi.getUpgradeProgress,
}
/**
* 数据看板聚合 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)
)
// 递归把对象/数组里所有 asset_thumb / thumb 字符串转换成真实可访问的 URL
// 覆盖topAssets.items[].asset_thumb, exhibition.top5[].asset_thumb,
// likeIncome.levels[].thumb, upgrades.upcoming[].asset_thumb, upgrades.recent[].asset_thumb
async function resolveThumbUrls(obj) {
if (!obj || typeof obj !== 'object') return
if (Array.isArray(obj)) {
await Promise.all(obj.map(resolveThumbUrls))
return
}
const tasks = []
for (const [k, v] of Object.entries(obj)) {
if ((k === 'asset_thumb' || k === 'thumb') && typeof v === 'string' && v) {
tasks.push(
getAssetCoverRealUrl(v).then((url) => {
obj[k] = url
}).catch(() => { /* helper 内部已 fallback吞错即可 */ })
)
} else if (v && typeof v === 'object') {
tasks.push(resolveThumbUrls(v))
}
}
await Promise.all(tasks)
}
// 内部辅助:单 section 加载
async function loadSection(section, fetcher) {
loading.value[section] = true
error.value[section] = null
try {
// 只有 USE_MOCK_API=true 时才走 mock 短路;否则调用真实 fetcher → dashboardRequest
const mockFactory = IS_MOCK_API ? MOCK_FACTORIES[section] : null
const result = mockFactory
? await mockFactory({ star_id: starId })
: await fetcher(starId)
const sectionData = result?.data ?? result
// 后端返回的 thumb 可能是 OSS 相对路径,需要批量转成可访问 URL
await resolveThumbUrls(sectionData)
data.value[section] = sectionData
} 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(
Object.entries(SECTION_FETCHERS).map(([section, fetcher]) =>
loadSection(section, fetcher)
)
)
lastFetched.value = Date.now()
} finally {
loading.value.overall = false
}
}
// 局部刷新refresh('curve') 只刷一个refresh() 全量 cache-awarerefresh(null, true) 全量强制
async function refresh(section, force = false) {
if (section) {
const fetcher = SECTION_FETCHERS[section]
if (!fetcher) return
return loadSection(section, fetcher)
}
return loadAll(force)
}
// 初次加载
loadAll()
return {
loading,
error,
data,
refresh,
isReady,
lastFetched,
}
}