【健康菜谱助手|04】HarmonyOS ArkTS 一周膳食计划实战:管理日期、餐次与菜谱替换的联动

一周膳食计划看起来是一排星期标签和几张餐食卡片,真正难点却是"时间"和"引用"。周一究竟对应哪一天?切到下周后数据是否隔离?一顿午餐关联的是菜谱 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}`)

这会导致三个确定问题:

  1. 周日显示 34,而任何月份都没有 34 日。
  2. 打开页面的实际日期无论是什么,仍显示 28--34。
  3. "本周"无法切换到上周或下周,因为数据没有 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碳水')

它们看起来像所选日期的动态统计,实际上替换任何菜都不会改变。解决方式有两种:

  1. 如果计划项都有 recipeId,按份数汇总真实菜谱字段。
  2. 如果自定义早餐缺少营养数据,则显示"已统计 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'
}

替换流程应该是:

  1. 生成新的不可变 WeekPlan。
  2. 暂时更新页面并显示保存中。
  3. 仓库保存成功后提示完成。
  4. 保存失败则恢复旧计划,或明确显示"仅本次会话有效"。

用户看到的 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。

二十六、落地顺序

  1. 先引入 dateKey 与 weekId,替换硬编码日期。
  2. 给 MealPlanItem 增加 id、slot、recipeId 和 portions。
  3. 把替换索引改为 dateKey + mealId。
  4. 实现营养汇总,删除固定数字。
  5. 修复生成候选的标签和时长规则,并避免重复。
  6. 让仓库返回保存结果,增加旧数据迁移。
  7. 最后补周切换、撤销和多设备布局。

每一步都能单独验证,避免日期、存储和 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 掩盖失败。

当用户在周三替换晚餐、下周回来仍能看到正确日期与营养变化时,这个页面才真正完成了"计划"而不只是"展示"。

参考资料

AI 辅助声明:本文由 AI 辅助完成源码统计、结构校对与配图设计,当前能力结论均以文中列出的本地 ArkTS 源码为依据,并由作者复核后发布;建议模型、迁移方案和测试矩阵不代表工程已经构建或上线。

相关推荐
贾伟康1 小时前
【健康菜谱助手|02】HarmonyOS ArkTS 烹饪步骤实战:把分步操作、完成状态和返回恢复做稳
软件测试·移动开发·harmonyos·arkts·arkui
AI浩1 小时前
Xiaomi-Lens-0 技术报告
人工智能·计算机视觉·1024程序员节
用户29469405448162 小时前
DSH Desktop - HarmonyOS Edition
harmonyos·deepseek
OH_TPC2 小时前
【鸿蒙优选三方库】@ohos/ffmpeg-kit:把 FFmpeg 的全部能量带进鸿蒙
华为·ffmpeg·harmonyos·鸿蒙
2601_967980112 小时前
Python之wxPython模块的安装
linux·windows·python·wxpython·gui设计·1024程序员节
minglie13 小时前
电枢的主题和概念
学习·1024程序员节
2601_967987374 小时前
你们都用Python实现了哪些自动化办公?
python·excel·自动化办公·1024程序员节·学习资料·跨平台操作
贾伟康4 小时前
【健康菜谱助手|03】HarmonyOS ArkTS 冰箱食材推荐实战:从本地食材选择生成可解释的候选菜谱
软件测试·移动开发·harmonyos·arkts·arkui·1024程序员节
Liaiyang664 小时前
空圈容错视角 :AI 几乎就是一个“天然的小空圈”
人工智能·大模型·1024程序员节·认知科学·ai幻觉·自我意识·空圈容错理论