HarmonyOS 实战|中式美食视频拆菜编辑面板:因子状态、草稿卡片与底部操作条

视频拆菜页最难的地方,不是把一段文字拆成几个字段,而是让用户愿意把它改完。很多类似功能做出来以后,看起来像一个结果页:给你一堆识别结果,剩下自己看。中式美食这里更偏向"草稿编辑器":先把视频或字幕整理成一版可修改的菜谱草稿,再让用户逐条确认、补充、保存。
所以这篇重点不放在算法,而放在编辑面板。也就是:因子怎么分类,状态怎么表达,用户怎么保留或删除一条内容,保存以后怎么变成"我的菜谱草稿"。

上图对应的是中式美食里的真实视频拆菜流程:编辑因子、底部保存入口、已保存卡片。它看起来是 UI 问题,但背后其实是数据状态设计。如果状态没拆清楚,页面会很快变成一堆按钮和输入框。

章节位置 内容
页面入口 entry/src/main/ets/view/pages/VideoInsightPage.ets
编辑面板 entry/src/main/ets/view/components/V3VideoEditorPanels.ets
数据模型 entry/src/main/ets/model/entity/VideoRecipeInsight.ets
草稿仓储 entry/src/main/ets/model/repository/VideoInsightRepository.ets
关键状态 draftconfirmeddetecteduncertainedited

本文验证环境

本文对应的是已上架的 HarmonyOS 应用中式美食,视频拆菜页面来自真实工程。

项目 版本与范围
开发工具 DevEco Studio 6.0.0 Release
运行系统 HarmonyOS NEXT / HarmonyOS 6.0
SDK/API HarmonyOS SDK API 20
语言与框架 ArkTS 6.0、ArkUI V2、Stage 模型
核心能力 @ComponentV2@Param@EventTextInputTextAreaForEach
验证方式 输入字幕文本、生成草稿、修改因子、切换状态、保存后回到草稿列表

我验证这块时,不只看页面能不能打开,而是按完整操作走一遍:先输入菜名和一段做菜文字,生成草稿;再修改某条食材、火候或步骤;把不确定内容标出来;最后保存并从历史列表重新打开。

本章导读

中式美食把视频拆菜后的内容叫 VideoRecipeInsight。它不是最终菜谱,而是一份还需要人工确认的草稿。
草稿里最关键的是 factors。每个 VideoFactor 表示一条可编辑信息,比如食材、步骤、火候、成熟信号、容易出错的提醒。每条因子都有自己的状态,用户可以继续改,也可以删掉。

typescript 复制代码
export type VideoInsightStatus = 'draft' | 'confirmed';
export type VideoFactorStatus = 'detected' | 'uncertain' | 'edited';
export type VideoFactorKind = 'ingredient' | 'step' | 'heat' | 'cue' | 'risk';

export interface VideoFactor {
  id: string;
  kind: VideoFactorKind;
  text: string;
  timeLabel: string;
  status: VideoFactorStatus;
}

这个模型的好处是直接。用户看到一条内容,不需要猜它属于哪里;开发者保存时,也不需要从一大段富文本里重新解析。

先把草稿建出来

VideoInsightRepository.createDraft() 负责创建一份拆菜草稿。它会把标题、来源、视频地址、原始文字和初始因子放在一起。

typescript 复制代码
createDraft(title: string, source: string, videoUri: string = '', rawText: string = ''): VideoRecipeInsight {
  const now: number = Date.now();
  const displayTitle: string = title.trim().length > 0 ? title.trim() : '未命名菜谱';
  const cleanedRawText: string = rawText.trim();

  const item: VideoRecipeInsight = {
    id: 'v' + now,
    title: displayTitle,
    source: source.trim(),
    videoUri,
    rawText: cleanedRawText,
    dishName: displayTitle,
    summary: cleanedRawText.length > 0 ? '已按文字整理成这道菜' : '待你整理的菜谱草稿',
    status: 'draft',
    factors: this.seedFactors(now, cleanedRawText),
    createdAt: now,
    updatedAt: now
  };
}

这里有两个细节。
第一,标题为空时必须兜底。用户可能只是想先试一下,不一定一开始就把菜名写完整。
第二,rawText 不为空时会走提取逻辑;如果没有输入文字,也会生成几条默认因子,保证编辑面板不是空的。空页面会让用户不知道下一步怎么做,有默认草稿就能直接改。

因子不是结果,是待确认项

仓储里有一段很实用的规则:从原始文字里按关键词拆因子。比如识别到"食材、材料、调料",就倾向归到 ingredient;识别到"大火、中火、小火、收汁",就归到 heat;识别到"不要、避免、小心、容易",就归到 risk

typescript 复制代码
private pushIfRoom(out: VideoFactor[], seed: number, kind: VideoFactorKind, text: string, timeLabel: string): void {
  const sameKindCount: number = out.filter((it: VideoFactor): boolean => it.kind === kind).length;
  if (sameKindCount >= 5 || out.length >= 18) {
    return;
  }

  const existed: VideoFactor | undefined = out.find((it: VideoFactor): boolean => {
    return it.kind === kind && it.text === text && it.timeLabel === timeLabel;
  });

  if (existed !== undefined) {
    return;
  }

  out.push(this.factorWithIndex(seed, out.length, kind, text, timeLabel));
}

这个限制很关键。视频文字可能很长,如果无脑拆出几十条,用户根本没有耐心一条条看。中式美食给每类最多 5 条、总数最多 18 条,目的就是把编辑量控制在可接受范围。
这不是追求"识别越多越好",而是追求"用户愿意确认完"。

编辑面板如何表达状态

V3VideoFactorListPanel 不直接修改仓储,它通过事件把操作抛出去:

typescript 复制代码
@Event onTextChange: (id: string, text: string) => void = () => {};
@Event onTimeChange: (id: string, timeLabel: string) => void = () => {};
@Event onStatusChange: (id: string, status: VideoFactorStatus) => void = () => {};
@Event onRemove: (id: string) => void = () => {};

这个写法比在组件里直接操作全局数据更干净。面板只负责展示和收集用户动作,真正的数据更新交给页面或 ViewModel。以后如果保存逻辑换成接口、或者加撤销,都不用重写卡片 UI。
每条因子卡片里,用户能改三类东西:

操作 对应字段 作用
改时间 timeLabel 保留视频中的时间线索
改文字 text 修正识别不准、补充做法
改状态 status 标记保留、不确定、已修改

状态显示也做了差异化:不确定的内容用提醒色,已保留或已修改的内容用更稳定的颜色。这样用户不用读完整段文字,也能知道哪些还没处理。

为什么要有"都留着"

编辑面板顶部有一个 onAcceptAll 事件,对应"都留着"这类操作。这个按钮看起来很小,但体验上很重要。
如果系统已经拆得比较准,用户不应该还要一条条点"保留"。一键保留能把人工确认成本降下来。反过来,如果某条不对,用户仍然可以单独改状态或删除。

typescript 复制代码
if (this.pendingCount > 0) {
  Button('都留着')
    .height(32)
    .fontSize(AppFonts.xs)
    .onClick((): void => this.onAcceptAll());
}

这里的设计原则是:批量操作给轻路径,单条编辑给精修路径。两条路都要有,不然功能会变得很累。

新增一条因子怎么做

识别结果一定会漏。比如字幕里没有说"转小火",但用户知道这一步很关键,就需要手动补一条火候提醒。
V3VideoAddFactorPanel 把新增因子拆成三部分:类型、时间、文字。

typescript 复制代码
@Param factorKind: VideoFactorKind = 'ingredient';
@Param factorText: string = '';
@Param factorTime: string = '';
@Event onKindChange: (kind: VideoFactorKind) => void = () => {};
@Event onTextChange: (text: string) => void = () => {};
@Event onTimeChange: (timeLabel: string) => void = () => {};
@Event onAdd: () => void = () => {};

这比让用户在一段大文本里继续追加要清楚。后续展示菜谱时,食材、步骤、火候、风险提醒都已经分好了,不需要再二次解析。

已保存卡片给用户一个结果感

用户点保存之后,如果页面只是弹个 toast,很快就没有上下文了。中式美食用了 V3VideoSavedDishCard,告诉用户这份草稿已经进了我的菜谱。

typescript 复制代码
export struct V3VideoSavedDishCard {
  @Param dishName: string = '';
  @Event onOpenCustomList: () => void = () => {};
}

卡片里保留菜名,并提供去查看入口。这个动作很重要,因为"视频拆菜"不是一次性功能,它最终要和我的菜谱、收藏、笔记这些长期使用场景连接起来。

草稿列表为什么要显示数量和状态

V3VideoLibraryPanel 展示的是已经创建过的拆菜草稿。每条记录会显示菜名、更新时间和状态。

typescript 复制代码
Text(item.status === 'confirmed' ? '已保存' : '待整理')
  .fontSize(10)
  .fontColor(item.status === 'confirmed' ? '#2F5D46' : '#9B2F21');

这类状态不是为了装饰,而是帮助用户快速判断:哪份已经能用了,哪份还需要继续整理。菜谱工具如果没有这种连续性,用户下次回来就很难接上。

工程验收记录

检查项 结果
创建草稿 菜名为空时有兜底标题,页面不空白
原始文字拆分 能按食材、步骤、火候、成熟信号、风险提醒形成因子
因子数量 单类和总数有限制,避免页面过长
单条编辑 时间、文字、状态都能独立修改
删除因子 删除后列表刷新,剩余内容不丢
一键保留 可快速处理全部待确认内容
新增因子 可选择类型、补时间、写内容
保存结果 显示已保存卡片,并能回到草稿列表查看

常见问题复盘

第一个问题是把"识别结果"当成"最终菜谱"。实际使用时,视频字幕经常不完整,用户必须能改。只展示结果不提供编辑,会让这个功能停在玩具阶段。
第二个问题是状态太少。如果只有保存和未保存,用户不知道哪一条是系统识别的、哪一条是自己改过的、哪一条还不确定。detecteduncertainedited 这三个状态虽然简单,但足够支撑编辑流程。
第三个问题是按钮太散。中式美食把批量保留、单条编辑、新增因子、保存结果分成不同面板,不把所有按钮塞进一个卡片里。这样页面更长一点,但每个区域的任务更清楚。

本章小结

中式美食的视频拆菜页不是单纯做一个"识别结果展示页",而是做成了草稿编辑流程。VideoRecipeInsight 负责承载整份草稿,VideoFactor 负责承载每条可确认内容,V3VideoEditorPanels 负责把这些状态变成用户能操作的面板。
这套设计的核心是把不确定性摆到明面上。系统先整理一版,用户再确认、修改、删除、补充,最后保存成自己的菜谱。对菜谱应用来说,这比一次性给一个看起来很完整但不能改的结果,更接近真实使用。

相关推荐
在书中成长2 小时前
HarmonyOS 小游戏《对战五子棋》开发第18篇 - 棋盘设计
算法·harmonyos
m0_749690232 小时前
HarmonyOS 本地备份与系统备份:BackupExtensionAbility、快照导出和恢复
harmonyos
心中有国也有家3 小时前
AtomGit Flutter 鸿蒙客户端:情绪日记的时间线实现
学习·flutter·华为·harmonyos
澹然疏离3 小时前
HarmonyOS应用《民族图鉴》开发第33篇:StorageService——本地存储服务封装
harmonyos
Nturmoils3 小时前
Agent 不只是聊天框:用 ArkUI 做一个鸿蒙任务工作台
harmonyos
GitCode官方3 小时前
开源鸿蒙跨平台直播| RN 三方库开源鸿蒙标准化适配实战分享
华为·开源·harmonyos·atomgit
不才难以繁此生6 小时前
ArkUI 列表性能实战|中式美食菜谱列表如何拆组件、控重绘和稳住滑动体验
harmonyos·arkts·状态管理·foreach·arkui·列表性能·组件拆分
Ww.xh6 小时前
DevEco Studio虚拟机部署指南
华为·harmonyos
yuanlaile6 小时前
前端转 Flutter 踩坑实录:一套可落地 Flutter 鸿蒙 App 完整学习路线,建议收藏!
flutter·harmonyos·flutter开发鸿蒙·跨平台开发app·flutter深入学习