鸿蒙原生应用开发实战(二):添加电影与表单交互 — 电影清单App

前言

在上一篇文章中我们搭建了项目框架和首页。今天来开发应用的数据录入功能——添加电影页面。这是用户与App交互的第一步,需要良好的表单设计和用户体验。

本文涵盖:

  1. 表单输入组件(TextInput/TextArea)
  2. Grid 分类选择器
  3. 状态切换 Chip 组件
  4. 表单校验与数据存储
  5. 5列 Grid 布局适配

一、页面设计

┌──────────────────────────────────┐
│  < 返回          添加电影        │
├──────────────────────────────────┤
│  电影名称 *                      │
│  ┌────────────────────────────┐  │
│  │ 请输入电影名称             │  │
│  └────────────────────────────┘  │
│                                  │
│  上映年份                        │
│  ┌────────────────────────────┐  │
│  │ 如: 2024                   │  │
│  └────────────────────────────┘  │
│                                  │
│  导演                            │
│  ┌────────────────────────────┐  │
│  │ 请输入导演名               │  │
│  └────────────────────────────┘  │
│                                  │
│  观影状态                        │
│  [👀 想看] [▶️ 在看] [✅ 已看]  │
│                                  │
│  电影分类                        │
│  ┌──┐ ┌──┐ ┌──┐ ┌──┐ ┌──┐     │
│  │💥│ │😂│ │🎭│ │🚀│ │👻│     │
│  │动作│ │喜剧│ │剧情│ │科幻│ │恐怖││
│  └──┘ └──┘ └──┘ └──┘ └──┘     │
│  ┌──┐ ┌──┐ ┌──┐ ┌──┐ ┌──┐     │
│  │❤️│ │🐭│ │🔍│ │📽️│ │🪄│     │
│  └──┘ └──┘ └──┘ └──┘ └──┘     │
│                                  │
│       [  保存到清单  ]           │
└──────────────────────────────────┘

二、表单状态定义

@Entry
@Component
struct AddMovie {
  @State title: string = '';
  @State year: string = '';
  @State director: string = '';
  @State selectedStatus: MovieStatus = MovieStatus.WANT_TO_WATCH;
  @State selectedGenreId: string = '';
  @State genres: Genre[] = [];
}

三、文本输入组件

3.1 电影名称输入

Column() {
  Text('电影名称 *')
    .fontSize(14)
    .fontColor('#999999')
    .width('100%')
  TextInput({ placeholder: '请输入电影名称', text: this.title })
    .fontSize(16)
    .layoutWeight(1)
    .height(44)
    .placeholderColor('#CCCCCC')
    .margin({ top: 4 })
    .onChange((v: string) => { this.title = v; })
}
.width('90%')
.margin({ top: 20, bottom: 16 })

3.2 年份输入

年份使用数字键盘(InputType.Number),方便用户快速输入:

Column() {
  Text('上映年份')
    .fontSize(14)
    .fontColor('#999999')
    .width('100%')
  TextInput({ placeholder: '如: 2024', text: this.year })
    .fontSize(16)
    .layoutWeight(1)
    .height(44)
    .placeholderColor('#CCCCCC')
    .type(InputType.Number)
    .margin({ top: 4 })
    .onChange((v: string) => { this.year = v; })
}
.width('90%')
.margin({ bottom: 16 })

3.3 导演输入

Column() {
  Text('导演')
    .fontSize(14)
    .fontColor('#999999')
    .width('100%')
  TextInput({ placeholder: '请输入导演名', text: this.director })
    .fontSize(16)
    .layoutWeight(1)
    .height(44)
    .placeholderColor('#CCCCCC')
    .margin({ top: 4 })
    .onChange((v: string) => { this.director = v; })
}
.width('90%')
.margin({ bottom: 20 })

四、状态切换 Chip

观影状态使用 Chip 标签组实现,选中标签高亮:

@Builder statusChip(label: string, status: MovieStatus) {
  Text(label)
    .fontSize(14)
    .fontColor(this.selectedStatus === status ? '#FFFFFF' : '#666666')
    .backgroundColor(this.selectedStatus === status ? '#6C63FF' : '#F0F0F0')
    .padding({ left: 14, right: 14, top: 6, bottom: 6 })
    .borderRadius(16)
    .margin({ right: 8 })
    .onClick(() => { this.onStatusClick(status); })
}

使用方式:

Row() {
  this.statusChip('👀 想看', MovieStatus.WANT_TO_WATCH)
  this.statusChip('▶️ 在看', MovieStatus.WATCHING)
  this.statusChip('✅ 已看', MovieStatus.WATCHED)
}
.width('90%')
.margin({ bottom: 20 })

五、Grid 分类选择器

5.1 五列 Grid

使用 columnsTemplate 设置为 '1fr 1fr 1fr 1fr 1fr' 实现5列布局:

Grid() {
  ForEach(this.genres, (g: Genre) => {
    GridItem() {
      Column() {
        Text(g.icon).fontSize(24)
        Text(g.name)
          .fontSize(11)
          .fontColor(this.selectedGenreId === g.id ? '#6C63FF' : '#666666')
          .margin({ top: 2 })
      }
      .width('100%')
      .padding({ top: 8, bottom: 8 })
      .backgroundColor(this.selectedGenreId === g.id ? '#EEEAFF' : '#F5F5F5')
      .borderRadius(10)
      .alignItems(HorizontalAlign.Center)
    }
    .onClick(() => { this.onGenreClick(g.id); })
  }, (g: Genre) => g.id)
}
.columnsTemplate('1fr 1fr 1fr 1fr 1fr')
.columnsGap(6)
.rowsGap(6)
.width('90%')

5.2 选中反馈

  • 未选中:灰色背景 #F5F5F5 + 灰色文字 #666666
  • 选中:浅紫色背景 #EEEAFF + 紫色文字 #6C63FF

视觉反馈让用户清晰知道当前选中哪个分类。

六、表单校验与保存

6.1 校验逻辑

saveMovie(): void {
  // 电影名称为必填项
  if (this.title.trim() === '') {
    return;
  }

  // 年份为空时默认当前年份
  let yearNum = Number.parseInt(this.year);
  if (isNaN(yearNum)) {
    yearNum = new Date().getFullYear();
  }

  // 构建电影对象
  let movie: Movie = {
    id: generateId(),
    title: this.title.trim(),
    year: yearNum,
    director: this.director.trim(),
    rating: 0,
    status: this.selectedStatus,
    isFavorite: false,
    review: '',
    dateAdded: getToday(),
    genreId: this.selectedGenreId
  };

  // 保存到 AppStorage
  let stored = AppStorage.get<Movie[]>('movies');
  let list: Movie[] = stored ? stored : [];
  list.unshift(movie);
  AppStorage.set<Movie[]>('movies', list);

  // 返回上一页
  router.back();
}

6.2 AppStorage 数据流

AddMovie (写入) → AppStorage → Index (读取展示)
                            → ListPage (读取筛选)
                            → DetailPage (读取修改)
                            → ProfilePage (读取统计)

七、Grid 布局适配技巧

7.1 列数选择

5列 Grid 在手机屏幕上能较好地展示10个分类(2行),兼顾信息密度和触摸面积:

1fr 1fr 1fr 1fr 1fr

7.2 间距设置

.columnsGap(6)  // 列间距 6vp
.rowsGap(6)     // 行间距 6vp

八、完整数据流

当用户填写完表单点击保存时:

  1. 表单校验 → 检查名称是否为空
  2. 构建对象 → 使用 generateId() 生成唯一ID
  3. 存储数据AppStorage.set('movies', list)
  4. 页面返回router.back()
  5. 数据刷新 → 首页 onPageShow 中重新加载数据

九、ArkTS 严格模式要点

在添加电影页面中,特别注意:

  1. @Builder 中不能有 letstatusChip 使用三元表达式内联判断
  2. Grid 的 key 生成 → ForEach 需要唯一 key
  3. 对象字面量 → Movie 对象使用 let movie: Movie = { ... } 显式类型

总结

本文完成了添加电影页面的开发:

  • ✅ TextInput 表单(名称/年份/导演)
  • ✅ InputType.Number 数字键盘
  • ✅ Chip 状态切换组件
  • ✅ Grid 五列分类选择器
  • ✅ 表单校验与 AppStorage 存储

下一篇,我们将开发电影列表页面,实现多维度筛选和搜索功能!


在这里插入图片描述

系列目录

  • ✅ 第一篇:项目搭建与首页概览
  • ✅ 第二篇:添加电影与表单交互(本篇)
  • 📝 第三篇:电影列表与搜索筛选
  • 📝 第四篇:电影详情与评分评价
  • 📝 第五篇:个人中心与数据统计
Logo

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

更多推荐