长表单拆成多步骤是提升填写完成率的关键 UX 手段。本文用 ArkUI 构建一个三步文章发布流程——自定义步骤指示器、每步独立校验、"上一步/下一步"导航,以及确认页面的一键发布。


一、我们要做什么

一个"发布文章"的三步表单:

  1. 第1步:基本信息 — 输入标题(必填,最多 30 字)+ 选择分类(5 个标签,点击选中/取消)
  2. 第2步:填写内容 — TextArea 输入正文(必填,不少于 10 个字符,最多 500 字)
  3. 第3步:确认发布 — 只读汇总(标题、分类、内容预览),点击"发布"弹窗确认

交互点:

  1. 步骤指示器 — 顶部三个圆点 + 连接线,已完成绿色对勾,当前蓝色数字,未完成灰色
  2. 上一步/下一步 — 底部按钮根据当前步骤动态切换,第 1 步只有"下一步",第 3 步只有"发布"
  3. 每步校验 — 第 1 步:标题非空 + 分类已选;第 2 步:内容 ≥ 10 字;校验不通过 → Toast 提示,不跳转
  4. 发布确认 — 第 3 步弹窗确认 → 发布成功 → 重置全部状态回到第 1 步

二、状态管理:一个 currentStep 掌控全局

@State currentStep: number = 1;
@State title: string = '';
@State category: string = '';
@State content: string = '';

整个三步表单只有 4 个 @StatecurrentStep 决定当前显示哪一步的内容,title / category / content 是三步共享的表单数据。

为什么三个步骤的数据放在同一个页面而不是拆成三个独立页面?因为步骤表单的数据是有依赖关系的——第 3 步需要汇总第 1 步和第 2 步的数据。如果拆成三个页面,需要通过路由参数传递这些数据,增加不必要的复杂性。

表单数据的持久性 — 用户在第 1 步填了标题 → 点"下一步"到第 2 步 → 后悔了,点"上一步"回第 1 步 → 标题还在。因为 currentStep 只是切换了内容区域的可见性,并没有销毁状态。数据一直保存在 @State 中。


在这里插入图片描述

三、交互点1:步骤指示器

@Builder
stepDot(step: number, label: string) {
  Column() {
    Row() {
      if (step < this.currentStep) {
        Text('✓')                       // 已完成:对勾
          .fontSize(14)
          .fontColor(Color.White)
      }
      if (step === this.currentStep) {
        Text(`${step}`)                  // 当前步骤:数字
          .fontSize(14)
          .fontWeight(FontWeight.Bold)
          .fontColor(Color.White)
      }
      if (step > this.currentStep) {
        Text(`${step}`)                  // 未完成:灰色数字
          .fontSize(14)
          .fontColor(AppColors.TEXT_DISABLED)
      }
    }
    .width(28).height(28)
    .borderRadius(14)                    // 圆形
    .backgroundColor(
      step < this.currentStep ? AppColors.PRIMARY :     // 已完成:蓝色
      (step === this.currentStep ? AppColors.PRIMARY : '#E8E8E8')  // 当前:蓝色 / 未完成:灰色
    )
    .justifyContent(FlexAlign.Center)

    Text(label)
      .fontSize(FontSize.CAPTION)
      .fontColor(step <= this.currentStep ? AppColors.PRIMARY : AppColors.TEXT_DISABLED)
      .fontWeight(step === this.currentStep ? FontWeight.Medium : FontWeight.Regular)
      .margin({ top: Spacing.SM })
  }
  .alignItems(HorizontalAlign.Center)
}

三个视觉状态:

状态 圆圈 文字
已完成(step < current) 蓝色实心 + 白色 ✓ 蓝色
当前(step === current) 蓝色实心 + 白色数字 蓝色加粗
未开始(step > current) 灰色实心 + 灰色数字 灰色常规

连接线

@Builder
stepLine(from: number) {
  Row()
    .width(40).height(2)
    .backgroundColor(from < this.currentStep ? AppColors.PRIMARY : '#E8E8E8')
    .margin({ left: Spacing.XS, right: Spacing.XS, bottom: Spacing.XXL })
}

from 是起点步骤的编号(1 或 2)。连接线 width 40vp + 左右 margin XS(4vp) ≈ 48vp 间距。线在圆圈的水平中间位置,margin-bottom: Spacing.XXL(24vp) 让线和圆圈的底部有一个固定的视觉间距。


在这里插入图片描述

四、交互点2:步骤导航与校验

4.1 "下一步"校验

private canNextStep1(): boolean {
  return this.title.trim().length > 0 && this.category.length > 0;
}

private canNextStep2(): boolean {
  return this.content.trim().length >= 10;
}

private goNext(): void {
  if (this.currentStep === 1) {
    if (!this.canNextStep1()) {
      promptAction.showToast({ message: '请填写标题并选择分类', duration: 1500 });
      return;
    }
    this.currentStep = 2;
  } else if (this.currentStep === 2) {
    if (!this.canNextStep2()) {
      promptAction.showToast({ message: '内容不少于10个字符', duration: 1500 });
      return;
    }
    this.currentStep = 3;
  }
}

每步的校验逻辑独立封装为 canNextStep1() / canNextStep2()。校验不通过 → Toast 提示 → return 阻止跳转。校验通过 → currentStep++ 切换到下一步。

为什么不在点"下一步"前就显示校验错误? 因为步骤表单的语义是"先填完这一步,再进入下一步"。在用户还没点"下一步"之前,不判断他"填得对不对"——他可能正在填写中。过早的校验错误只会让用户焦虑。

4.2 “上一步”

private goPrev(): void {
  if (this.currentStep > 1) {
    this.currentStep--;
  }
}

上一步不需要校验——用户回头修改是正常的。只需要 currentStep-- 回到前一页,之前填的数据原封不动。

4.3 底部按钮的动态切换

if (this.currentStep > 1) {
  Text('上一步')         // 第 2、3 步显示
    .border({ width: 1, color: AppColors.BORDER })
    .borderRadius(9999)
    .onClick(() => this.goPrev())
}

if (this.currentStep < 3) {
  Text('下一步')         // 第 1、2 步显示
    .backgroundColor(AppColors.PRIMARY)
    .borderRadius(9999)
    .onClick(() => this.goNext())
}

if (this.currentStep === 3) {
  Text('发布')           // 仅第 3 步显示
    .backgroundColor(AppColors.ERROR)
    .borderRadius(9999)
    .onClick(() => this.publish())
}

三个按钮通过 if 条件渲染,"上一步"和"下一步"不会同时出现在第 1 步或第 3 步。"发布"按钮用红色(AppColors.ERROR)——发布是破坏性的"提交"操作,红色在视觉上让用户审慎对待。


在这里插入图片描述

五、交互点3:发布确认与重置

private publish(): void {
  promptAction.showDialog({
    title: '确认发布',
    message: `标题:${this.title}\n分类:${this.category}\n内容长度:${this.content.length}`,
    buttons: [
      { text: '取消', color: AppColors.TEXT_TERTIARY },
      { text: '发布', color: AppColors.PRIMARY }
    ]
  }).then((result) => {
    if (result.index === 1) {
      this.title = '';
      this.category = '';
      this.content = '';
      this.currentStep = 1;
      promptAction.showToast({ message: '发布成功!', duration: 1500 });
    }
  });
}

弹窗再次确认来自用户的操作——在第 3 步已经看到了全部信息,最后一步用弹窗做最终确认。result.index === 1 表示用户点了"发布"。

发布成功后:

  • 清空三个表单字段
  • 回到第 1 步
  • Toast 反馈

这模拟了"发布完成,可以重新填写下一篇"的完整流程闭环。


六、第3步的只读汇总

this.summaryRow('标题', this.title)
this.summaryRow('分类', this.category)
// 内容预览
Text(this.content)
  .maxLines(5)
  .textOverflow({ overflow: TextOverflow.Ellipsis })
@Builder
summaryRow(label: string, value: string) {
  Row() {
    Text(label)
      .fontSize(FontSize.BODY)
      .fontColor(AppColors.TEXT_TERTIARY)
      .width(60)
    Text(value)
      .fontSize(FontSize.BODY)
      .fontColor(AppColors.TEXT_PRIMARY)
      .fontWeight(FontWeight.Medium)
      .layoutWeight(1)
  }
}

summaryRow 是一个统一的键值对展示组件——左边灰色标签固定 60vp 宽度,右边黑色值占据剩余空间。内容预览用 maxLines(5) 限制高度,超出显示省略号——用户在确认页不需要读完整内容,只需要确认"是自己刚才写的"。


七、第1步的分类标签

Flex({ wrap: FlexWrap.Wrap }) {
  ForEach(CATEGORIES, (item: string) => {
    Text(item)
      .fontColor(this.category === item ? Color.White : AppColors.TEXT_SECONDARY)
      .backgroundColor(this.category === item ? AppColors.PRIMARY : AppColors.BACKGROUND)
      .borderRadius(BorderRadius.SM)
      .padding(...)
      .onClick(() => {
        this.category = this.category === item ? '' : item;  // 点击切换
      })
  })
}

5 个分类标签,用 Flex({ wrap: FlexWrap.Wrap }) 包裹——超过一行自动换行。Row 不支持换行,Flex 支持。这是 ArkUI 布局的一个重要区别。

点击行为:this.category === item ? '' : item——选中后再次点击可取消。这是一个轻量的单选交互,比 RadioGroup 更直观。


八、页面结构总结

StepperPage (~300 行)
├── 状态层
│   ├── @State currentStep: number   — 当前步骤 (1/2/3)
│   ├── @State title: string         — 文章标题
│   ├── @State category: string      — 文章分类
│   └── @State content: string       — 文章内容
├── 校验方法
│   ├── canNextStep1()               — 标题 + 分类校验
│   └── canNextStep2()               — 内容 ≥ 10 字校验
├── 导航方法
│   ├── goNext()                     — 校验通过 → 下一步
│   └── goPrev()                     — 无校验 → 上一步
├── 业务方法
│   └── publish()                    — 弹窗确认 → 发布 → 重置
├── 步骤指示 Builder
│   ├── stepDot(step, label)         — 圆形步骤标记
│   └── stepLine(from)               — 连接线
├── 步骤内容 Builder
│   ├── stepContent1()               — 标题输入 + 分类选择
│   ├── stepContent2()               — 内容 TextArea + 字数
│   └── stepContent3()               — 只读汇总 + 发布
└── UI
    ├── Header
    ├── 步骤指示器 (3 个 stepDot + 2 条 stepLine)
    ├── 步骤内容 (条件渲染)
    └── 底部导航按钮

九、常见面试题 / 踩坑点

9.1 Flex vs Row 什么时候用哪个?

  • Row — 单行排列,子元素不换行。适合固定数量的元素(如导航栏的按钮)
  • Flex + wrap — 多行排列,子元素超出容器宽度自动换行。适合数量不固定的标签/芯片

常见错误:在 Row 中放了 7-8 个 Chip/Tag,期望换行,但 Row 不支持 wrap。改用 Flex({ wrap: FlexWrap.Wrap })

9.2 第三步为什么是"只读"而不是"可编辑"?

步骤表单有两种常见模式:

  • 确认模式(本 Demo)— 第 3 步只读展示,不能修改。要改 → 点"上一步"回到对应步骤
  • 全文模式 — 第 3 步可以编辑所有字段,等同于一个长表单

确认模式的优点:每步职责单一,校验逻辑不重复(只在第 1、2 步校验,第 3 步不需要)。全文模式适合"用户不喜欢来回跳"的场景,但校验逻辑需要集中在最后一步。

9.3 步骤表单的数据会在"上一步"时丢失吗?

不会。@State 变量不会被 if (currentStep === n) 的条件渲染销毁。数据一直保存在组件中,切换步骤只是切换了内容的可见性。

但如果某个步骤的内容过于复杂(如富文本编辑器),可以考虑用 visibility.offset 隐藏而不是 if 条件渲染——避免重建组件的开销。

9.4 为什么第 2 步的字符数提示用红色/灰色切换?

Text(this.content.length < 10
  ? `还差${10 - this.content.length}个字符`
  : `${this.content.length}/500`)
  .fontColor(this.content.length < 10 ? AppColors.ERROR : AppColors.TEXT_DISABLED)

少于 10 字时用红色提示"还差 N 个字符"——这是一个实时校验反馈,让用户知道"为什么不让我点下一步"。达到 10 字后变回灰色,提示消失。这种"即时反馈"可以减少用户点击"下一步"后被 Toast 拒绝的挫败感。

9.5 步骤指示器的 step < this.currentStep 判断为什么用 < 而不是 <=

三种状态的覆盖逻辑:

  • step < currentStep → 已完成(step 1 < current 2 → 第 1 步已完成)
  • step === currentStep → 当前
  • step > currentStep → 未完成

< 确保了第 3 步(currentStep=3)时,前两步都显示为"已完成"(对勾)。


十、运行方式

代码位于 dev/entry/src/main/ets/pages/StepperPage.ets

用 DevEco Studio 打开 dev/ 项目,首页点击"步骤表单 — 三步发布文章与进度指示"即可体验:

  1. 进入页面 → 第 1 步,步骤指示器显示蓝色"1",输入标题 + 选择分类
  2. 点击"下一步" → 进入第 2 步(如未填 → Toast 提示)
  3. 第 2 步输入内容,字数不足 10 时显示红色提示"还差 N 个字符"
  4. 点击"下一步" → 进入第 3 步,查看汇总信息
  5. 点"上一步"回到第 2 步修改 → 再回来,数据不变
  6. 第 3 步点击"发布" → 弹窗确认 → 发布成功 → 回到第 1 步(已清空)

十一、扩展方向

  • 步骤指示器动画 — 步骤切换时有平滑的颜色过渡(animateTo),对勾出现时有缩放动画
  • 保存草稿 — 用 Preferences 持久化表单数据,用户退出页面后回来可以继续填写
  • 条件步骤 — 根据第 1 步的分类选择,动态决定是否需要第 2 步(如"转载"分类跳过内容填写)
  • 步骤校验前置 — 第 1 步实时校验标题长度和分类,不等到点"下一步"才报错
  • 长表单拆分策略 — 超过 10 个字段的长表单,自动拆分为 3-4 步,每步 2-3 个字段
  • 进度百分比 — 在步骤指示器下方显示"已完成 66%",给用户明确的目标感
  • 步骤间数据依赖 — 第 2 步的选项列表根据第 1 步的选择动态变化(如选了"技术文章"分类 → 第 2 步出现"技术标签"字段)
Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐