- 在个人信息页面中修改用户姓名字段为“name”,并添加注销帐号功能,支持用户删除帐号及相关数据 - 在打卡页面中集成从后端获取当天打卡列表的功能,确保用户数据的实时同步 - 更新 Redux 状态管理,支持打卡记录的同步和更新 - 新增打卡服务,提供创建、更新和删除打卡记录的 API 接口 - 优化样式以适应新功能的展示和交互
144 lines
5.2 KiB
TypeScript
144 lines
5.2 KiB
TypeScript
import { createCheckin, fetchDailyCheckins, updateCheckin } from '@/services/checkins';
|
|
import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit';
|
|
|
|
export type CheckinExercise = {
|
|
key: string;
|
|
name: string;
|
|
category: string;
|
|
sets: number; // 组数
|
|
reps?: number; // 每组重复(计次型)
|
|
durationSec?: number; // 每组时长(计时型)
|
|
completed?: boolean; // 是否已完成该动作
|
|
};
|
|
|
|
export type CheckinRecord = {
|
|
id: string;
|
|
date: string; // YYYY-MM-DD
|
|
items: CheckinExercise[];
|
|
note?: string;
|
|
};
|
|
|
|
export type CheckinState = {
|
|
byDate: Record<string, CheckinRecord>;
|
|
currentDate: string | null;
|
|
};
|
|
|
|
const initialState: CheckinState = {
|
|
byDate: {},
|
|
currentDate: null,
|
|
};
|
|
|
|
function ensureRecord(state: CheckinState, date: string): CheckinRecord {
|
|
if (!state.byDate[date]) {
|
|
state.byDate[date] = {
|
|
id: `rec_${date}`,
|
|
date,
|
|
items: [],
|
|
};
|
|
}
|
|
return state.byDate[date];
|
|
}
|
|
|
|
const checkinSlice = createSlice({
|
|
name: 'checkin',
|
|
initialState,
|
|
reducers: {
|
|
setCurrentDate(state, action: PayloadAction<string>) {
|
|
state.currentDate = action.payload; // 期望格式 YYYY-MM-DD
|
|
ensureRecord(state, action.payload);
|
|
},
|
|
addExercise(state, action: PayloadAction<{ date: string; item: CheckinExercise }>) {
|
|
const rec = ensureRecord(state, action.payload.date);
|
|
// 若同 key 已存在则覆盖参数(更接近用户“重新选择/编辑”的心智)
|
|
const idx = rec.items.findIndex((it) => it.key === action.payload.item.key);
|
|
const normalized: CheckinExercise = { ...action.payload.item, completed: false };
|
|
if (idx >= 0) rec.items[idx] = normalized; else rec.items.push(normalized);
|
|
},
|
|
removeExercise(state, action: PayloadAction<{ date: string; key: string }>) {
|
|
const rec = ensureRecord(state, action.payload.date);
|
|
rec.items = rec.items.filter((it) => it.key !== action.payload.key);
|
|
},
|
|
toggleExerciseCompleted(state, action: PayloadAction<{ date: string; key: string }>) {
|
|
const rec = ensureRecord(state, action.payload.date);
|
|
const idx = rec.items.findIndex((it) => it.key === action.payload.key);
|
|
if (idx >= 0) rec.items[idx].completed = !rec.items[idx].completed;
|
|
},
|
|
setNote(state, action: PayloadAction<{ date: string; note: string }>) {
|
|
const rec = ensureRecord(state, action.payload.date);
|
|
rec.note = action.payload.note;
|
|
},
|
|
resetDate(state, action: PayloadAction<string>) {
|
|
delete state.byDate[action.payload];
|
|
},
|
|
},
|
|
extraReducers: (builder) => {
|
|
builder
|
|
.addCase(syncCheckin.fulfilled, (state, action) => {
|
|
if (!action.payload) return;
|
|
const { date, items, note, id } = action.payload;
|
|
state.byDate[date] = {
|
|
id: id || state.byDate[date]?.id || `rec_${date}`,
|
|
date,
|
|
items: items || [],
|
|
note,
|
|
};
|
|
})
|
|
.addCase(getDailyCheckins.fulfilled, (state, action) => {
|
|
const date = action.payload.date as string | undefined;
|
|
const list = action.payload.list || [];
|
|
if (!date) return;
|
|
let mergedItems: CheckinExercise[] = [];
|
|
let note: string | undefined = undefined;
|
|
let id: string | undefined = state.byDate[date]?.id;
|
|
for (const rec of list) {
|
|
if (!rec) continue;
|
|
if (rec.id && !id) id = String(rec.id);
|
|
const itemsFromMetrics = rec?.metrics?.items ?? rec?.items;
|
|
if (Array.isArray(itemsFromMetrics)) {
|
|
mergedItems = itemsFromMetrics as CheckinExercise[];
|
|
}
|
|
if (typeof rec.notes === 'string') note = rec.notes as string;
|
|
}
|
|
state.byDate[date] = {
|
|
id: id || state.byDate[date]?.id || `rec_${date}`,
|
|
date,
|
|
items: mergedItems,
|
|
note,
|
|
};
|
|
});
|
|
},
|
|
});
|
|
|
|
export const { setCurrentDate, addExercise, removeExercise, toggleExerciseCompleted, setNote, resetDate } = checkinSlice.actions;
|
|
export default checkinSlice.reducer;
|
|
|
|
// Thunks
|
|
export const syncCheckin = createAsyncThunk('checkin/sync', async (record: { date: string; items: CheckinExercise[]; note?: string; id?: string }, { getState }) => {
|
|
const state = getState() as any;
|
|
const existingId: string | undefined = record.id || state?.checkin?.byDate?.[record.date]?.id;
|
|
const metrics = { items: record.items } as any;
|
|
if (!existingId || existingId.startsWith('rec_')) {
|
|
const created = await createCheckin({
|
|
title: '每日训练打卡',
|
|
checkinDate: record.date,
|
|
notes: record.note,
|
|
metrics,
|
|
startedAt: new Date().toISOString(),
|
|
});
|
|
const newId = (created as any)?.id;
|
|
return { id: newId, date: record.date, items: record.items, note: record.note };
|
|
}
|
|
const updated = await updateCheckin({ id: existingId, notes: record.note, metrics });
|
|
const newId = (updated as any)?.id ?? existingId;
|
|
return { id: newId, date: record.date, items: record.items, note: record.note };
|
|
});
|
|
|
|
// 获取当天打卡列表(用于进入页面时拉取最新云端数据)
|
|
export const getDailyCheckins = createAsyncThunk('checkin/getDaily', async (date?: string) => {
|
|
const list = await fetchDailyCheckins(date);
|
|
|
|
return { date, list } as { date?: string; list: any[] };
|
|
});
|
|
|
|
|