【寻迹校园 HarmonyOS NEXT 实战 09】三步结构化发布表单:ArkUI 如何降低失物描述成本
【寻迹校园 HarmonyOS NEXT 实战 09】三步结构化发布表单:ArkUI 如何降低失物描述成本
这是“寻迹校园 HarmonyOS NEXT 实战”系列第 9 篇。本文结合
PublishTypePage与PublishFormPage,拆解丢失/拾得分流、三步字段组织、草稿恢复、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 负责。

上图展示从类型选择到成功回执的完整链路。每一步只暴露当前需要处理的信息,最终写入只有一个入口。
五、步骤切换要使用完整校验结果
点击“下一步”时,页面调用同一个 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 异常等跨字段问题则显示页面级错误。
合理的反馈层级是:
- 字段错误:告诉用户具体改哪一项;
- 步骤错误:阻止进入下一步并保留已填内容;
- 提交错误:恢复按钮状态,允许安全重试;
- 成功状态:进入独立成功页,提供查看详情和继续匹配入口。
不要只弹一个“参数错误”,也不要在失败后清空整张表单。
十二、验收表单不能只走一条成功路径
至少需要覆盖:
- 丢失与拾得两种类型;
- 每个必填字段为空或过短;
- 未来日期和非法日期;
- 公开描述含联系方式;
- 拾得物未填写私密特征;
- 选择 0、1、3 张照片和取消选择;
- 草稿保存、恢复和发布后清理;
- 连续点击提交只产生一条记录;
- 保存失败后按钮恢复、内容保留;
- 最大字体、小屏和底部安全区。
这些场景共同证明表单可交付,而不仅是页面能渲染。
十三、当前方案的边界
本文验证的是单机 HarmonyOS 表单和本地持久化链路。它不证明服务端幂等键、账号级草稿同步、多设备并发编辑、云端内容审核和上传断点续传。
如果接入后端,应增加客户端请求 ID、服务端幂等约束、文件上传状态、草稿版本和冲突合并,而不是继续扩大页面状态。
十四、字段背后的数据所有权不能混乱
表单字段看起来都在一个页面里,但生命周期并不相同。把它们全部当成页面状态,会让恢复、提交和清理互相牵连:
| 数据 | 页面持有 | Service 负责 | Repository 负责 |
|---|---|---|---|
| 当前步骤、焦点、展开态 | 短生命周期草稿态 | 不负责 | 不负责 |
| 标题、分类、区域、日期 | 编辑中的输入 | 归一化与校验 | 保存草稿或正式记录 |
| 公开描述、私密特征 | 输入与错误提示 | 隐私门禁、业务规则 | 按访问边界持久化 |
| Photo Picker 返回 URI | 临时引用 | 编排物化与失败处理 | 复制到沙箱、引用感知清理 |
| 发布状态 | loading/成功提示 | 防重复、调用写入 | 保存唯一权威记录 |
这张表也定义了修改边界:更换输入组件不应改数据库规则,新增 Repository 字段不能让页面直接拼 SQL,图片复制失败不能由页面猜测哪些文件需要删除。
十五、失败回滚应该保住用户已经输入的内容
提交失败时最糟糕的体验不是出现错误,而是错误发生后草稿、图片和当前步骤全部丢失。安全的失败路径应该满足:
- 校验失败不进入图片复制和数据库写入;
- 图片复制中途失败,清理本轮新文件,但保留页面原始 URI 与文本;
- Repository 写入失败,Service 返回稳定错误分类,页面恢复按钮状态;
- 发布成功后才清理对应
ReportType的草稿; - 用户取消 Photo Picker 时不覆盖原有选择,也不显示红色异常页。
如果一次改动导致发布页异常,可以回滚页面交互实现,但不能直接删除草稿库或整个图片目录。草稿与正式记录可能共享长期文件引用,清理动作必须以 Repository 的引用关系为依据。
十六、建立可复核的发布验收记录
表单验收应同时记录输入、动作、持久化结果和重新打开后的状态。例如“拾得物、3 张照片、有效私密特征”这一用例,不仅要看到成功页,还要返回列表找到新记录、重新打开详情检查公开字段,并确认私密特征没有出现在公开区域。
对于当前项目,还要区分证据边界:代码阅读可以证明最多 3 张和取消分支;拉起选择器并取消可以证明系统入口可用;只有真实选择、重启后再次展示,才能证明临时 URI 物化和冷启动恢复。未执行的层级必须写成 not run,不能用“逻辑已实现”替代运行证据。
十七、本文小结
三步结构化表单把发布任务拆成类型选择、基本信息、公开/私密特征和最终确认。页面只保存草稿态,Service 负责校验与写入,Repository 管理数据和图片生命周期。
下一篇将进一步拆解最终门禁:为什么 maxLength 和按钮禁用不足以保护业务与隐私,以及 ReportService.validateDraft() 应该如何统一输出字段错误。
系列导航:第 9 篇 / 共 50 篇。上一篇:《V1.0 核心应用 + V1.1 小艺增强》;下一篇:《ReportService 的隐私友好校验策略》。
更多推荐


所有评论(0)