import { createCheckin, fetchCheckinsInRange, 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; currentDate: string | null; monthLoaded: Record; // key: YYYY-MM, 标记该月数据是否已加载 }; const initialState: CheckinState = { byDate: {}, currentDate: null, monthLoaded: {}, }; 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) { 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) { 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, }; }) .addCase(loadMonthCheckins.fulfilled, (state, action) => { const monthKey = action.payload.monthKey; const merged = action.payload.byDate; for (const d of Object.keys(merged)) { state.byDate[d] = merged[d]; } state.monthLoaded[monthKey] = true; }); }, }); 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[] }; }); // 按月加载:优先使用区间接口,失败则逐日回退 export const loadMonthCheckins = createAsyncThunk( 'checkin/loadMonth', async (payload: { year: number; month1Based: number }, { getState }) => { const { year, month1Based } = payload; const pad = (n: number) => `${n}`.padStart(2, '0'); const monthKey = `${year}-${pad(month1Based)}`; const start = `${year}-${pad(month1Based)}-01`; const endDate = new Date(year, month1Based, 0).getDate(); const end = `${year}-${pad(month1Based)}-${pad(endDate)}`; try { const list = await fetchCheckinsInRange(start, end); const byDate: Record = {}; for (const rec of list) { const date = rec?.checkinDate || rec?.date; if (!date) continue; const id = rec?.id ? String(rec.id) : `rec_${date}`; const items = (rec?.metrics?.items ?? rec?.items) ?? []; const note = typeof rec?.notes === 'string' ? rec.notes : undefined; byDate[date] = { id, date, items, note }; } return { monthKey, byDate } as { monthKey: string; byDate: Record }; } catch { // 回退逐日请求(并行) const endNum = new Date(year, month1Based, 0).getDate(); const dates = Array.from({ length: endNum }, (_, i) => `${year}-${pad(month1Based)}-${pad(i + 1)}`); const results = await Promise.all( dates.map(async (d) => ({ d, list: await fetchDailyCheckins(d) })) ); const byDate: Record = {}; for (const { d, list } of results) { let items: CheckinExercise[] = []; let note: string | undefined; let id: string | undefined; for (const rec of list) { if (rec?.id && !id) id = String(rec.id); const metricsItems = rec?.metrics?.items ?? rec?.items; if (Array.isArray(metricsItems)) items = metricsItems as CheckinExercise[]; if (typeof rec?.notes === 'string') note = rec.notes as string; } byDate[d] = { id: id || `rec_${d}`, date: d, items, note }; } return { monthKey, byDate } as { monthKey: string; byDate: Record }; } }, { condition: (payload, { getState }) => { const state = getState() as any; const pad = (n: number) => `${n}`.padStart(2, '0'); const monthKey = `${payload.year}-${pad(payload.month1Based)}`; return !state?.checkin?.monthLoaded?.[monthKey]; }, } );