鸿蒙原生应用开发实战(二):添加电影与表单交互 — 电影清单App
·
鸿蒙原生应用开发实战(二):添加电影与表单交互 — 电影清单App
前言
在上一篇文章中我们搭建了项目框架和首页。今天来开发应用的数据录入功能——添加电影页面。这是用户与App交互的第一步,需要良好的表单设计和用户体验。
本文涵盖:
- 表单输入组件(TextInput/TextArea)
- Grid 分类选择器
- 状态切换 Chip 组件
- 表单校验与数据存储
- 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
八、完整数据流
当用户填写完表单点击保存时:
- 表单校验 → 检查名称是否为空
- 构建对象 → 使用
generateId()生成唯一ID - 存储数据 →
AppStorage.set('movies', list) - 页面返回 →
router.back() - 数据刷新 → 首页
onPageShow中重新加载数据
九、ArkTS 严格模式要点
在添加电影页面中,特别注意:
- @Builder 中不能有
let→statusChip使用三元表达式内联判断 - Grid 的 key 生成 → ForEach 需要唯一 key
- 对象字面量 → Movie 对象使用
let movie: Movie = { ... }显式类型
总结
本文完成了添加电影页面的开发:
- ✅ TextInput 表单(名称/年份/导演)
- ✅ InputType.Number 数字键盘
- ✅ Chip 状态切换组件
- ✅ Grid 五列分类选择器
- ✅ 表单校验与 AppStorage 存储
下一篇,我们将开发电影列表页面,实现多维度筛选和搜索功能!

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



所有评论(0)