164 lines
4.9 KiB
JavaScript
164 lines
4.9 KiB
JavaScript
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-aware;refresh(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,
|
||
}
|
||
}
|