feat(challenges): 实现自定义挑战的编辑与删除功能并完善多语言支持

- 新增自定义挑战的编辑模式,支持修改挑战信息
- 在详情页为创建者添加删除(归档)挑战的功能入口
- 全面完善挑战创建页面的国际化(i18n)文案适配
- 优化个人中心页面的字体样式,统一使用 AliBold/Regular
- 更新 Store 逻辑以处理挑战更新、删除及列表数据映射调整
This commit is contained in:
richarjiang
2025-11-26 19:07:19 +08:00
parent 39671ed70f
commit 518282ecb8
6 changed files with 866 additions and 160 deletions

View File

@@ -10,6 +10,8 @@ import {
type ChallengeStatus,
type CreateCustomChallengePayload,
type RankingItemDto,
type UpdateCustomChallengePayload,
archiveCustomChallenge,
createCustomChallenge,
getChallengeByShareCode,
getChallengeDetail,
@@ -19,6 +21,7 @@ import {
leaveChallenge as leaveChallengeApi,
listChallenges,
reportChallengeProgress as reportChallengeProgressApi,
updateCustomChallenge,
} from '@/services/challengesApi';
import { createAsyncThunk, createSelector, createSlice } from '@reduxjs/toolkit';
import type { RootState } from './index';
@@ -63,6 +66,10 @@ type ChallengesState = {
rankingError: Record<string, string | undefined>;
createStatus: AsyncStatus;
createError?: string;
updateStatus: AsyncStatus;
updateError?: string;
archiveStatus: Record<string, AsyncStatus>;
archiveError: Record<string, string | undefined>;
joinByCodeStatus: AsyncStatus;
joinByCodeError?: string;
};
@@ -86,6 +93,10 @@ const initialState: ChallengesState = {
rankingError: {},
createStatus: 'idle',
createError: undefined,
updateStatus: 'idle',
updateError: undefined,
archiveStatus: {},
archiveError: {},
joinByCodeStatus: 'idle',
joinByCodeError: undefined,
};
@@ -210,6 +221,31 @@ export const joinChallengeByCode = createAsyncThunk<
}
});
export const updateCustomChallengeThunk = createAsyncThunk<
ChallengeDetail,
{ id: string; payload: UpdateCustomChallengePayload },
{ rejectValue: string }
>('challenges/updateCustom', async ({ id, payload }, { rejectWithValue }) => {
try {
return await updateCustomChallenge(id, payload);
} catch (error) {
return rejectWithValue(toErrorMessage(error));
}
});
export const archiveCustomChallengeThunk = createAsyncThunk<
{ id: string },
string,
{ rejectValue: string }
>('challenges/archiveCustom', async (id, { rejectWithValue }) => {
try {
await archiveCustomChallenge(id);
return { id };
} catch (error) {
return rejectWithValue(toErrorMessage(error));
}
});
const challengesSlice = createSlice({
name: 'challenges',
initialState,
@@ -394,6 +430,55 @@ const challengesSlice = createSlice({
state.createError = action.payload ?? toErrorMessage(action.error);
});
builder
.addCase(updateCustomChallengeThunk.pending, (state) => {
state.updateStatus = 'loading';
state.updateError = undefined;
})
.addCase(updateCustomChallengeThunk.fulfilled, (state, action) => {
state.updateStatus = 'succeeded';
state.updateError = undefined;
const challenge = action.payload;
const existing = state.entities[challenge.id];
const source = ChallengeSource.CUSTOM;
state.entities[challenge.id] = { ...(existing ?? {}), ...challenge, source };
})
.addCase(updateCustomChallengeThunk.rejected, (state, action) => {
state.updateStatus = 'failed';
state.updateError = action.payload ?? toErrorMessage(action.error);
});
builder
.addCase(archiveCustomChallengeThunk.pending, (state, action) => {
const id = action.meta.arg;
state.archiveStatus[id] = 'loading';
state.archiveError[id] = undefined;
})
.addCase(archiveCustomChallengeThunk.fulfilled, (state, action) => {
const { id } = action.payload;
state.archiveStatus[id] = 'succeeded';
state.archiveError[id] = undefined;
delete state.entities[id];
state.orderedIds = state.orderedIds.filter((itemId) => itemId !== id);
delete state.detailStatus[id];
delete state.detailError[id];
delete state.joinStatus[id];
delete state.joinError[id];
delete state.leaveStatus[id];
delete state.leaveError[id];
delete state.progressStatus[id];
delete state.progressError[id];
delete state.rankingList[id];
delete state.rankingStatus[id];
delete state.rankingLoadMoreStatus[id];
delete state.rankingError[id];
})
.addCase(archiveCustomChallengeThunk.rejected, (state, action) => {
const id = action.meta.arg;
state.archiveStatus[id] = 'failed';
state.archiveError[id] = action.payload ?? toErrorMessage(action.error);
});
builder
.addCase(joinChallengeByCode.pending, (state) => {
state.joinByCodeStatus = 'loading';
@@ -545,8 +630,8 @@ export const selectChallengeCards = createSelector([selectChallengeList], (chall
source: challenge.source,
shareCode: challenge.shareCode ?? null,
challengeState: challenge.challengeState,
progressUnit: challenge.progressUnit,
targetValue: challenge.targetValue,
progressUnit: challenge.unit,
targetValue: challenge.progress?.target,
isCreator: challenge.isCreator,
};
})
@@ -578,8 +663,8 @@ export const selectCustomChallengeCards = createSelector(
source: challenge.source ?? ChallengeSource.CUSTOM,
shareCode: challenge.shareCode ?? null,
challengeState: challenge.challengeState,
progressUnit: challenge.progressUnit,
targetValue: challenge.targetValue,
progressUnit: challenge.unit,
targetValue: challenge.progress?.target,
isCreator: challenge.isCreator,
};
})
@@ -611,8 +696,8 @@ export const selectOfficialChallengeCards = createSelector(
source: challenge.source ?? ChallengeSource.SYSTEM,
shareCode: challenge.shareCode ?? null,
challengeState: challenge.challengeState,
progressUnit: challenge.progressUnit,
targetValue: challenge.targetValue,
progressUnit: challenge.unit,
targetValue: challenge.progress?.target,
isCreator: challenge.isCreator,
};
})
@@ -676,3 +761,19 @@ export const selectJoinByCodeError = createSelector(
[selectChallengesState],
(state) => state.joinByCodeError
);
export const selectUpdateChallengeStatus = createSelector(
[selectChallengesState],
(state) => state.updateStatus
);
export const selectUpdateChallengeError = createSelector(
[selectChallengesState],
(state) => state.updateError
);
export const selectArchiveStatus = (id: string) =>
createSelector([selectChallengesState], (state) => state.archiveStatus[id] ?? 'idle');
export const selectArchiveError = (id: string) =>
createSelector([selectChallengesState], (state) => state.archiveError[id]);