一周膳食计划看起来是一排星期标签和几张餐食卡片,真正难点却是"时间"和"引用"。周一究竟对应哪一天?切到下周后数据是否隔离?一顿午餐关联的是菜谱 ID,还是一段不可追踪的名称?替换菜品后热量概览是否同步?本地写入失败时,页面能否准确告诉用户?
本文基于"健康菜谱助手"1.0.5 的真实 HarmonyOS ArkTS 工程展开。MealPlanPage.ets 已提供 7 天默认计划、工作日三餐、周末加餐、60 道菜的替换面板和 Preferences 持久化。但页面把日期显示成 28 + index,最后会出现 34;currentWeek 固定为"本周",数据模型没有具体日期和 weekId;餐食只保存 emoji、名称与时间,没有 recipeId;顶部营养概览固定显示 1850 kcal、78g 蛋白、55g 脂肪和 210g 碳水,替换菜品后不会变化。
周计划不是七组卡片的集合,而是由 weekId、dateKey、mealSlot 和 recipeId 共同定位的一组可持久化安排。
HEALTHY-RECIPE-ONE7-MEAL-PLAN-20260730

本文将完成以下内容:
- 还原默认计划、自动生成、手动替换与本地保存链路。
- 用真实日期替代
28 + index,处理跨月和跨年。 - 重新设计 weekId、dateKey、餐次和菜谱引用。
- 让替换、营养计算和详情跳转共享 recipeId。
- 修复
duration.indexOf('20')把 120 分钟菜谱当快手菜的问题。 - 区分生成、重置、保存失败和撤销状态。
- 给出纯本地周计划的验证矩阵和迁移策略。
一、当前页面已经具备哪些能力
页面状态如下:
ts
@State currentWeek: string = '本周'
@State selectedDay: number = 0
@State showPicker: boolean = false
@State editingMealIndex: number = -1
@StorageLink('appThemeColor') appThemeColor: string = '#4CAF50'
@StorageLink('mealPlans') plans: DayPlan[] = []
第一次进入且 plans 为空时,页面写入默认计划:
ts
aboutToAppear(): void {
if (this.plans.length === 0) {
this.plans = MealPlanPage.getDefaultPlans()
UserDataManager.persistMealPlans(this.plans)
}
}
默认数据包含周一到周日。周一至周五每天早餐、午餐、晚餐三项;周六和周日增加一项加餐,总数为:
text
5 × 3 + 2 × 4 = 23 餐
已有功能包括:
- 点击七个星期标签切换当天列表。
- 点击"更换"打开 60 道菜的底部选择面板。
- 替换时保留原餐食时间。
- 点击"生成"按本地规则重新生成一周计划。
- 点击"重置"恢复默认 23 餐。
- 通过
UserDataManager.persistMealPlans()写入 Preferences。
交互骨架完整,接下来要解决的是数据语义与联动一致性。
二、当前模型无法定位真实日期
页面模型只有:
ts
interface MealItem {
emoji: string
name: string
time: string
}
interface DayPlan {
day: string
meals: MealItem[]
}
day 只是"周一"这样的展示文本,没有年份、月份和日。页面顶部日期则直接计算:
ts
Text(`${28 + index}`)
这会导致三个确定问题:
- 周日显示 34,而任何月份都没有 34 日。
- 打开页面的实际日期无论是什么,仍显示 28--34。
- "本周"无法切换到上周或下周,因为数据没有 weekId。
如果只修 UI,把 34 改成 3,持久化仍然不知道这是哪个月。日期必须进入领域模型,而不是只在视图中格式化。
三、用 weekId 和 dateKey 建立稳定主键
建议使用周一的本地日期作为 weekId,每天使用 ISO 风格 YYYY-MM-DD 作为 dateKey:
ts
export interface WeekPlan {
schemaVersion: number
weekId: string
days: DayPlan[]
updatedAt: number
}
export interface DayPlan {
dateKey: string
meals: MealPlanItem[]
}
例如 2026 年 7 月 13 日所在周:
text
weekId = 2026-07-13
周一 dateKey = 2026-07-13
周日 dateKey = 2026-07-19
这样跨月、跨年都无需特殊字符串拼接。展示层可以从 dateKey 计算"周一""7月13日",存储层则始终使用稳定键。
四、计算一周日期时避免 24 小时毫秒偏移
可以用本地 Date 逐日构造,避免把夏令时地区的"一天"简单当作固定毫秒数。中国时区通常没有夏令时,但领域函数仍应保持正确:
ts
function startOfLocalWeek(input: Date): Date {
const date = new Date(
input.getFullYear(),
input.getMonth(),
input.getDate()
)
const day = date.getDay()
const offsetToMonday = day === 0 ? -6 : 1 - day
date.setDate(date.getDate() + offsetToMonday)
return date
}
function addLocalDays(input: Date, days: number): Date {
const result = new Date(
input.getFullYear(),
input.getMonth(),
input.getDate()
)
result.setDate(result.getDate() + days)
return result
}
生成七天:
ts
function buildWeekDates(anchor: Date): Date[] {
const monday = startOfLocalWeek(anchor)
return Array.from({ length: 7 }, (_, index) => addLocalDays(monday, index))
}
Date.setDate() 会自然处理月末和年末,不会出现 34 日。

五、selectedDay 应该由今天决定
当前 selectedDay 初始为 0,任何一天进入都默认周一。更自然的规则是:查看本周时默认选中今天;查看其他周时默认选中周一,或保留用户上次选择。
ts
function resolveInitialDayIndex(weekDates: Date[], today: Date): number {
const key = formatDateKey(today)
const index = weekDates.findIndex((date) => formatDateKey(date) === key)
return index >= 0 ? index : 0
}
当用户切换上一周、下一周时,必须先更新 weekId 和 weekDates,再钳制 selectedDay:
ts
this.selectedDay = Math.max(0, Math.min(this.selectedDay, 6))
六、MealItem 必须保存 recipeId
当前替换逻辑把菜名复制到字符串:
ts
newMeals[this.editingMealIndex] = {
emoji: this.getEmojiByCategory(recipe.category),
name: recipe.name,
time: oldMeal.time
}
替换后丢失了 recipeId,因此无法可靠完成:
- 点击餐食跳转菜谱详情。
- 读取热量、蛋白、脂肪和碳水。
- 菜谱改名后自动更新展示。
- 生成购物清单。
- 判断同一周是否重复安排同一道菜。
建议模型改为:
ts
export type MealSlot = 'breakfast' | 'lunch' | 'dinner' | 'snack'
export interface MealPlanItem {
id: string
slot: MealSlot
time: string
recipeId?: number
customName?: string
portions: number
}
早餐"小米粥 + 鸡蛋"可能不是单一 RecipeModel,可使用 customName;午餐和晚餐替换为应用菜谱时保存 recipeId。两者互斥,避免 name 同时成为主键和展示文案。
七、展示模型与持久化模型分开
页面需要 emoji、名称和营养值,但这些不必全部写进 Preferences。可由 recipeId 派生:
ts
export interface MealPlanItemView {
id: string
slotLabel: string
emoji: string
name: string
time: string
calories?: number
recipeId?: number
}
function toMealView(item: MealPlanItem): MealPlanItemView {
const recipe = item.recipeId
? RECIPE_LIST.find((value) => value.id === item.recipeId)
: undefined
return {
id: item.id,
slotLabel: getMealSlotLabel(item.slot),
emoji: recipe ? getEmojiByCategory(recipe.category) : '🍽',
name: recipe?.name ?? item.customName ?? '未安排',
time: item.time,
calories: recipe?.calories,
recipeId: recipe?.id
}
}
菜谱内容更新后,周计划只要 recipeId 仍有效,就能显示新名称和新营养数据。
八、替换动作必须校验三层索引
当前 replaceMeal() 直接读取:
ts
const dayPlan = newPlans[this.selectedDay]
const oldMeal = newMeals[this.editingMealIndex]
正常点击路径会先设置索引,但页面重建、快速关闭弹窗或未来异步加载都可能让索引失效。替换前应校验:
ts
private canReplaceMeal(): boolean {
if (this.selectedDay < 0 || this.selectedDay >= this.weekPlan.days.length) {
return false
}
const meals = this.weekPlan.days[this.selectedDay].meals
return this.editingMealIndex >= 0 && this.editingMealIndex < meals.length
}
校验失败时关闭面板并给出明确提示,不能访问 undefined.time。
九、用不可变更新保持 ArkUI 刷新
当前代码通过复制 plans、day 和 meals 三层数组进行更新,这是值得保留的做法。改造后的函数可以继续遵循:
ts
function replaceMealInWeek(
plan: WeekPlan,
dateKey: string,
mealId: string,
recipeId: number
): WeekPlan {
return {
...plan,
updatedAt: Date.now(),
days: plan.days.map((day) => {
if (day.dateKey !== dateKey) return day
return {
...day,
meals: day.meals.map((meal) =>
meal.id === mealId
? { ...meal, recipeId, customName: undefined }
: meal
)
}
})
}
}
用 dateKey 和 mealId 替代数组位置后,排序或插入加餐不会让编辑目标漂移。
十、营养概览不能写死
当前四个徽章固定显示:
ts
this.NutrientBadge('🔥', '1850', 'kcal')
this.NutrientBadge('🥩', '78', 'g蛋白')
this.NutrientBadge('🫒', '55', 'g脂肪')
this.NutrientBadge('🌾', '210', 'g碳水')
它们看起来像所选日期的动态统计,实际上替换任何菜都不会改变。解决方式有两种:
- 如果计划项都有 recipeId,按份数汇总真实菜谱字段。
- 如果自定义早餐缺少营养数据,则显示"已统计 2/3 餐",避免假装完整。
ts
export interface DayNutritionSummary {
calories: number
protein: number
fat: number
carbs: number
countedMeals: number
totalMeals: number
}
汇总函数:
ts
function summarizeDay(day: DayPlan): DayNutritionSummary {
const summary: DayNutritionSummary = {
calories: 0,
protein: 0,
fat: 0,
carbs: 0,
countedMeals: 0,
totalMeals: day.meals.length
}
day.meals.forEach((meal) => {
const recipe = meal.recipeId
? RECIPE_LIST.find((item) => item.id === meal.recipeId)
: undefined
if (!recipe) return
summary.calories += recipe.calories * meal.portions
summary.protein += recipe.protein * meal.portions
summary.fat += recipe.fat * meal.portions
summary.carbs += recipe.carbs * meal.portions
summary.countedMeals++
})
return summary
}
这些值属于菜谱估算,不应表述为医疗建议或精确营养处方。
十一、当前"智能生成"是确定性本地规则
generateSmartWeekPlan() 使用四种早餐循环,并按索引从午餐候选和清淡晚餐候选中取菜:
ts
const lunchRecipe = this.pickRecipeByIndex(dayIndex * 2, false)
const dinnerRecipe = this.pickRecipeByIndex(dayIndex * 2 + 1, true)
这不是 AI 模型,而是确定性规则生成器。优点是离线、快速、可复现;缺点是规则目前没有考虑重复菜谱、营养总量、用户忌口和跨周变化。UI 更准确的文案可以是"按规则生成"或"一键搭配",避免夸大。
十二、修复快手菜筛选中的字符串误判
非清淡候选的当前条件是:
ts
return recipe.difficulty === '简单'
|| recipe.tags.indexOf('下饭') >= 0
|| recipe.duration.indexOf('20') >= 0
这里有两个真实问题:
- 数据标签是"下饭菜",数组
indexOf('下饭')要求完全相等,因此无法命中。 120分钟包含字符20,会被误判为 20 分钟菜谱。
应使用精确标签与数值时长:
ts
function isLunchCandidate(recipe: RecipeModel): boolean {
return recipe.difficulty === '简单'
|| recipe.tags.includes('下饭菜')
|| parseDurationMinutes(recipe.duration) <= 30
}
时间最好直接在模型中保存为 durationMinutes,避免每个功能各自解析展示文案。
十三、晚餐清淡规则也要可解释
当前晚餐候选满足任一条件即可:热量不高于 180、标签包含清淡,或菜名包含清蒸。这是可用的第一版,但需要把命中理由带到结果中:
ts
interface MealCandidate {
recipe: RecipeModel
reasons: string[]
}
function explainLightCandidate(recipe: RecipeModel): MealCandidate | undefined {
const reasons: string[] = []
if (recipe.calories <= 180) reasons.push('热量较低')
if (recipe.tags.includes('清淡')) reasons.push('清淡标签')
if (recipe.name.includes('清蒸')) reasons.push('清蒸做法')
return reasons.length ? { recipe, reasons } : undefined
}
生成记录中保存规则版本,未来调整阈值时能解释旧计划为何不同。
十四、避免一周内重复菜谱
按候选索引取模可能在候选较少时重复。可以维护已使用 recipeId:
ts
function pickUnused(
candidates: RecipeModel[],
used: Set<number>,
fallbackIndex: number
): RecipeModel {
const unused = candidates.find((recipe) => !used.has(recipe.id))
const selected = unused ?? candidates[fallbackIndex % candidates.length]
used.add(selected.id)
return selected
}
规则可以允许早餐重复,但午餐和晚餐在同一周尽量不重复;当候选不足时允许重复,并在日志中记录降级。
十五、周切换需要独立的数据仓库
当前 Preferences 只使用一个 mealPlans key,覆盖的是一份无日期数组。支持上一周、下一周后,可以保存:
ts
interface StoredMealPlans {
schemaVersion: number
weeks: Record<string, WeekPlan>
}
仓库接口:
ts
export interface MealPlanRepository {
getWeek(weekId: string): Promise<WeekPlan | undefined>
saveWeek(plan: WeekPlan): Promise<SaveResult>
deleteWeek(weekId: string): Promise<SaveResult>
}
页面不直接操作 JSON,不知道 Preferences key。这样以后改用 RDB 时不需要重写 ArkUI 页面。
十六、保存必须返回结果
UserDataManager.persist() 当前返回 void,prefs 未初始化时直接返回,异常也被 catch 后忽略。页面仍会显示"已生成本周菜单"或"已更换为某菜"。这会制造假成功。
建议返回:
ts
export interface SaveResult {
ok: boolean
reason?: 'not_initialized' | 'serialize_failed' | 'write_failed'
}
替换流程应该是:
- 生成新的不可变 WeekPlan。
- 暂时更新页面并显示保存中。
- 仓库保存成功后提示完成。
- 保存失败则恢复旧计划,或明确显示"仅本次会话有效"。
用户看到的 Toast 必须与真实写入结果一致。
十七、重置属于破坏性操作
当前点击"重置"立即覆盖整周计划。用户可能已经逐餐调整,误触会丢失全部修改。更稳的交互是确认或提供撤销:
ts
interface ResetSnapshot {
previous: WeekPlan
resetAt: number
}
重置成功后显示"已恢复默认菜单 · 撤销",短时间内可以写回 previous。对只有一周本地数据的应用,撤销比复杂版本历史更实用。
十八、替换面板的状态要绑定 mealId
editingMealIndex 与当前数组位置耦合。建议改为:
ts
@State editingMealId: string = ''
@State pickerState: 'closed' | 'open' | 'saving' = 'closed'
打开时保存 mealId,关闭时清空。保存期间禁用重复选择,避免连续点击两道菜触发乱序写入。
ts
if (this.pickerState === 'saving') return
this.pickerState = 'saving'
try {
await this.replaceAndPersist(recipe.id)
} finally {
this.pickerState = 'closed'
this.editingMealId = ''
}
十九、页面状态应该完整
周计划至少需要:
ts
type MealPlanPageState =
'loading' | 'content' | 'empty' | 'saving' | 'error'
当前 plans[this.selectedDay].meals 假设 plans 一定已初始化。如果 Preferences 解析失败、迁移失败或异步加载尚未完成,直接访问会越界。只有 content 状态才渲染餐食列表;error 提供重试和恢复默认计划入口。

二十、架构分层
20.1 DateService
负责周起始日、七天日期、dateKey 格式化和前后周切换。它不依赖页面状态。
20.2 MealPlanRepository
负责 schemaVersion、周数据加载、保存、迁移和错误映射。
20.3 MealPlanService
负责生成默认周、按规则生成、替换 mealId、避免重复和计算营养汇总。
20.4 MealPlanPage
负责选择日期、显示卡片、打开替换面板、展示保存结果和导航详情。
分层后,日期和生成规则可以纯 ArkTS 单元测试,不需要启动页面。
二十一、旧数据如何迁移
现有 mealPlans 是 DayPlan[],没有版本字段。首次升级时可以识别数组格式并迁移:
ts
function migrateLegacyPlans(raw: unknown, anchor: Date): WeekPlan {
if (!Array.isArray(raw)) {
return createDefaultWeek(anchor)
}
const weekDates = buildWeekDates(anchor)
return {
schemaVersion: 2,
weekId: formatDateKey(weekDates[0]),
updatedAt: Date.now(),
days: weekDates.map((date, dayIndex) => ({
dateKey: formatDateKey(date),
meals: migrateLegacyMeals(raw[dayIndex], dayIndex)
}))
}
}
旧字符串名称如果能唯一匹配 RECIPE_LIST.name,补上 recipeId;匹配不到则保留为 customName。迁移失败不能清空所有用户数据,应记录问题并保留恢复默认的显式选择。
二十二、多设备布局
手机适合顶部七天横排和下方餐食 List。小屏手机若 7 个 44vp 日期项再加间距,容易拥挤;可使用等分宽度而非固定宽度。平板和 2in1 可左侧显示周日历,右侧显示当天计划和营养汇总。
| 宽度 | 布局 | 重点 |
|---|---|---|
| 小于 600vp | 单列 | 七天等分,底部面板避让系统导航 |
| 600--960vp | 左日历右计划 | 替换面板限宽并居中 |
| 大于 960vp | 周视图网格 + 详情侧栏 | 鼠标、键盘、窗口缩放 |
底部选菜面板当前固定 360vp 高。小屏横屏可能压缩可用区域,建议使用最大高度比例和内部滚动,确保最后一条菜谱与关闭按钮都可达。
二十三、回归测试矩阵
| 编号 | 场景 | 预期 |
|---|---|---|
| M01 | 周一进入本周 | 默认选中今天,而不是固定周一 |
| M02 | 月末跨月 | 日期从当月最后一天自然进入下月 |
| M03 | 年末跨年 | weekId 和 dateKey 年份正确 |
| M04 | 周日为锚点 | 正确回溯到本周周一 |
| M05 | 默认计划 | 工作日 3 餐、周末 4 餐,总计 23 餐 |
| M06 | 替换午餐 | 保留 mealId、slot 与时间,只更新 recipeId |
| M07 | 打开替换后切日 | 不把菜替换到错误日期 |
| M08 | 非法 mealId | 拒绝保存并关闭异常面板 |
| M09 | 替换后营养 | 当天汇总按新 recipeId 重新计算 |
| M10 | 120 分钟菜谱 | 不因包含字符 20 被判为快手 |
| M11 | 下饭菜标签 | 使用 includes('下饭菜') 正确命中 |
| M12 | 自动生成 | 午晚餐尽量不重复,并记录规则版本 |
| M13 | 保存失败 | 不显示假成功,页面可重试或回滚 |
| M14 | 重置误触 | 有确认或短时撤销 |
| M15 | 旧数据迁移 | 可匹配名称补 recipeId,其余保留 customName |
| M16 | 周切换 | 不同 weekId 的计划相互隔离 |
| M17 | 空数据 | 显示空态或默认恢复入口,不越界 |
| M18 | 快速重复选择 | 只保存最后一次有效动作,不乱序覆盖 |
二十四、可复核的验收步骤
24.1 日期函数
分别输入月中、月末、年末和周日四个日期,检查 buildWeekDates() 始终返回连续 7 天,第一项为周一,dateKey 不重复。
24.2 数据关联
遍历所有带 recipeId 的 MealPlanItem,确认 ID 能在 60 道菜中找到。替换后检查 recipeId、时间、slot 和 portions;名称和 emoji 应由菜谱派生。
24.3 营养联动
记录替换前当天汇总,换成一条热量不同的菜谱,再检查 calories、protein、fat、carbs 是否同步变化;自定义餐食不应被当作 0 营养悄悄计入完整统计。
24.4 持久化
完成一次替换后退出并重启应用,确认同 weekId、dateKey、mealId 和 recipeId 恢复。模拟 Preferences 写入失败,检查页面没有"已更换"假提示。
24.5 多设备
在手机竖屏、横屏、平板和 2in1 窗口下检查七天标签、营养徽章、餐食名称长文本、底部面板和安全区。深色模式下检查选中日期、卡片背景、遮罩和禁用态对比度。
二十五、常见问题与定位
25.1 日期出现 34
原因: 视图直接使用 28 + index。
处理: 从周一 Date 逐日调用 setDate(),展示真实月日。
25.2 换菜后营养数字不变
原因: 四个营养徽章是硬编码文本,MealItem 又没有 recipeId。
处理: 保存 recipeId,按所选日期的餐食实时汇总。
25.3 生成菜单出现超慢菜
原因: duration.indexOf('20') 会命中 120分钟。
处理: 使用数值分钟比较,不对展示字符串做子串判断。
25.4 下饭菜没有进入候选
原因: 标签数组中是"下饭菜",代码却查找精确值"下饭"。
处理: 使用统一标签常量并精确匹配"下饭菜"。
25.5 重启后计划恢复默认
原因: Preferences 未初始化、JSON 解析失败或写入失败被静默吞掉。
处理: 仓库返回结构化错误,保留旧数据并提供恢复或重试。
25.6 切换周后仍看到同一份计划
原因: 存储只有一个 mealPlans 数组,没有 weekId。
处理: 按周一 dateKey 保存多个 WeekPlan。
二十六、落地顺序
- 先引入 dateKey 与 weekId,替换硬编码日期。
- 给 MealPlanItem 增加 id、slot、recipeId 和 portions。
- 把替换索引改为 dateKey + mealId。
- 实现营养汇总,删除固定数字。
- 修复生成候选的标签和时长规则,并避免重复。
- 让仓库返回保存结果,增加旧数据迁移。
- 最后补周切换、撤销和多设备布局。
每一步都能单独验证,避免日期、存储和 UI 同时大改后难以定位问题。
二十七、源码事实与建议实现的边界
本文同时包含源码复盘和演进设计。为了避免把规划方案描述成当前安装包能力,下面按本地源码快照归档。事实项可从 MealPlanPage.ets、UserDataManager.ets、MockData.ets 和 RecipeModel.ets 直接复核。
| 检查项 | 当前源码事实 | 本文建议,不代表已经落地 |
|---|---|---|
| 默认计划 | getDefaultPlans() 返回 7 天;工作日各 3 餐,周末各 4 餐,共 23 餐 |
用带版本号的 WeekPlan 管理多周数据 |
| 日期显示 | 星期固定为周一至周日,日期文本直接使用 28 + index,会显示到 34 |
从真实周一 Date 生成连续 dateKey,支持跨月跨年 |
| 当前周 | currentWeek 固定为"本周",没有上一周、下一周数据隔离 |
用周一 dateKey 作为 weekId,并按周读取仓库 |
| 默认选中日 | selectedDay 初始为 0,每次进入默认周一 |
查看本周时默认选中今天,其他周钳制到有效索引 |
| 餐食模型 | MealItem 只保存 emoji、name、time,没有 mealId、slot、recipeId、portions |
持久化稳定 ID,展示名称和营养由 recipeId 派生 |
| 替换动作 | 通过 selectedDay 与 editingMealIndex 定位,保留原时间并复制新菜名 |
使用 dateKey + mealId 校验目标,保存期间防重复操作 |
| 菜谱面板 | 替换面板遍历 RECIPE_LIST 的 60 道菜 |
增加筛选、搜索、焦点与大屏限宽,不改变真实菜谱总量 |
| 营养概览 | 四个徽章固定显示 1850 kcal、78g 蛋白、55g 脂肪、210g 碳水,换菜不会联动 | 按 recipeId 与份数汇总,并标明已统计餐数 |
| 自动生成 | 四种早餐循环;午晚餐按候选索引确定性选择;周末增加加餐 | 避免重复、解析数值时长、保存规则版本和命中理由 |
| 快手筛选 | 检查"简单"、精确标签"下饭"或时长字符串包含"20",会漏掉"下饭菜"并误命中 120分钟 |
使用标签常量与数值分钟判断 |
| 持久化 | mealPlans 通过 UserDataManager.persistMealPlans() 写入单一 key;prefs 未初始化时返回,异常被忽略,flush() 未等待结果 |
仓库返回 SaveResult,失败时提示、回滚或重试 |
| 重置 | 点击"重置"立即覆盖计划并持久化,没有确认或撤销 | 增加确认或短时撤销,避免误触丢失全部调整 |
| 构建与设备验证 | 本文完成了本地源码统计,没有声称执行 HAP 构建、安装或真机测试 | 在 phone、tablet、2in1 上验证日期栏、底部面板、安全区和键鼠焦点 |
因此,真实日期、跨周仓库、动态营养、失败回滚和旧数据迁移都属于建议实现。只有相关代码落地、构建通过并完成设备验证后,才能把它们写成应用当前能力。
二十八、总结
健康菜谱助手的一周计划页已经有 7 天、23 餐默认数据、自动生成、手动替换和 Preferences 保存,是一个可操作的原型。真正阻碍它成为可靠工具的,是日期没有主键、餐食没有 recipeId、营养概览不联动,以及生成规则直接对字符串做判断。
把周计划做稳,需要守住五条原则:
- weekId 与 dateKey 由真实日期计算,跨月跨年仍正确。
- MealPlanItem 用 mealId 和 recipeId 建立稳定关联。
- 替换后营养、详情跳转和持久化围绕同一个 recipeId 更新。
- 自动生成是可解释本地规则,时长必须按数值比较。
- 保存、重置和迁移都要有明确结果,不能用 Toast 掩盖失败。
当用户在周三替换晚餐、下周回来仍能看到正确日期与营养变化时,这个页面才真正完成了"计划"而不只是"展示"。
参考资料
- HarmonyOS ArkUI 概述
- HarmonyOS ArkTS 概述
- HarmonyOS 数据管理概述
- HarmonyOS AppStorage 开发指导
- 工程源码:
MealPlanPage.ets、UserDataManager.ets、MockData.ets、RecipeModel.ets
AI 辅助声明:本文由 AI 辅助完成源码统计、结构校对与配图设计,当前能力结论均以文中列出的本地 ArkTS 源码为依据,并由作者复核后发布;建议模型、迁移方案和测试矩阵不代表工程已经构建或上线。