【寻迹校园 HarmonyOS NEXT 实战 09】三步结构化发布表单:ArkUI 如何降低失物描述成本

这是“寻迹校园 HarmonyOS NEXT 实战”系列第 9 篇。本文结合 PublishTypePagePublishFormPage,拆解丢失/拾得分流、三步字段组织、草稿恢复、Photo Picker、步骤校验和防重复提交。

三步结构化发布表单原创概念图

上图为本文原创生成的三步表单概念图,不是应用截图。表单依次收集基本信息、公开/私密特征和最终确认,减少用户面对长表单时的认知压力。

一、自由文本发布为什么很难匹配

如果发布页只有一个大文本框,用户可能写出:

昨天好像在教学楼丢了一个包,有看到的联系我。

这段话对人类勉强可读,对筛选、匹配和安全核验却很不友好:

  • 不知道是丢失还是拾得;
  • “昨天”会随日期变化;
  • “教学楼”范围过大;
  • “包”没有稳定分类;
  • 公开描述可能夹带联系方式;
  • 没有私密特征用于确认归属。

结构化表单的目标不是让用户多填字段,而是把后续搜索、匹配、认领和审核需要的信息提前组织好。

二、第一步先区分丢失与拾得

发布入口先展示两种业务类型:

  • 我丢了物品:进入丢失信息表单;
  • 我捡到物品:进入拾得信息表单。

PublishTypePage 使用稳定路由跳转,而不是把显示文案当路由 key:

this.typeCard(
  '我丢了物品',
  '发布丢失信息',
  AppIconName.LOST_BAG,
  AppColors.ON_LOST,
  AppColors.LOST_CONTAINER,
  AppRoute.PUBLISH_LOST_FORM
)

类型一旦进入表单就成为 ReportType。编辑已有记录时不允许修改丢失/拾得类型,因为类型变化会影响匹配方向、私密字段和状态机语义。

三、三步表单分别收集什么

项目把长表单拆成三个步骤:

步骤 字段 用户目标
1. 基本信息 名称、分类、校园区域、事件日期、照片 让记录可被筛选和排序
2. 物品特征 公开描述、拾得物私密核验特征 支撑匹配与安全核验
3. 确认发布 汇总类型、分类、地点、日期、照片和描述 在写入前发现错误

步骤文案由一个简单函数统一:

private stepLabel(step: number): string {
  if (step === 1) return '基本信息';
  if (step === 2) return '物品特征';
  return '确认发布';
}

它不是为了炫技,而是让页面层只维护当前步骤,业务规则仍交给 ReportService

四、页面只保存草稿态

PublishFormPage 使用 @Local 保存短生命周期输入:标题、分类、区域、日期、公开描述、私密特征、图片 URI、当前步骤和错误状态。

提交前页面把这些字段组装成 ReportDraft

private createDraft(): ReportDraft {
  return new ReportDraft(
    this.reportType,
    this.title,
    this.category,
    this.area,
    this.description,
    this.privateFeature,
    this.eventDate,
    this.imageUris
  );
}

页面不直接写数据库,也不决定照片如何复制到应用沙箱。草稿只是输入契约,最终校验和持久化由 Service 负责。

表单步骤、草稿与 Service 写入原创流程图

上图展示从类型选择到成功回执的完整链路。每一步只暴露当前需要处理的信息,最终写入只有一个入口。

五、步骤切换要使用完整校验结果

点击“下一步”时,页面调用同一个 validateDraft(),但只检查当前步骤相关字段:

private goNext(): void {
  const validation: PublishValidation = reportService.validateDraft(this.createDraft());
  if (this.currentStep === 1) {
    this.validation = validation;
    if (validation.titleError.length === 0 &&
      validation.categoryError.length === 0 &&
      validation.areaError.length === 0 &&
      validation.eventDateError.length === 0 &&
      validation.photoError.length === 0) {
      this.currentStep = 2;
      this.validation = new PublishValidation();
    }
  }
}

这样避免页面自己维护第二套规则。即使按钮禁用条件遗漏,Service 的完整校验仍是最终门禁。

六、日期必须保存绝对值

“昨天”“上周一”适合自然语言,不适合长期记录。项目通过系统日期选择器保存 YYYY-MM-DD

  • 最早日期限制为项目允许范围;
  • 最晚日期不超过今天;
  • 接受后统一格式化;
  • Service 再次验证日期格式与真实性。

绝对日期可以稳定计算候选相差天数,也不会因为用户第二天打开页面而改变含义。

七、照片是可选输入,但生命周期不能随意处理

页面通过 PhotoPickerService.selectImages(3) 拉起系统选择器,最多接收 3 张图片。选择器取消不是异常,应该给出可理解提示并允许继续填写。

页面只持有返回 URI。真正提交或保存草稿时,Service/Repository 才负责把需要长期保存的内容复制到应用管理位置;失败时还要清理本轮已复制但未被记录引用的文件。

这条边界避免页面同时处理权限、文件复制和数据库事务。

八、为什么拾得物需要私密核验特征

公开描述用于搜索,不能包含所有细节。拾得者可以填写“夹层内有一张特定颜色卡片”等私密特征,只在认领审核时核验。

丢失者的认领证明与拾得记录的私密特征应该分开保存、分开展示。公开列表和 Agent 输入都不应拿到完整私密字段。

项目页面同时提示用户:不要在公开描述中填写手机号、微信号、密码或完整证件号码。私密字段不是“随便放敏感信息的地方”,它同样需要最小化和访问边界。

九、草稿恢复如何降低中途退出成本

用户可能在选择照片、查找日期或思考特征时退出。新建表单出现时,页面通过 reportService.loadDraft(reportType) 恢复当前设备上次保存内容。

草稿恢复需要注意:

  • 丢失和拾得草稿分别保存;
  • 只有确实存在内容时才显示“已恢复”;
  • 编辑已发布记录时不加载新建草稿;
  • 发布成功后清理对应类型草稿;
  • 草稿图片更新后清理不再引用的旧文件;
  • 保存失败显示错误,不假装已落盘。

当前草稿只在单机本地保存,不是账号云同步。

十、防重复提交与底部操作区

异步提交期间必须禁用再次点击:

private async submit(): Promise<void> {
  if (this.saving) return;
  const draft: ReportDraft = this.createDraft();
  this.validation = reportService.validateDraft(draft);
  if (!this.validation.isValid()) return;

  this.saving = true;
  const result = await reportService.createReport(draft);
  this.saving = false;
  // success / error mapping
}

同时还要考虑键盘和安全区:底部“上一步 / 下一步 / 发布”不能被软键盘永久遮挡,小屏和最大字体下按钮文案不能溢出,loading 时布局不能突然跳动。

十一、错误态应该靠近字段,也要有页面级反馈

字段错误应显示在对应输入项附近,例如日期无效、公开描述太短、私密特征缺失。存储失败、Photo Picker 异常等跨字段问题则显示页面级错误。

合理的反馈层级是:

  • 字段错误:告诉用户具体改哪一项;
  • 步骤错误:阻止进入下一步并保留已填内容;
  • 提交错误:恢复按钮状态,允许安全重试;
  • 成功状态:进入独立成功页,提供查看详情和继续匹配入口。

不要只弹一个“参数错误”,也不要在失败后清空整张表单。

十二、验收表单不能只走一条成功路径

至少需要覆盖:

  1. 丢失与拾得两种类型;
  2. 每个必填字段为空或过短;
  3. 未来日期和非法日期;
  4. 公开描述含联系方式;
  5. 拾得物未填写私密特征;
  6. 选择 0、1、3 张照片和取消选择;
  7. 草稿保存、恢复和发布后清理;
  8. 连续点击提交只产生一条记录;
  9. 保存失败后按钮恢复、内容保留;
  10. 最大字体、小屏和底部安全区。

这些场景共同证明表单可交付,而不仅是页面能渲染。

十三、当前方案的边界

本文验证的是单机 HarmonyOS 表单和本地持久化链路。它不证明服务端幂等键、账号级草稿同步、多设备并发编辑、云端内容审核和上传断点续传。

如果接入后端,应增加客户端请求 ID、服务端幂等约束、文件上传状态、草稿版本和冲突合并,而不是继续扩大页面状态。

十四、字段背后的数据所有权不能混乱

表单字段看起来都在一个页面里,但生命周期并不相同。把它们全部当成页面状态,会让恢复、提交和清理互相牵连:

数据 页面持有 Service 负责 Repository 负责
当前步骤、焦点、展开态 短生命周期草稿态 不负责 不负责
标题、分类、区域、日期 编辑中的输入 归一化与校验 保存草稿或正式记录
公开描述、私密特征 输入与错误提示 隐私门禁、业务规则 按访问边界持久化
Photo Picker 返回 URI 临时引用 编排物化与失败处理 复制到沙箱、引用感知清理
发布状态 loading/成功提示 防重复、调用写入 保存唯一权威记录

这张表也定义了修改边界:更换输入组件不应改数据库规则,新增 Repository 字段不能让页面直接拼 SQL,图片复制失败不能由页面猜测哪些文件需要删除。

十五、失败回滚应该保住用户已经输入的内容

提交失败时最糟糕的体验不是出现错误,而是错误发生后草稿、图片和当前步骤全部丢失。安全的失败路径应该满足:

  1. 校验失败不进入图片复制和数据库写入;
  2. 图片复制中途失败,清理本轮新文件,但保留页面原始 URI 与文本;
  3. Repository 写入失败,Service 返回稳定错误分类,页面恢复按钮状态;
  4. 发布成功后才清理对应 ReportType 的草稿;
  5. 用户取消 Photo Picker 时不覆盖原有选择,也不显示红色异常页。

如果一次改动导致发布页异常,可以回滚页面交互实现,但不能直接删除草稿库或整个图片目录。草稿与正式记录可能共享长期文件引用,清理动作必须以 Repository 的引用关系为依据。

十六、建立可复核的发布验收记录

表单验收应同时记录输入、动作、持久化结果和重新打开后的状态。例如“拾得物、3 张照片、有效私密特征”这一用例,不仅要看到成功页,还要返回列表找到新记录、重新打开详情检查公开字段,并确认私密特征没有出现在公开区域。

对于当前项目,还要区分证据边界:代码阅读可以证明最多 3 张和取消分支;拉起选择器并取消可以证明系统入口可用;只有真实选择、重启后再次展示,才能证明临时 URI 物化和冷启动恢复。未执行的层级必须写成 not run,不能用“逻辑已实现”替代运行证据。

十七、本文小结

三步结构化表单把发布任务拆成类型选择、基本信息、公开/私密特征和最终确认。页面只保存草稿态,Service 负责校验与写入,Repository 管理数据和图片生命周期。

下一篇将进一步拆解最终门禁:为什么 maxLength 和按钮禁用不足以保护业务与隐私,以及 ReportService.validateDraft() 应该如何统一输出字段错误。

系列导航:第 9 篇 / 共 50 篇。上一篇:《V1.0 核心应用 + V1.1 小艺增强》;下一篇:《ReportService 的隐私友好校验策略》。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐