feat: 完善目标管理功能及相关组件

- 新增创建目标弹窗,支持用户输入目标信息并提交
- 实现目标数据的转换,支持将目标转换为待办事项和时间轴事件
- 优化目标页面,集成Redux状态管理,处理目标的创建、完成和错误提示
- 更新时间轴组件,支持动态显示目标安排
- 编写目标管理功能实现文档,详细描述功能和组件架构
This commit is contained in:
richarjiang
2025-08-22 12:05:27 +08:00
parent 136c800084
commit 231620d778
11 changed files with 1811 additions and 169 deletions

608
store/goalsSlice.ts Normal file
View File

@@ -0,0 +1,608 @@
import { goalsApi } from '@/services/goalsApi';
import {
BatchGoalOperationRequest,
CompleteGoalRequest,
CreateGoalRequest,
GetGoalCompletionsQuery,
GetGoalsQuery,
GoalCompletion,
GoalDetailResponse,
GoalListItem,
GoalStats,
GoalStatus,
UpdateGoalRequest
} from '@/types/goals';
import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit';
// 目标管理状态类型
export interface GoalsState {
// 目标列表
goals: GoalListItem[];
goalsLoading: boolean;
goalsError: string | null;
goalsPagination: {
page: number;
pageSize: number;
total: number;
hasMore: boolean;
};
// 当前查看的目标详情
currentGoal: GoalDetailResponse | null;
currentGoalLoading: boolean;
currentGoalError: string | null;
// 目标完成记录
completions: GoalCompletion[];
completionsLoading: boolean;
completionsError: string | null;
completionsPagination: {
page: number;
pageSize: number;
total: number;
hasMore: boolean;
};
// 目标统计
stats: GoalStats | null;
statsLoading: boolean;
statsError: string | null;
// 创建/更新目标
createLoading: boolean;
createError: string | null;
updateLoading: boolean;
updateError: string | null;
// 批量操作
batchLoading: boolean;
batchError: string | null;
// 筛选和搜索
filters: GetGoalsQuery;
}
const initialState: GoalsState = {
goals: [],
goalsLoading: false,
goalsError: null,
goalsPagination: {
page: 1,
pageSize: 20,
total: 0,
hasMore: false,
},
currentGoal: null,
currentGoalLoading: false,
currentGoalError: null,
completions: [],
completionsLoading: false,
completionsError: null,
completionsPagination: {
page: 1,
pageSize: 20,
total: 0,
hasMore: false,
},
stats: null,
statsLoading: false,
statsError: null,
createLoading: false,
createError: null,
updateLoading: false,
updateError: null,
batchLoading: false,
batchError: null,
filters: {
page: 1,
pageSize: 20,
sortBy: 'createdAt',
sortOrder: 'desc',
},
};
// 异步操作
/**
* 获取目标列表
*/
export const fetchGoals = createAsyncThunk(
'goals/fetchGoals',
async (query: GetGoalsQuery = {}, { rejectWithValue }) => {
try {
const response = await goalsApi.getGoals(query);
console.log('fetchGoals response', response);
return { query, response };
} catch (error: any) {
return rejectWithValue(error.message || '获取目标列表失败');
}
}
);
/**
* 加载更多目标
*/
export const loadMoreGoals = createAsyncThunk(
'goals/loadMoreGoals',
async (_, { getState, rejectWithValue }) => {
try {
const state = getState() as { goals: GoalsState };
const { filters, goalsPagination } = state.goals;
if (!goalsPagination.hasMore) {
return { goals: [], pagination: goalsPagination };
}
const query = {
...filters,
page: goalsPagination.page + 1,
};
const response = await goalsApi.getGoals(query);
console.log('response', response);
return { query, response };
} catch (error: any) {
return rejectWithValue(error.message || '加载更多目标失败');
}
}
);
/**
* 获取目标详情
*/
export const fetchGoalDetail = createAsyncThunk(
'goals/fetchGoalDetail',
async (goalId: string, { rejectWithValue }) => {
try {
const response = await goalsApi.getGoalById(goalId);
return response.data;
} catch (error: any) {
return rejectWithValue(error.message || '获取目标详情失败');
}
}
);
/**
* 创建目标
*/
export const createGoal = createAsyncThunk(
'goals/createGoal',
async (goalData: CreateGoalRequest, { rejectWithValue }) => {
try {
const response = await goalsApi.createGoal(goalData);
console.log('createGoal response', response);
return response;
} catch (error: any) {
return rejectWithValue(error.message || '创建目标失败');
}
}
);
/**
* 更新目标
*/
export const updateGoal = createAsyncThunk(
'goals/updateGoal',
async ({ goalId, goalData }: { goalId: string; goalData: UpdateGoalRequest }, { rejectWithValue }) => {
try {
const response = await goalsApi.updateGoal(goalId, goalData);
return response.data;
} catch (error: any) {
return rejectWithValue(error.message || '更新目标失败');
}
}
);
/**
* 删除目标
*/
export const deleteGoal = createAsyncThunk(
'goals/deleteGoal',
async (goalId: string, { rejectWithValue }) => {
try {
await goalsApi.deleteGoal(goalId);
return goalId;
} catch (error: any) {
return rejectWithValue(error.message || '删除目标失败');
}
}
);
/**
* 记录目标完成
*/
export const completeGoal = createAsyncThunk(
'goals/completeGoal',
async ({ goalId, completionData }: { goalId: string; completionData?: CompleteGoalRequest }, { rejectWithValue }) => {
try {
const response = await goalsApi.completeGoal(goalId, completionData);
return { goalId, completion: response.data };
} catch (error: any) {
return rejectWithValue(error.message || '记录目标完成失败');
}
}
);
/**
* 获取目标完成记录
*/
export const fetchGoalCompletions = createAsyncThunk(
'goals/fetchGoalCompletions',
async ({ goalId, query }: { goalId: string; query?: GetGoalCompletionsQuery }, { rejectWithValue }) => {
try {
const response = await goalsApi.getGoalCompletions(goalId, query);
return { query, response: response.data };
} catch (error: any) {
return rejectWithValue(error.message || '获取完成记录失败');
}
}
);
/**
* 获取目标统计
*/
export const fetchGoalStats = createAsyncThunk(
'goals/fetchGoalStats',
async (_, { rejectWithValue }) => {
try {
const response = await goalsApi.getGoalStats();
return response.data;
} catch (error: any) {
return rejectWithValue(error.message || '获取目标统计失败');
}
}
);
/**
* 批量操作目标
*/
export const batchOperateGoals = createAsyncThunk(
'goals/batchOperateGoals',
async (operationData: BatchGoalOperationRequest, { rejectWithValue }) => {
try {
const response = await goalsApi.batchOperateGoals(operationData);
return { operation: operationData, results: response.data };
} catch (error: any) {
return rejectWithValue(error.message || '批量操作失败');
}
}
);
// Redux Slice
const goalsSlice = createSlice({
name: 'goals',
initialState,
reducers: {
// 设置筛选条件
setFilters: (state, action: PayloadAction<Partial<GetGoalsQuery>>) => {
state.filters = { ...state.filters, ...action.payload };
},
// 重置筛选条件
resetFilters: (state) => {
state.filters = {
page: 1,
pageSize: 20,
sortBy: 'createdAt',
sortOrder: 'desc',
};
},
// 清除错误
clearErrors: (state) => {
state.goalsError = null;
state.currentGoalError = null;
state.completionsError = null;
state.statsError = null;
state.createError = null;
state.updateError = null;
state.batchError = null;
},
// 清除当前目标详情
clearCurrentGoal: (state) => {
state.currentGoal = null;
state.currentGoalError = null;
},
// 本地更新目标状态(用于乐观更新)
updateGoalStatus: (state, action: PayloadAction<{ goalId: string; status: GoalStatus }>) => {
const { goalId, status } = action.payload;
// 更新目标列表中的状态
const goalIndex = state.goals.findIndex(goal => goal.id === goalId);
if (goalIndex !== -1) {
state.goals[goalIndex].status = status;
}
// 更新当前目标详情中的状态
if (state.currentGoal && state.currentGoal.id === goalId) {
state.currentGoal.status = status;
}
},
// 本地增加完成次数(用于乐观更新)
incrementGoalCompletion: (state, action: PayloadAction<{ goalId: string; count?: number }>) => {
const { goalId, count = 1 } = action.payload;
// 更新目标列表中的完成次数
const goalIndex = state.goals.findIndex(goal => goal.id === goalId);
if (goalIndex !== -1) {
state.goals[goalIndex].completedCount += count;
// 重新计算进度百分比
if (state.goals[goalIndex].targetCount && state.goals[goalIndex].targetCount > 0) {
state.goals[goalIndex].progressPercentage = Math.round(
(state.goals[goalIndex].completedCount / state.goals[goalIndex].targetCount) * 100
);
}
}
// 更新当前目标详情中的完成次数
if (state.currentGoal && state.currentGoal.id === goalId) {
state.currentGoal.completedCount += count;
if (state.currentGoal.targetCount && state.currentGoal.targetCount > 0) {
state.currentGoal.progressPercentage = Math.round(
(state.currentGoal.completedCount / state.currentGoal.targetCount) * 100
);
}
}
},
},
extraReducers: (builder) => {
builder
// 获取目标列表
.addCase(fetchGoals.pending, (state) => {
state.goalsLoading = true;
state.goalsError = null;
})
.addCase(fetchGoals.fulfilled, (state, action) => {
state.goalsLoading = false;
const { query, response } = action.payload;
// 如果是第一页,替换数据;否则追加数据
if (query.page === 1) {
state.goals = response.list;
} else {
state.goals = [...state.goals, ...response.list];
}
state.goalsPagination = {
page: response.page,
pageSize: response.pageSize,
total: response.total,
hasMore: response.page * response.pageSize < response.total,
};
})
.addCase(fetchGoals.rejected, (state, action) => {
state.goalsLoading = false;
state.goalsError = action.payload as string;
})
// 加载更多目标
.addCase(loadMoreGoals.pending, (state) => {
state.goalsLoading = true;
})
.addCase(loadMoreGoals.fulfilled, (state, action) => {
state.goalsLoading = false;
const { response } = action.payload;
if (!response) {
return;
}
state.goals = [...state.goals, ...response.list];
state.goalsPagination = {
page: response.page,
pageSize: response.pageSize,
total: response.total,
hasMore: response.page * response.pageSize < response.total,
};
})
.addCase(loadMoreGoals.rejected, (state, action) => {
state.goalsLoading = false;
state.goalsError = action.payload as string;
})
// 获取目标详情
.addCase(fetchGoalDetail.pending, (state) => {
state.currentGoalLoading = true;
state.currentGoalError = null;
})
.addCase(fetchGoalDetail.fulfilled, (state, action) => {
state.currentGoalLoading = false;
state.currentGoal = action.payload;
})
.addCase(fetchGoalDetail.rejected, (state, action) => {
state.currentGoalLoading = false;
state.currentGoalError = action.payload as string;
})
// 创建目标
.addCase(createGoal.pending, (state) => {
state.createLoading = true;
state.createError = null;
})
.addCase(createGoal.fulfilled, (state, action) => {
state.createLoading = false;
// 将新目标添加到列表开头
const newGoal: GoalListItem = {
...action.payload,
progressPercentage: action.payload.targetCount && action.payload.targetCount > 0
? Math.round((action.payload.completedCount / action.payload.targetCount) * 100)
: 0,
};
state.goals.unshift(newGoal);
state.goalsPagination.total += 1;
})
.addCase(createGoal.rejected, (state, action) => {
state.createLoading = false;
state.createError = action.payload as string;
})
// 更新目标
.addCase(updateGoal.pending, (state) => {
state.updateLoading = true;
state.updateError = null;
})
.addCase(updateGoal.fulfilled, (state, action) => {
state.updateLoading = false;
const updatedGoal = action.payload;
// 计算进度百分比
const progressPercentage = updatedGoal.targetCount && updatedGoal.targetCount > 0
? Math.round((updatedGoal.completedCount / updatedGoal.targetCount) * 100)
: 0;
// 更新目标列表中的目标
const goalIndex = state.goals.findIndex(goal => goal.id === updatedGoal.id);
if (goalIndex !== -1) {
state.goals[goalIndex] = {
...state.goals[goalIndex],
...updatedGoal,
progressPercentage,
};
}
// 更新当前目标详情
if (state.currentGoal && state.currentGoal.id === updatedGoal.id) {
state.currentGoal = {
...state.currentGoal,
...updatedGoal,
progressPercentage,
};
}
})
.addCase(updateGoal.rejected, (state, action) => {
state.updateLoading = false;
state.updateError = action.payload as string;
})
// 删除目标
.addCase(deleteGoal.fulfilled, (state, action) => {
const goalId = action.payload;
// 从目标列表中移除
state.goals = state.goals.filter(goal => goal.id !== goalId);
state.goalsPagination.total -= 1;
// 如果删除的是当前查看的目标,清除详情
if (state.currentGoal && state.currentGoal.id === goalId) {
state.currentGoal = null;
}
})
// 记录目标完成
.addCase(completeGoal.fulfilled, (state, action) => {
const { goalId, completion } = action.payload;
// 增加完成次数
goalsSlice.caseReducers.incrementGoalCompletion(state, {
type: 'goals/incrementGoalCompletion',
payload: { goalId, count: completion.completionCount },
});
// 将完成记录添加到列表开头
state.completions.unshift(completion);
})
// 获取完成记录
.addCase(fetchGoalCompletions.pending, (state) => {
state.completionsLoading = true;
state.completionsError = null;
})
.addCase(fetchGoalCompletions.fulfilled, (state, action) => {
state.completionsLoading = false;
const { query, response } = action.payload;
// 如果是第一页,替换数据;否则追加数据
if (query?.page === 1) {
state.completions = response.list;
} else {
state.completions = [...state.completions, ...response.list];
}
state.completionsPagination = {
page: response.page,
pageSize: response.pageSize,
total: response.total,
hasMore: response.page * response.pageSize < response.total,
};
})
.addCase(fetchGoalCompletions.rejected, (state, action) => {
state.completionsLoading = false;
state.completionsError = action.payload as string;
})
// 获取目标统计
.addCase(fetchGoalStats.pending, (state) => {
state.statsLoading = true;
state.statsError = null;
})
.addCase(fetchGoalStats.fulfilled, (state, action) => {
state.statsLoading = false;
state.stats = action.payload;
})
.addCase(fetchGoalStats.rejected, (state, action) => {
state.statsLoading = false;
state.statsError = action.payload as string;
})
// 批量操作
.addCase(batchOperateGoals.pending, (state) => {
state.batchLoading = true;
state.batchError = null;
})
.addCase(batchOperateGoals.fulfilled, (state, action) => {
state.batchLoading = false;
const { operation, results } = action.payload;
// 根据操作类型更新状态
results.forEach(result => {
if (result.success) {
const goalIndex = state.goals.findIndex(goal => goal.id === result.goalId);
if (goalIndex !== -1) {
switch (operation.action) {
case 'pause':
state.goals[goalIndex].status = 'paused';
break;
case 'resume':
state.goals[goalIndex].status = 'active';
break;
case 'complete':
state.goals[goalIndex].status = 'completed';
break;
case 'delete':
state.goals = state.goals.filter(goal => goal.id !== result.goalId);
state.goalsPagination.total -= 1;
break;
}
}
}
});
})
.addCase(batchOperateGoals.rejected, (state, action) => {
state.batchLoading = false;
state.batchError = action.payload as string;
});
},
});
export const {
setFilters,
resetFilters,
clearErrors,
clearCurrentGoal,
updateGoalStatus,
incrementGoalCompletion,
} = goalsSlice.actions;
export default goalsSlice.reducer;

View File

@@ -2,6 +2,7 @@ import { configureStore, createListenerMiddleware } from '@reduxjs/toolkit';
import challengeReducer from './challengeSlice';
import checkinReducer, { addExercise, autoSyncCheckin, removeExercise, replaceExercises, setNote, toggleExerciseCompleted } from './checkinSlice';
import exerciseLibraryReducer from './exerciseLibrarySlice';
import goalsReducer from './goalsSlice';
import moodReducer from './moodSlice';
import scheduleExerciseReducer from './scheduleExerciseSlice';
import trainingPlanReducer from './trainingPlanSlice';
@@ -41,6 +42,7 @@ export const store = configureStore({
user: userReducer,
challenge: challengeReducer,
checkin: checkinReducer,
goals: goalsReducer,
mood: moodReducer,
trainingPlan: trainingPlanReducer,
scheduleExercise: scheduleExerciseReducer,