import type { BadgeDto, BadgeRarity } from '@/services/badges'; import { getAvailableBadges } from '@/services/badges'; import { createAsyncThunk, createSelector, createSlice } from '@reduxjs/toolkit'; import dayjs from 'dayjs'; import type { RootState } from './index'; export interface BadgesState { items: BadgeDto[]; loading: boolean; error: string | null; lastFetched: string | null; } const initialState: BadgesState = { items: [], loading: false, error: null, lastFetched: null, }; export const fetchAvailableBadges = createAsyncThunk( 'badges/fetchAvailable', async (_, { rejectWithValue }) => { try { return await getAvailableBadges(); } catch (error: any) { const message = error?.message ?? '获取勋章列表失败'; return rejectWithValue(message); } }, { condition: (_, { getState }) => { const state = getState() as RootState; const { loading, lastFetched } = state.badges ?? {}; // 如果正在加载,阻止重复请求 if (loading) { return false; } // 如果没有缓存数据,允许请求 if (!lastFetched) { return true; } // 如果缓存时间超过 5 分钟,允许请求 const CACHE_DURATION = 5 * 60 * 1000; // 5分钟 const now = Date.now(); const lastFetchedTime = new Date(lastFetched).getTime(); const isExpired = now - lastFetchedTime > CACHE_DURATION; return isExpired; }, } ); const badgesSlice = createSlice({ name: 'badges', initialState, reducers: {}, extraReducers: (builder) => { builder .addCase(fetchAvailableBadges.pending, (state) => { state.loading = true; state.error = null; }) .addCase(fetchAvailableBadges.fulfilled, (state, action) => { state.loading = false; state.items = action.payload; state.lastFetched = new Date().toISOString(); }) .addCase(fetchAvailableBadges.rejected, (state, action) => { state.loading = false; state.error = typeof action.payload === 'string' ? action.payload : action.error.message ?? '加载勋章失败'; }); }, }); export default badgesSlice.reducer; const selectBadgesSlice = (state: RootState) => state.badges; export const selectBadgesState = selectBadgesSlice; export const selectBadgesLoading = (state: RootState) => state.badges.loading; export const selectBadgesError = (state: RootState) => state.badges.error; export const selectBadges = (state: RootState) => state.badges.items; const rarityPriority: Record = { common: 0, uncommon: 1, rare: 2, epic: 3, legendary: 4, }; const compareBadges = (a: BadgeDto, b: BadgeDto) => { if (a.isAwarded !== b.isAwarded) { return a.isAwarded ? -1 : 1; } const sortOrderA = a.sortOrder ?? Number.MAX_SAFE_INTEGER; const sortOrderB = b.sortOrder ?? Number.MAX_SAFE_INTEGER; if (sortOrderA !== sortOrderB) { return sortOrderA - sortOrderB; } if (a.isAwarded && b.isAwarded) { const earnedDiff = (b.earnedAt ? dayjs(b.earnedAt).valueOf() : 0) - (a.earnedAt ? dayjs(a.earnedAt).valueOf() : 0); if (earnedDiff !== 0) { return earnedDiff; } } const rarityKeyA = a.rarity ?? 'common'; const rarityKeyB = b.rarity ?? 'common'; const rarityDiff = rarityPriority[rarityKeyB] - rarityPriority[rarityKeyA]; if (rarityDiff !== 0) { return rarityDiff; } return a.name.localeCompare(b.name); }; export const selectSortedBadges = createSelector([selectBadges], (items) => { return [...items].sort(compareBadges); }); export const selectBadgeCounts = createSelector([selectBadges], (items) => { const earned = items.filter((badge) => badge.isAwarded).length; return { earned, total: items.length, }; }); export const selectBadgePreview = createSelector([selectSortedBadges], (items) => items.slice(0, 3));