Files
digital-pilates/store/checkinSlice.ts
richarjiang 5d09cc05dc feat: 更新文章功能和相关依赖
- 新增文章详情页面,支持根据文章 ID 加载和展示文章内容
- 添加文章卡片组件,展示推荐文章的标题、封面和阅读量
- 更新文章服务,支持获取文章列表和根据 ID 获取文章详情
- 集成腾讯云 COS SDK,支持文件上传功能
- 优化打卡功能,支持按日期加载和展示打卡记录
- 更新相关依赖,确保项目兼容性和功能完整性
- 调整样式以适应新功能的展示和交互
2025-08-14 16:03:19 +08:00

237 lines
9.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { createCheckin, fetchCheckinsInRange, fetchDailyCheckins, updateCheckin } from '@/services/checkins';
import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit';
export type CheckinExercise = {
key: string;
name: string;
category: string;
/**
* itemType
* - exercise: 正常训练动作(默认)
* - rest: 组间/动作间休息(仅展示,不可勾选完成)
* - note: 备注/口令提示(仅展示)
*/
itemType?: 'exercise' | 'rest' | 'note';
sets: number; // 组数rest/note 可为 0
reps?: number; // 每组重复(计次型)
durationSec?: number; // 每组时长(计时型)
restSec?: number; // 休息时长(当 itemType=rest 时使用)
note?: string; // 备注内容(当 itemType=note 时使用)
completed?: boolean; // 是否已完成该动作
};
export type CheckinRecord = {
id: string;
date: string; // YYYY-MM-DD
items: CheckinExercise[];
note?: string;
// 保留后端原始返回,便于当 metrics.items 为空时做回退展示
raw?: any[];
};
export type CheckinState = {
byDate: Record<string, CheckinRecord>;
currentDate: string | null;
monthLoaded: Record<string, boolean>; // 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<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);
},
replaceExercises(state, action: PayloadAction<{ date: string; items: CheckinExercise[]; note?: string }>) {
const rec = ensureRecord(state, action.payload.date);
rec.items = (action.payload.items || []).map((it) => ({ ...it, completed: false }));
if (typeof action.payload.note === 'string') rec.note = action.payload.note;
},
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].itemType ?? 'exercise') === 'exercise') {
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,
raw: list,
};
})
.addCase(loadMonthCheckins.fulfilled, (state, action) => {
const monthKey = action.payload.monthKey;
const merged = action.payload.byDate;
for (const d of Object.keys(merged)) {
const prev = state.byDate[d];
const next = merged[d];
const items = (next.items && next.items.length > 0) ? next.items : (prev?.items ?? []);
const note = (typeof next.note === 'string') ? next.note : prev?.note;
const id = next.id || prev?.id || `rec_${d}`;
const raw = prev?.raw ?? (next as any)?.raw;
state.byDate[d] = { id, date: d, items, note, raw } as CheckinRecord;
}
state.monthLoaded[monthKey] = true;
});
},
});
export const { setCurrentDate, addExercise, removeExercise, replaceExercises, 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 dateParam = date ?? new Date().toISOString().slice(0, 10);
const list = await fetchDailyCheckins(dateParam);
try { console.log('getDailyCheckins', { date: dateParam, count: Array.isArray(list) ? list.length : -1 }); } catch { }
return { date: dateParam, 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<string, CheckinRecord> = {};
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<string, CheckinRecord> };
} 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<string, CheckinRecord> = {};
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<string, CheckinRecord> };
}
},
{
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];
},
}
);