Files
digital-pilates/store/badgesSlice.ts
richarjiang 21e57634e0 feat(hrv): 添加心率变异性监控和压力评估功能
- 新增 HRV 监听服务,实时监控心率变异性数据
- 实现 HRV 到压力指数的转换算法和压力等级评估
- 添加智能通知服务,在压力偏高时推送健康建议
- 优化日志系统,修复日志丢失问题并增强刷新机制
- 改进个人页面下拉刷新,支持并行数据加载
- 优化勋章数据缓存策略,减少不必要的网络请求
- 重构应用初始化流程,优化权限服务和健康监听服务的启动顺序
- 移除冗余日志输出,提升应用性能
2025-11-18 14:08:20 +08:00

138 lines
3.9 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 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<BadgeDto[], void, { rejectValue: string }>(
'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<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));