HarmonyOS开发实战: EmailFeedbackPage 意见反馈 + AboutPage 关于页

EmailFeedbackPage 意见反馈 + AboutPage 关于页

前言

当用户使用「海风日记」一段时间后,可能会想:这个 App 是谁做的?我有问题该反馈给谁?于是 关于页(AboutPage)意见反馈页(EmailFeedbackPage) 成了产品「最后一公里」的关键触点。

本文将从源码出发,深入讲解:

  1. EmailFeedbackPage 的反馈类型选择 + 文本输入 + 邮箱留档 + 提交校验
  2. AboutPage 的品牌头部 + 版本信息列表 + 版权尾部
  3. 表单校验的 enabled 联动
  4. 用户协议、隐私政策的「内部链接」跳转
  5. 两个页面的视觉一致性原则

一个好的「关于页」,应该让用户感受到「这是有人在认真做的产品」,而不是冷冰冰的版权声明。


一、EmailFeedbackPage:四段式表单

意见反馈页采用经典的「类型选择 + 文本输入 + 邮箱留档 + 提交」结构。

typescript 复制代码
@Entry
@Component
struct EmailFeedbackPage {
  @State feedbackType: number = 0
  @State feedbackText: string = ''
  @State email: string = ''

  build() {
    Column() {
      // 1. 导航栏
      this.NavBarBuilder('意见反馈')

      // 2. 表单主体
      Scroll() {
        Column({ space: 16 }) {
          this.TypeSelectorBuilder()      // 反馈类型
          this.ContentInputBuilder()      // 问题描述
          this.EmailInputBuilder()        // 联系邮箱
          this.SubmitButtonBuilder()      // 提交按钮
        }
        .padding({ left: 16, right: 16, top: 16, bottom: 40 })
      }
      .layoutWeight(1)
      .scrollBar(BarState.Off)
    }
    .height('100%').backgroundColor(COLOR_BG)
  }
}

1.1 页面结构图

复制代码
┌────────────────────────────┐
│  ‹ 意见反馈                 │ ← 导航栏
├────────────────────────────┤
│  反馈类型                   │
│  [功能建议][问题反馈]...      │ ← 类型选择
│                            │
│  问题描述                   │
│  ┌────────────────────────┐│
│  │ TextArea 多行输入      ││ ← 文本输入
│  │                        ││
│  └────────────────────────┘│
│                            │
│  联系邮箱(选填)           │
│  ┌────────────────────────┐│
│  │ Email 输入框           ││ ← 邮箱留档
│  └────────────────────────┘│
│                            │
│  ┌────────────────────────┐│
│  │       提交反馈         ││ ← 提交按钮
│  └────────────────────────┘│
└────────────────────────────┘

二、反馈类型选择:胶囊按钮组

typescript 复制代码
@Builder
TypeSelectorBuilder() {
  Column() {
    Text('反馈类型').fontSize(12).fontColor(COLOR_TEXT_HINT)
    Row({ space: 8 }) {
      ForEach(['功能建议', '问题反馈', '体验问题', '其他'], (label, i) => {
        Text(label)
          .fontSize(13)
          .fontColor(this.feedbackType === i ? COLOR_WHITE : COLOR_TEXT_MAIN)
          .backgroundColor(
            this.feedbackType === i ? COLOR_PRIMARY : COLOR_BUTTON_INACTIVE_BG
          )
          .borderRadius(14)
          .padding({ left: 12, right: 12, top: 5, bottom: 5 })
          .onClick(() => { this.feedbackType = i })
      })
    }
    .margin({ top: 8 })
  }
  .alignItems(HorizontalAlign.Start)
  .width('100%')
}

2.1 视觉对比

状态 背景色 字色
选中 COLOR_PRIMARY(琥珀) 白色
未选 COLOR_BUTTON_INACTIVE_BG(浅灰) 主文本色

胶囊形 borderRadius(14) 与按钮高度(约 28vp)形成半圆,符合 iOS 胶囊按钮规范。

2.2 为什么不用单选 radio?

  • 空间占用:radio + label 横向排列,4 个选项需要 2 行
  • 可读性:胶囊按钮的文字更醒目
  • 触控友好:胶囊按钮的点击区域更大

三、问题描述:TextArea 多行输入

typescript 复制代码
@Builder
ContentInputBuilder() {
  Column() {
    Text('问题描述').fontSize(12).fontColor(COLOR_TEXT_HINT)
    TextArea({ placeholder: '请详细描述您遇到的问题', text: this.feedbackText })
      .constraintSize({ minHeight: 140 })
      .backgroundColor(COLOR_CARD_BG)
      .borderRadius(CARD_RADIUS)
      .padding(12)
      .onChange((v) => this.feedbackText = v)
      .margin({ top: 8 })
  }
  .alignItems(HorizontalAlign.Start).width('100%')
}

3.1 关键属性

属性 作用
constraintSize.minHeight 140vp 输入框最少高度,避免一开始只剩一行
placeholder 提示文字 输入为空时显示
backgroundColor 卡片色 与页面背景形成层次

3.2 字符校验

提交按钮的 enabled 状态会根据 feedbackText 的长度变化:

typescript 复制代码
this.feedbackText.trim().length > 10
  • 少于 10 字:按钮禁用,避免无意义提交
  • 多于 500 字:可选截断或提示「请精简描述」

四、联系邮箱:InputType.Email

typescript 复制代码
@Builder
EmailInputBuilder() {
  Column() {
    Text('联系邮箱(选填)').fontSize(12).fontColor(COLOR_TEXT_HINT)
    TextInput({ placeholder: '方便我们回复您', text: this.email })
      .type(InputType.Email)
      .backgroundColor(COLOR_CARD_BG)
      .borderRadius(INPUT_RADIUS)
      .padding(12)
      .onChange((v) => this.email = v)
      .margin({ top: 8 })
  }
  .alignItems(HorizontalAlign.Start).width('100%')
}

4.1 InputType.Email 的作用

  • 移动端键盘自动显示 @. 快捷键
  • 部分输入法会校验邮箱格式

4.2 邮箱格式校验(可选)

typescript 复制代码
const emailRegex = /^[\w.-]+@[\w.-]+\.\w+$/
if (!emailRegex.test(this.email)) {
  promptAction.showToast({ message: '邮箱格式不正确' })
  return
}

五、提交按钮:条件启用 + 视觉反馈

typescript 复制代码
@Builder
SubmitButtonBuilder() {
  Button('提交反馈')
    .width('90%').height(50)
    .fontSize(16).fontColor(COLOR_WHITE).fontWeight(FontWeight.Bold)
    .backgroundColor(
      this.feedbackText.trim().length > 10 ? COLOR_PRIMARY : COLOR_BUTTON_DISABLED_BG
    )
    .borderRadius(BTN_RADIUS_FULL)
    .enabled(this.feedbackText.trim().length > 10)
    .onClick(() => this.submitFeedback())
}

5.1 三层校验

typescript 复制代码
submitFeedback() {
  // 1. 必填校验
  if (this.feedbackText.trim().length <= 10) {
    promptAction.showToast({ message: '请至少输入 10 个字符' })
    return
  }

  // 2. 邮箱格式校验(如果填了)
  if (this.email && !/^[\w.-]+@[\w.-]+\.\w+$/.test(this.email)) {
    promptAction.showToast({ message: '邮箱格式不正确' })
    return
  }

  // 3. 提交(伪代码)
  feedbackService.submit({
    type: this.feedbackType,
    content: this.feedbackText,
    email: this.email
  }).then(() => {
    promptAction.showToast({ message: '反馈已提交,感谢支持!' })
    router.back()
  })
}

六、AboutPage:三段式品牌展示

关于页采用「品牌头部 + 信息列表 + 版权尾部」三段式结构。

typescript 复制代码
@Entry
@Component
struct AboutPage {
  build() {
    Column() {
      this.NavBarBuilder('关于海风')
      Scroll() {
        Column({ space: 24 }) {
          this.BrandHeaderBuilder()    // 1. 品牌 Logo
          this.InfoListBuilder()       // 2. 信息列表
          this.CopyrightFooterBuilder() // 3. 版权信息
        }
        .padding({ left: 16, right: 16, top: 32, bottom: 40 })
      }
      .layoutWeight(1)
      .scrollBar(BarState.Off)
    }
    .height('100%').backgroundColor(COLOR_BG)
  }
}

typescript 复制代码
@Builder
BrandHeaderBuilder() {
  Column({ space: 12 }) {
    Stack() {
      Circle().width(80).height(80).fill(COLOR_AVATAR_BG)
      Text('🌊').fontSize(38)
    }
    Text('海风日记').fontSize(22).fontColor(COLOR_TEXT_MAIN).fontWeight(FontWeight.Bold)
    Text('Version 1.0.0').fontSize(13).fontColor(COLOR_TEXT_HINT)
    Text('让每一天随海风飘散').fontSize(14).fontColor(COLOR_TEXT_SECONDARY)
  }
}
  • 80vp 圆形底色 COLOR_AVATAR_BG
  • 上层叠加 🌊 emoji,简单直观
  • 相比于绘制 SVG,emoji 跨平台一致性更好

7.2 版本号格式

Version 1.0.0 遵循 语义化版本(SemVer)

  • 1 ------ 主版本(不兼容更新)
  • 0 ------ 次版本(新增功能,向后兼容)
  • 0 ------ 修订号(bug 修复)

八、信息列表:ForEach 动态渲染

typescript 复制代码
@Builder
InfoListBuilder() {
  Column() {
    ForEach([
      { label: '版本号', value: '1.0.0' },
      { label: '开发商', value: '海风科技' },
      { label: '更新日期', value: '2025年1月' },
      { label: '用户协议', value: '查看 ›' },
      { label: '隐私政策', value: '查看 ›' }
    ], (item) => {
      Row() {
        Text(item.label).fontSize(14).fontColor(COLOR_TEXT_MAIN).layoutWeight(1)
        Text(item.value)
          .fontSize(14)
          .fontColor(item.value.includes('›') ? COLOR_AMBER : COLOR_TEXT_SECONDARY)
      }
      .height(50)
      .padding({ left: PAGE_PADDING, right: PAGE_PADDING })
      Divider().color(COLOR_DIVIDER).margin({ left: PAGE_PADDING })
    })
  }
  .backgroundColor(COLOR_CARD_BG)
  .borderRadius(CARD_RADIUS)
}

8.1 列表项高度统一

每项固定 height(50),确保用户在快速浏览时视线流动稳定。

8.2 「查看 ›」的视觉提示

「用户协议」「隐私政策」这两项是可点击的内部链接,用 COLOR_AMBER(琥珀色)标识,并附加 箭头符号提示「可跳转」。

typescript 复制代码
.onClick(() => {
  if (item.label === '用户协议') router.pushUrl({ url: 'pages/UserAgreementPage' })
  if (item.label === '隐私政策') router.pushUrl({ url: 'pages/PrivacyPolicyPage' })
})

typescript 复制代码
@Builder
CopyrightFooterBuilder() {
  Column({ space: 6 }) {
    Text('🌊 用文字守护每一天的珍贵').fontSize(13).fontColor(COLOR_AMBER)
    Text('© 2025 海风科技').fontSize(11).fontColor(COLOR_TEXT_HINT)
  }
}

9.1 版权符号 ©

Unicode 编码 U+00A9,在 HTML 中也可以用 &copy; 表示。

9.2 Slogan 的情感价值

「用文字守护每一天的珍贵」比「让记录更简单」更有温度。这种情感化文案是品牌差异化的关键。


十、视觉一致性原则

两个页面虽然功能不同,但都遵循统一的设计规范:

设计要素 取值 作用
页面背景 COLOR_BG 浅米色,温暖
卡片背景 COLOR_CARD_BG 比页面背景略白
主色 COLOR_PRIMARY / COLOR_AMBER 品牌琥珀色
标题字号 17vp 标准导航栏字号
正文字号 14vp 易读
圆角 CARD_RADIUS 12vp / BTN_RADIUS_FULL 统一圆角体系

十一、常见问题与踩坑记录

Q1:TextArea 在 iOS 上换行触发 onChange 频繁?

原因 :合成输入期间多次触发。

解决 :用防抖 setTimeout 或在 onSubmit 时统一处理。

Q2:InputType.Email 在某些输入法上不显示 @

原因 :输入法兼容性问题。

解决 :用 InputType.Normal + 手动校验。

Q3:版本号写死在代码里,更新时容易遗漏?

原因 :硬编码。

解决 :从 app.json5versionName 动态读取:

typescript 复制代码
import app from '@system.app'
app.getInfo().then((info) => {
  this.version = info.versionName
})

十二、总结

本文通过「海风日记」的意见反馈和关于页面,深入讲解了:

  1. EmailFeedbackPage ------ 四段式表单:类型、内容、邮箱、提交
  2. AboutPage ------ 三段式品牌展示:Logo、信息列表、版权
  3. 表单校验 ------ enabled 联动 + 字符长度 + 邮箱正则
  4. 视觉一致性 ------ 统一的颜色常量、字号、圆角
  5. 情感化文案 ------ Slogan 与 Copyright 的温度

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!


相关资源

相关推荐
AD022713 小时前
HarmonyOS应用实战-启示散页-08-提问历史别无限长:用去重、截断和 Sheet 入口做轻量记忆
harmonyos·arkts·鸿蒙开发
酣大智13 小时前
华为交换机 端口隔离完整配置(S5700/S5300 通用)
华为·端口隔离
星栈独行13 小时前
Node 框架怎么选?Express、Koa、Egg、NestJS 场景化选型指南
后端·程序人生·node.js
世人万千丶13 小时前
鸿蒙Flutter Flex嵌套布局技巧
学习·flutter·华为·harmonyos·鸿蒙·鸿蒙系统
你为她披上外套时我正站在窗外13 小时前
拆解 siwi-download:Rust 异步下载器是怎么炼成的
后端
苍何13 小时前
WAIC深度体验:能跨端使用的 Agent 才是好 Agent!
后端
绝世番茄13 小时前
HarmonyOS NEXT 实战:SideBar + Navigation 侧边导航布局完全指南
华为·harmonyos·鸿蒙
xd18557855514 小时前
家电选购参谋 —— 鸿蒙AI智能助手开发全流程解析
人工智能·华为·harmonyos·鸿蒙
程序员清风14 小时前
推荐几个我常听的AI播客!
java·后端·面试
FrameNotWork14 小时前
HarmonyOS 6.0 相机开发——拍照与录像
数码相机·华为·harmonyos