- 在多个页面添加 isLoggedIn 检查,防止未登录时进行不必要的数据获取 - 使用 React.memo 和 useMemo 优化个人页面徽章渲染性能 - 为 badges API 添加节流机制,避免频繁请求 - 优化图片缓存策略和字符串处理 - 移除调试日志并改进推送通知的认证检查
148 lines
4.2 KiB
TypeScript
148 lines
4.2 KiB
TypeScript
import type { BadgeDto, BadgeRarity } from '@/services/badges';
|
|
import { getAvailableBadges } from '@/services/badges';
|
|
import { createAsyncThunk, createSelector, createSlice } from '@reduxjs/toolkit';
|
|
import dayjs from 'dayjs';
|
|
import { throttle } from 'lodash';
|
|
|
|
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,
|
|
};
|
|
|
|
// 创建节流版本的 fetchAvailableBadges 内部函数
|
|
const throttledFetchAvailableBadges = throttle(
|
|
async (): Promise<BadgeDto[]> => {
|
|
return await getAvailableBadges();
|
|
},
|
|
2000, // 2秒节流
|
|
{ leading: true, trailing: false }
|
|
);
|
|
|
|
export const fetchAvailableBadges = createAsyncThunk<BadgeDto[], void, { rejectValue: string }>(
|
|
'badges/fetchAvailable',
|
|
async (_, { rejectWithValue }) => {
|
|
try {
|
|
return await throttledFetchAvailableBadges();
|
|
} 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<BadgeRarity, number> = {
|
|
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));
|