昨天我們將「AI 圖片解析」與「真實日曆」結合,跑者終於能驕傲地在 APP 上留下包含配速與教練評語的訓練日記。
但實際可怕的往往不是寫出新功能,而是「新舊狀態的衝突」:
setAiPlan(newPlan),那前 4 週辛苦上傳的跑錶截圖、汗水紀錄與 AI 講評將在瞬間被全新 JSON 完全抹除!今天要實作一套的「狀態全生命週期防護」:既要在微調時無損保留紀錄,又要在全新生成時乾淨隔離重設!
| 業務情境 | 觸發位置 | 正確行為 | 狀態處理方法 |
|---|---|---|---|
| 受傷/疲勞微調 | AI 診療室 (clinic.tsx) |
保留同賽事週期已打卡的血汗紀錄 | 呼叫 adjustAiPlan() 執行 Map 比對智慧無損合併 |
| 重新規劃新目標 | 課表頁 (schedule.tsx) |
開啟全新備賽週期,打卡紀錄全部歸零 | 呼叫 setAiPlan() 或 resetPlan() 重置打卡字典 |
Prompt
建立一個純函數模組
src/utils/scheduleMerge.ts:
- 設計
advancedMergeSchedules(oldSchedule, newSchedule)函數,以${weekNumber}-${dayOfWeek}作為唯一 Key。- 以新課表為基底,若對應的舊課表已被打卡 (
isCompleted === true),則保留舊課表的實跑里程、均速、心率、AI 講評、截圖與打卡時間。- 兼容處理扁平陣列 (
scheduleData) 與巢狀結構 (schedule: [{ week, days }])。
**產出內容與程式碼,**在 src/utils/scheduleMerge.ts 建立純函數合併器:
export interface MergedWorkoutItem {
weekNumber: number;
dayOfWeek: number;
workoutType: string;
distanceKm: number;
description: string;
isCompleted?: boolean;
actualDistance?: number | string;
actualPace?: string;
averageHeartRate?: number;
aiFeedback?: string;
imageUri?: string;
completedAt?: string;
}
export function advancedMergeSchedules(
oldSchedule: MergedWorkoutItem[],
newSchedule: MergedWorkoutItem[]
): MergedWorkoutItem[] {
// 建立舊課表打卡資料 Map
const oldCompletedMap = new Map<string, MergedWorkoutItem>();
for (const item of oldSchedule) {
if (item.isCompleted) {
oldCompletedMap.set(`${item.weekNumber}-${item.dayOfWeek}`, item);
}
}
// 遍歷新課表,若舊課表已打卡則無損保留
return newSchedule.map((newItem) => {
const key = `${newItem.weekNumber}-${newItem.dayOfWeek}`;
const oldCompleted = oldCompletedMap.get(key);
if (oldCompleted) {
return {
...newItem,
isCompleted: true,
actualDistance: oldCompleted.actualDistance,
actualPace: oldCompleted.actualPace,
averageHeartRate: oldCompleted.averageHeartRate,
aiFeedback: oldCompleted.aiFeedback,
imageUri: oldCompleted.imageUri,
completedAt: oldCompleted.completedAt,
};
}
return { ...newItem, isCompleted: false };
});
}
// ... extractWorkoutList 輔助工具函式略 ...
Prompt
重構 Zustand Store (
useStore.ts):
- 區分微調動作
adjustAiPlan與全新生成動作setAiPlan。adjustAiPlan:使用advancedMergeSchedules結合本地持久化的completedWorkouts字典進行合併。setAiPlan與resetPlan:當跑者建立新目標賽事時,自動將completedWorkouts: {}歸零,防止舊賽事打卡影響新課表。
**產出內容與程式碼,**在 src/store/useStore.ts 實作狀態隔離:
export const useStore = create<StoreState>()(
persist(
(set, get) => ({
aiPlan: null,
currentPlan: null,
completedWorkouts: {},
// 1. 生成全新課表:自動清空打卡歷史字典
setAiPlan: (plan) =>
set((state) => ({
aiPlan: plan,
completedWorkouts: {}, // 👈 全新課表強制歸零
currentPlan: plan ? { ...plan, data: plan.data || plan } : null,
})),
// 2. AI 診療室微調:啟動無損智慧合併
adjustAiPlan: (newPlanFromBackend) =>
set((state) => {
const oldSchedule = extractWorkoutList(state.currentPlan || state.aiPlan);
const newSchedule = extractWorkoutList(newPlanFromBackend);
const merged = advancedMergeSchedules(oldSchedule, newSchedule);
return {
aiPlan: { ...newPlanFromBackend, scheduleData: merged },
currentPlan: { ...state.currentPlan, data: { ...newPlanFromBackend, scheduleData: merged } },
};
}),
// 3. 重新設定課表與目標
resetPlan: () =>
set({ aiPlan: null, currentPlan: null, completedWorkouts: {}, sessionId: '' }),
// ... 其他狀態略 ...
}),
{ name: 'kakeru-storage', storage: createJSONStorage(() => AsyncStorage) }
)
);
微調之前:
微調之後:
重新產生課表:
Prompt
在
DailyWorkoutList.tsx渲染每日訓練卡片時,請以completedWorkouts字典作為打卡狀態的唯一依據 (isCompleted: Boolean(record?.isCompleted)),不要使用Boolean(record || item.isCompleted),避免舊物件快取中的isCompleted: true殘留導致誤判。
**產出內容與程式碼,**在 src/components/DailyWorkoutList.tsx 規範打卡屬性來源:
const recordKey = `${selectedWeek}-${dayIdx}`;
const record = completedWorkouts[recordKey];
return {
weekNumber: selectedWeek,
dayOfWeek: dayIdx,
title: item.workoutType || item.title || '訓練',
distance: dist,
description: item.description || '',
// 嚴格以 completedWorkouts 作為唯一真實來源
isCompleted: Boolean(record?.isCompleted),
actualDistance: record?.actualDistance,
actualPace: record?.actualPace,
aiFeedback: record?.aiFeedback || record?.coachFeedback,
};
今天我們解決了前端狀態管理中棘手的難題:
advancedMergeSchedules):讓 AI 診療室在微調課表時,100% 守護跑者過往的汗水紀錄;completedWorkouts 掌控,完全隔絕物件快取殘留問題。明天(Day 26)將進行上線前的最後一次細節打磨!我們將實作「API 載入鎖定防呆機制」來防止連點問題,並為 KAKERU 導入「深淺色模式」,提升不同使用體驗,為本地端開發畫下完美的句點。我們明天見!