feat(challenges): 添加自定义挑战功能和多语言支持
- 新增自定义挑战创建页面,支持设置挑战类型、时间范围、目标值等 - 实现挑战邀请码系统,支持通过邀请码加入自定义挑战 - 完善挑战详情页面的多语言翻译支持 - 优化用户认证状态检查逻辑,使用token作为主要判断依据 - 添加阿里字体文件支持,提升UI显示效果 - 改进确认弹窗组件,支持Liquid Glass效果和自定义内容 - 优化应用启动流程,直接读取onboarding状态而非预加载用户数据
This commit is contained in:
@@ -1,13 +1,21 @@
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import { appStoreReviewService } from '@/services/appStoreReview';
|
||||
import {
|
||||
type ChallengeDetailDto,
|
||||
type ChallengeListItemDto,
|
||||
type ChallengeProgressDto,
|
||||
ChallengeSource,
|
||||
ChallengeState,
|
||||
type ChallengeStatus,
|
||||
type CreateCustomChallengePayload,
|
||||
type RankingItemDto,
|
||||
createCustomChallenge,
|
||||
getChallengeByShareCode,
|
||||
getChallengeDetail,
|
||||
getChallengeRankings,
|
||||
joinChallenge as joinChallengeApi,
|
||||
joinChallengeByCode as joinChallengeByCodeApi,
|
||||
leaveChallenge as leaveChallengeApi,
|
||||
listChallenges,
|
||||
reportChallengeProgress as reportChallengeProgressApi,
|
||||
@@ -21,9 +29,9 @@ export type ChallengeProgress = ChallengeProgressDto;
|
||||
export type RankingItem = RankingItemDto;
|
||||
export type ChallengeSummary = ChallengeListItemDto;
|
||||
export type ChallengeDetail = ChallengeDetailDto;
|
||||
export type { ChallengeStatus };
|
||||
export type { ChallengeSource, ChallengeState, ChallengeStatus };
|
||||
export type ChallengeEntity = ChallengeSummary & {
|
||||
summary?: string;
|
||||
summary?: string | null;
|
||||
rankings?: RankingItem[];
|
||||
userRank?: number;
|
||||
};
|
||||
@@ -38,7 +46,7 @@ type ChallengeRankingList = {
|
||||
|
||||
type ChallengesState = {
|
||||
entities: Record<string, ChallengeEntity>;
|
||||
order: string[];
|
||||
orderedIds: string[];
|
||||
listStatus: AsyncStatus;
|
||||
listError?: string;
|
||||
detailStatus: Record<string, AsyncStatus>;
|
||||
@@ -53,11 +61,15 @@ type ChallengesState = {
|
||||
rankingStatus: Record<string, AsyncStatus>;
|
||||
rankingLoadMoreStatus: Record<string, AsyncStatus>;
|
||||
rankingError: Record<string, string | undefined>;
|
||||
createStatus: AsyncStatus;
|
||||
createError?: string;
|
||||
joinByCodeStatus: AsyncStatus;
|
||||
joinByCodeError?: string;
|
||||
};
|
||||
|
||||
const initialState: ChallengesState = {
|
||||
entities: {},
|
||||
order: [],
|
||||
orderedIds: [],
|
||||
listStatus: 'idle',
|
||||
listError: undefined,
|
||||
detailStatus: {},
|
||||
@@ -72,6 +84,10 @@ const initialState: ChallengesState = {
|
||||
rankingStatus: {},
|
||||
rankingLoadMoreStatus: {},
|
||||
rankingError: {},
|
||||
createStatus: 'idle',
|
||||
createError: undefined,
|
||||
joinByCodeStatus: 'idle',
|
||||
joinByCodeError: undefined,
|
||||
};
|
||||
|
||||
const toErrorMessage = (error: unknown): string => {
|
||||
@@ -168,10 +184,41 @@ export const fetchChallengeRankings = createAsyncThunk<
|
||||
}
|
||||
});
|
||||
|
||||
export const createCustomChallengeThunk = createAsyncThunk<
|
||||
ChallengeDetail,
|
||||
CreateCustomChallengePayload,
|
||||
{ rejectValue: string }
|
||||
>('challenges/createCustom', async (payload, { rejectWithValue }) => {
|
||||
try {
|
||||
return await createCustomChallenge(payload);
|
||||
} catch (error) {
|
||||
return rejectWithValue(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
|
||||
export const joinChallengeByCode = createAsyncThunk<
|
||||
{ challenge: ChallengeDetail; progress: ChallengeProgress },
|
||||
string,
|
||||
{ rejectValue: string }
|
||||
>('challenges/joinByCode', async (shareCode, { rejectWithValue }) => {
|
||||
try {
|
||||
const progress = await joinChallengeByCodeApi(shareCode);
|
||||
const challenge = await getChallengeByShareCode(shareCode);
|
||||
return { challenge: { ...challenge, progress }, progress };
|
||||
} catch (error) {
|
||||
return rejectWithValue(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
|
||||
const challengesSlice = createSlice({
|
||||
name: 'challenges',
|
||||
initialState,
|
||||
reducers: {},
|
||||
reducers: {
|
||||
resetJoinByCodeState: (state) => {
|
||||
state.joinByCodeStatus = 'idle';
|
||||
state.joinByCodeError = undefined;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
.addCase(fetchChallenges.pending, (state) => {
|
||||
@@ -181,18 +228,15 @@ const challengesSlice = createSlice({
|
||||
.addCase(fetchChallenges.fulfilled, (state, action) => {
|
||||
state.listStatus = 'succeeded';
|
||||
state.listError = undefined;
|
||||
const ids = new Set<string>();
|
||||
const incomingIds = new Set<string>();
|
||||
action.payload.forEach((challenge) => {
|
||||
ids.add(challenge.id);
|
||||
incomingIds.add(challenge.id);
|
||||
const source = challenge.source ?? ChallengeSource.SYSTEM;
|
||||
const existing = state.entities[challenge.id];
|
||||
if (existing) {
|
||||
Object.assign(existing, challenge);
|
||||
} else {
|
||||
state.entities[challenge.id] = { ...challenge };
|
||||
}
|
||||
state.entities[challenge.id] = { ...(existing ?? {}), ...challenge, source };
|
||||
});
|
||||
Object.keys(state.entities).forEach((id) => {
|
||||
if (!ids.has(id)) {
|
||||
if (!incomingIds.has(id) && !state.entities[id]?.isJoined) {
|
||||
delete state.entities[id];
|
||||
delete state.detailStatus[id];
|
||||
delete state.detailError[id];
|
||||
@@ -204,7 +248,7 @@ const challengesSlice = createSlice({
|
||||
delete state.progressError[id];
|
||||
}
|
||||
});
|
||||
state.order = action.payload.map((item) => item.id);
|
||||
state.orderedIds = action.payload.map((item) => item.id);
|
||||
})
|
||||
.addCase(fetchChallenges.rejected, (state, action) => {
|
||||
state.listStatus = 'failed';
|
||||
@@ -220,11 +264,8 @@ const challengesSlice = createSlice({
|
||||
state.detailStatus[detail.id] = 'succeeded';
|
||||
state.detailError[detail.id] = undefined;
|
||||
const existing = state.entities[detail.id];
|
||||
if (existing) {
|
||||
Object.assign(existing, detail);
|
||||
} else {
|
||||
state.entities[detail.id] = { ...detail };
|
||||
}
|
||||
const source = detail.source ?? existing?.source ?? ChallengeSource.SYSTEM;
|
||||
state.entities[detail.id] = { ...(existing ?? {}), ...detail, source };
|
||||
})
|
||||
.addCase(fetchChallengeDetail.rejected, (state, action) => {
|
||||
const id = action.meta.arg;
|
||||
@@ -333,9 +374,50 @@ const challengesSlice = createSlice({
|
||||
state.rankingError[id] = message;
|
||||
}
|
||||
});
|
||||
|
||||
builder
|
||||
.addCase(createCustomChallengeThunk.pending, (state) => {
|
||||
state.createStatus = 'loading';
|
||||
state.createError = undefined;
|
||||
})
|
||||
.addCase(createCustomChallengeThunk.fulfilled, (state, action) => {
|
||||
state.createStatus = 'succeeded';
|
||||
state.createError = undefined;
|
||||
const challenge = action.payload;
|
||||
const existing = state.entities[challenge.id];
|
||||
const source = ChallengeSource.CUSTOM;
|
||||
state.entities[challenge.id] = { ...(existing ?? {}), ...challenge, source };
|
||||
state.orderedIds = [challenge.id, ...state.orderedIds.filter((id) => id !== challenge.id)];
|
||||
})
|
||||
.addCase(createCustomChallengeThunk.rejected, (state, action) => {
|
||||
state.createStatus = 'failed';
|
||||
state.createError = action.payload ?? toErrorMessage(action.error);
|
||||
});
|
||||
|
||||
builder
|
||||
.addCase(joinChallengeByCode.pending, (state) => {
|
||||
state.joinByCodeStatus = 'loading';
|
||||
state.joinByCodeError = undefined;
|
||||
})
|
||||
.addCase(joinChallengeByCode.fulfilled, (state, action) => {
|
||||
state.joinByCodeStatus = 'succeeded';
|
||||
state.joinByCodeError = undefined;
|
||||
const { challenge, progress } = action.payload;
|
||||
const existing = state.entities[challenge.id];
|
||||
const source = challenge.source ?? existing?.source ?? ChallengeSource.SYSTEM;
|
||||
const merged = { ...(existing ?? {}), ...challenge, progress, isJoined: true, source };
|
||||
state.entities[challenge.id] = merged as ChallengeEntity;
|
||||
state.orderedIds = [challenge.id, ...state.orderedIds.filter((id) => id !== challenge.id)];
|
||||
})
|
||||
.addCase(joinChallengeByCode.rejected, (state, action) => {
|
||||
state.joinByCodeStatus = 'failed';
|
||||
state.joinByCodeError = action.payload ?? toErrorMessage(action.error);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const { resetJoinByCodeState } = challengesSlice.actions;
|
||||
|
||||
export default challengesSlice.reducer;
|
||||
|
||||
const selectChallengesState = (state: RootState) => state.challenges;
|
||||
@@ -355,20 +437,30 @@ export const selectChallengeEntities = createSelector(
|
||||
(state) => state.entities
|
||||
);
|
||||
|
||||
export const selectChallengeOrder = createSelector(
|
||||
const selectChallengeOrder = createSelector(
|
||||
[selectChallengesState],
|
||||
(state) => state.order
|
||||
(state) => state.orderedIds
|
||||
);
|
||||
|
||||
export const selectChallengeList = createSelector(
|
||||
[selectChallengeEntities, selectChallengeOrder],
|
||||
(entities, order) => order.map((id) => entities[id]).filter(Boolean) as ChallengeEntity[]
|
||||
(entities, orderedIds) => orderedIds.map((id) => entities[id]).filter(Boolean) as ChallengeEntity[]
|
||||
);
|
||||
|
||||
export const selectCustomChallengeList = createSelector(
|
||||
[selectChallengeList],
|
||||
(list) => list.filter((challenge) => challenge.source === ChallengeSource.CUSTOM)
|
||||
);
|
||||
|
||||
export const selectOfficialChallengeList = createSelector(
|
||||
[selectChallengeList],
|
||||
(list) => list.filter((challenge) => challenge.source !== ChallengeSource.CUSTOM)
|
||||
);
|
||||
|
||||
const formatNumberWithSeparator = (value: number): string =>
|
||||
value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||
|
||||
const formatMonthDay = (input: string | undefined): string | undefined => {
|
||||
const formatMonthDay = (input: string | number | undefined): string | undefined => {
|
||||
if (!input) return undefined;
|
||||
const date = new Date(input);
|
||||
if (Number.isNaN(date.getTime())) return undefined;
|
||||
@@ -384,6 +476,26 @@ const buildDateRangeLabel = (challenge: ChallengeEntity): string => {
|
||||
return challenge.periodLabel ?? challenge.durationLabel;
|
||||
};
|
||||
|
||||
const deriveStatus = (challenge: ChallengeEntity): ChallengeStatus => {
|
||||
if (challenge.status) return challenge.status;
|
||||
if (challenge.challengeState === ChallengeState.ARCHIVED) {
|
||||
return 'expired';
|
||||
}
|
||||
const now = dayjs();
|
||||
const start = challenge.startAt ? dayjs(challenge.startAt) : null;
|
||||
const end = challenge.endAt ? dayjs(challenge.endAt) : null;
|
||||
if (start?.isValid() && start.isAfter(now)) {
|
||||
return 'upcoming';
|
||||
}
|
||||
if (end?.isValid() && end.isBefore(now)) {
|
||||
return 'expired';
|
||||
}
|
||||
return 'ongoing';
|
||||
};
|
||||
|
||||
const FALLBACK_CHALLENGE_IMAGE =
|
||||
'https://images.unsplash.com/photo-1506126613408-eca07ce68773?auto=format&fit=crop&w=1000&q=80';
|
||||
|
||||
export type ChallengeCardViewModel = {
|
||||
id: string;
|
||||
title: string;
|
||||
@@ -392,7 +504,7 @@ export type ChallengeCardViewModel = {
|
||||
participantsLabel: string;
|
||||
status: ChallengeStatus;
|
||||
isJoined: boolean;
|
||||
endAt?: string;
|
||||
endAt?: string | number;
|
||||
periodLabel?: string;
|
||||
durationLabel: string;
|
||||
requirementLabel: string;
|
||||
@@ -401,27 +513,109 @@ export type ChallengeCardViewModel = {
|
||||
ctaLabel: string;
|
||||
progress?: ChallengeProgress;
|
||||
avatars: string[];
|
||||
source?: ChallengeSource;
|
||||
shareCode?: string | null;
|
||||
challengeState?: ChallengeState;
|
||||
progressUnit?: string;
|
||||
targetValue?: number;
|
||||
isCreator?: boolean;
|
||||
};
|
||||
|
||||
export const selectChallengeCards = createSelector([selectChallengeList], (challenges) =>
|
||||
challenges.map<ChallengeCardViewModel>((challenge) => ({
|
||||
id: challenge.id,
|
||||
title: challenge.title,
|
||||
image: challenge.image,
|
||||
dateRange: buildDateRangeLabel(challenge),
|
||||
participantsLabel: `${formatNumberWithSeparator(challenge.participantsCount)} 人参与`,
|
||||
status: challenge.status,
|
||||
isJoined: challenge.isJoined,
|
||||
endAt: challenge.endAt,
|
||||
periodLabel: challenge.periodLabel,
|
||||
durationLabel: challenge.durationLabel,
|
||||
requirementLabel: challenge.requirementLabel,
|
||||
highlightTitle: challenge.highlightTitle,
|
||||
highlightSubtitle: challenge.highlightSubtitle,
|
||||
ctaLabel: challenge.ctaLabel,
|
||||
progress: challenge.progress,
|
||||
avatars: [],
|
||||
}))
|
||||
challenges.map<ChallengeCardViewModel>((challenge) => {
|
||||
const participants =
|
||||
typeof challenge.participantsCount === 'number' ? challenge.participantsCount : 0;
|
||||
return {
|
||||
id: challenge.id,
|
||||
title: challenge.title,
|
||||
image: challenge.image ?? FALLBACK_CHALLENGE_IMAGE,
|
||||
dateRange: buildDateRangeLabel(challenge),
|
||||
participantsLabel: `${formatNumberWithSeparator(participants)} 人参与`,
|
||||
status: deriveStatus(challenge),
|
||||
isJoined: challenge.isJoined,
|
||||
endAt: challenge.endAt,
|
||||
periodLabel: challenge.periodLabel,
|
||||
durationLabel: challenge.durationLabel,
|
||||
requirementLabel: challenge.requirementLabel,
|
||||
highlightTitle: challenge.highlightTitle,
|
||||
highlightSubtitle: challenge.highlightSubtitle,
|
||||
ctaLabel: challenge.ctaLabel,
|
||||
progress: challenge.progress,
|
||||
avatars: [],
|
||||
source: challenge.source,
|
||||
shareCode: challenge.shareCode ?? null,
|
||||
challengeState: challenge.challengeState,
|
||||
progressUnit: challenge.progressUnit,
|
||||
targetValue: challenge.targetValue,
|
||||
isCreator: challenge.isCreator,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
export const selectCustomChallengeCards = createSelector(
|
||||
[selectCustomChallengeList],
|
||||
(challenges) =>
|
||||
challenges.map<ChallengeCardViewModel>((challenge) => {
|
||||
const participants =
|
||||
typeof challenge.participantsCount === 'number' ? challenge.participantsCount : 0;
|
||||
return {
|
||||
id: challenge.id,
|
||||
title: challenge.title,
|
||||
image: challenge.image ?? FALLBACK_CHALLENGE_IMAGE,
|
||||
dateRange: buildDateRangeLabel(challenge),
|
||||
participantsLabel: `${formatNumberWithSeparator(participants)} 人参与`,
|
||||
status: deriveStatus(challenge),
|
||||
isJoined: challenge.isJoined,
|
||||
endAt: challenge.endAt,
|
||||
periodLabel: challenge.periodLabel,
|
||||
durationLabel: challenge.durationLabel,
|
||||
requirementLabel: challenge.requirementLabel,
|
||||
highlightTitle: challenge.highlightTitle,
|
||||
highlightSubtitle: challenge.highlightSubtitle,
|
||||
ctaLabel: challenge.ctaLabel,
|
||||
progress: challenge.progress,
|
||||
avatars: [],
|
||||
source: challenge.source ?? ChallengeSource.CUSTOM,
|
||||
shareCode: challenge.shareCode ?? null,
|
||||
challengeState: challenge.challengeState,
|
||||
progressUnit: challenge.progressUnit,
|
||||
targetValue: challenge.targetValue,
|
||||
isCreator: challenge.isCreator,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
export const selectOfficialChallengeCards = createSelector(
|
||||
[selectOfficialChallengeList],
|
||||
(challenges) =>
|
||||
challenges.map<ChallengeCardViewModel>((challenge) => {
|
||||
const participants =
|
||||
typeof challenge.participantsCount === 'number' ? challenge.participantsCount : 0;
|
||||
return {
|
||||
id: challenge.id,
|
||||
title: challenge.title,
|
||||
image: challenge.image ?? FALLBACK_CHALLENGE_IMAGE,
|
||||
dateRange: buildDateRangeLabel(challenge),
|
||||
participantsLabel: `${formatNumberWithSeparator(participants)} 人参与`,
|
||||
status: deriveStatus(challenge),
|
||||
isJoined: challenge.isJoined,
|
||||
endAt: challenge.endAt,
|
||||
periodLabel: challenge.periodLabel,
|
||||
durationLabel: challenge.durationLabel,
|
||||
requirementLabel: challenge.requirementLabel,
|
||||
highlightTitle: challenge.highlightTitle,
|
||||
highlightSubtitle: challenge.highlightSubtitle,
|
||||
ctaLabel: challenge.ctaLabel,
|
||||
progress: challenge.progress,
|
||||
avatars: [],
|
||||
source: challenge.source ?? ChallengeSource.SYSTEM,
|
||||
shareCode: challenge.shareCode ?? null,
|
||||
challengeState: challenge.challengeState,
|
||||
progressUnit: challenge.progressUnit,
|
||||
targetValue: challenge.targetValue,
|
||||
isCreator: challenge.isCreator,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
export const selectChallengeById = (id: string) =>
|
||||
@@ -462,3 +656,23 @@ export const selectChallengeRankingLoadMoreStatus = (id: string) =>
|
||||
|
||||
export const selectChallengeRankingError = (id: string) =>
|
||||
createSelector([selectChallengesState], (state) => state.rankingError[id]);
|
||||
|
||||
export const selectCreateChallengeStatus = createSelector(
|
||||
[selectChallengesState],
|
||||
(state) => state.createStatus
|
||||
);
|
||||
|
||||
export const selectCreateChallengeError = createSelector(
|
||||
[selectChallengesState],
|
||||
(state) => state.createError
|
||||
);
|
||||
|
||||
export const selectJoinByCodeStatus = createSelector(
|
||||
[selectChallengesState],
|
||||
(state) => state.joinByCodeStatus
|
||||
);
|
||||
|
||||
export const selectJoinByCodeError = createSelector(
|
||||
[selectChallengesState],
|
||||
(state) => state.joinByCodeError
|
||||
);
|
||||
|
||||
@@ -5,65 +5,51 @@ import AsyncStorage from '@/utils/kvStore';
|
||||
import { createAsyncThunk, createSelector, createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
// 预加载的用户数据存储
|
||||
let preloadedUserData: {
|
||||
token: string | null;
|
||||
profile: UserProfile;
|
||||
privacyAgreed: boolean;
|
||||
onboardingCompleted: boolean;
|
||||
} | null = null;
|
||||
|
||||
// 预加载用户数据的函数
|
||||
export async function preloadUserData() {
|
||||
/**
|
||||
* 同步加载用户数据(在 Redux store 初始化时立即执行)
|
||||
* 使用 getItemSync 确保数据在 store 创建前就已加载
|
||||
*/
|
||||
function loadUserDataSync() {
|
||||
try {
|
||||
const [profileStr, privacyAgreedStr, token, onboardingCompletedStr] = await Promise.all([
|
||||
AsyncStorage.getItem(STORAGE_KEYS.userProfile),
|
||||
AsyncStorage.getItem(STORAGE_KEYS.privacyAgreed),
|
||||
AsyncStorage.getItem(STORAGE_KEYS.authToken),
|
||||
AsyncStorage.getItem(STORAGE_KEYS.onboardingCompleted),
|
||||
]);
|
||||
const profileStr = AsyncStorage.getItemSync(STORAGE_KEYS.userProfile);
|
||||
const token = AsyncStorage.getItemSync(STORAGE_KEYS.authToken);
|
||||
const onboardingCompletedStr = AsyncStorage.getItemSync(STORAGE_KEYS.onboardingCompleted);
|
||||
|
||||
let profile: UserProfile = {
|
||||
memberNumber: 0
|
||||
};
|
||||
|
||||
if (profileStr) {
|
||||
try {
|
||||
profile = JSON.parse(profileStr) as UserProfile;
|
||||
} catch {
|
||||
profile = {
|
||||
memberNumber: 0
|
||||
};
|
||||
profile = { memberNumber: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
const privacyAgreed = privacyAgreedStr === 'true';
|
||||
const onboardingCompleted = onboardingCompletedStr === 'true';
|
||||
|
||||
// 如果有 token,需要设置到 API 客户端
|
||||
// 如果有 token,需要异步设置到 API 客户端(但不阻塞初始化)
|
||||
if (token) {
|
||||
await setAuthToken(token);
|
||||
setAuthToken(token).catch(err => {
|
||||
console.error('设置 auth token 失败:', err);
|
||||
});
|
||||
}
|
||||
|
||||
preloadedUserData = { token, profile, privacyAgreed, onboardingCompleted };
|
||||
return preloadedUserData;
|
||||
return { token, profile, onboardingCompleted };
|
||||
} catch (error) {
|
||||
console.error('预加载用户数据失败:', error);
|
||||
preloadedUserData = {
|
||||
console.error('同步加载用户数据失败:', error);
|
||||
return {
|
||||
token: null,
|
||||
profile: {
|
||||
memberNumber: 0
|
||||
},
|
||||
privacyAgreed: false,
|
||||
profile: { memberNumber: 0 },
|
||||
onboardingCompleted: false
|
||||
};
|
||||
return preloadedUserData;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取预加载的用户数据
|
||||
function getPreloadedUserData() {
|
||||
return preloadedUserData || { token: null, profile: {}, privacyAgreed: false, onboardingCompleted: false };
|
||||
}
|
||||
// 在模块加载时立即同步加载用户数据
|
||||
const preloadedUserData = loadUserDataSync();
|
||||
|
||||
|
||||
export type Gender = 'male' | 'female' | '';
|
||||
|
||||
@@ -120,22 +106,23 @@ export type UserState = {
|
||||
export const DEFAULT_MEMBER_NAME = '朋友';
|
||||
|
||||
const getInitialState = (): UserState => {
|
||||
const preloaded = getPreloadedUserData();
|
||||
// 使用模块加载时同步加载的数据
|
||||
console.log('初始化 Redux state,使用预加载数据:', preloadedUserData);
|
||||
|
||||
return {
|
||||
token: preloaded.token,
|
||||
token: preloadedUserData.token,
|
||||
profile: {
|
||||
name: DEFAULT_MEMBER_NAME,
|
||||
isVip: false,
|
||||
freeUsageCount: 3,
|
||||
memberNumber: 0,
|
||||
maxUsageCount: 5,
|
||||
...preloaded.profile, // 合并预加载的用户资料
|
||||
...preloadedUserData.profile, // 合并预加载的用户资料(包含 memberNumber)
|
||||
},
|
||||
loading: false,
|
||||
error: null,
|
||||
weightHistory: [],
|
||||
activityHistory: [],
|
||||
onboardingCompleted: preloaded.onboardingCompleted, // 引导完成状态
|
||||
onboardingCompleted: preloadedUserData.onboardingCompleted, // 引导完成状态
|
||||
};
|
||||
};
|
||||
|
||||
@@ -198,8 +185,11 @@ export const login = createAsyncThunk(
|
||||
|
||||
if (!token) throw new Error('登录响应缺少 token');
|
||||
|
||||
// 先持久化到本地存储
|
||||
await AsyncStorage.setItem(STORAGE_KEYS.authToken, token);
|
||||
await AsyncStorage.setItem(STORAGE_KEYS.userProfile, JSON.stringify(profile ?? {}));
|
||||
|
||||
// 再设置到 API 客户端(内部会同步更新 AsyncStorage)
|
||||
await setAuthToken(token);
|
||||
|
||||
return { token, profile } as { token: string; profile: UserProfile };
|
||||
@@ -222,12 +212,15 @@ export const setOnboardingCompleted = createAsyncThunk('user/setOnboardingComple
|
||||
});
|
||||
|
||||
export const logout = createAsyncThunk('user/logout', async () => {
|
||||
// 先清除 API 客户端的 token(内部会清除 AsyncStorage)
|
||||
await setAuthToken(null);
|
||||
|
||||
// 再清除其他本地存储数据
|
||||
await Promise.all([
|
||||
AsyncStorage.removeItem(STORAGE_KEYS.authToken),
|
||||
AsyncStorage.removeItem(STORAGE_KEYS.userProfile),
|
||||
AsyncStorage.removeItem(STORAGE_KEYS.privacyAgreed),
|
||||
]);
|
||||
await setAuthToken(null);
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user