欢迎光临
我们一直在努力

HarmonyOS7 ArkUI 电影票务 - 影片选择、场次与座位预订实战:把案例真正写懂

文章目录

      • 前言
      • 业务场景先说清
      • 使用方法
      • 状态和列表怎么配合
      • 核心逻辑拆读
        • 片段 1:`aboutToAppear() {`
        • 片段 2:`generateSeats() {`
      • 必读小结
      • 完整代码
      • 复盘

前言

手绘架构图解:HarmonyOS7 ArkUI 电影票务系统的数据流。左侧为输入数据(@State

这篇我不打算空讲概念,直接贴着代码往下拆。页面怎么跑起来、状态怎么变、交互怎么收口,都会在文章里说清楚。

这个案例的主线是 电影票务,后面的 影片选择、场次与座位预订 则把页面做得更像真实业务页。读代码时,建议把交互、状态和列表这三条线一起看。

业务场景先说清

MovieTicketBookingPage 对应的是 电影票务 – 影片选择、场次与座位预订 这个业务场景。别把它只当成一个 UI 练习页,它真正有价值的地方在于:同一个页面里同时出现了状态切换、列表渲染、条件展示和用户反馈。

观察点在这个案例里的表现
页面主题 电影票务
主要文案 星际探索2, 科幻/冒险, 12.5万, 汤姆·汉克斯, 长安三万里, 动画/历史
常用组件 Button, Column, ForEach, Row, Scroll, Text
适合练习 状态驱动 UI、局部刷新、事件回调、页面结构拆分

使用方法

把完整代码放进 ArkTS 页面文件后,就可以直接在预览器里运行。实际使用时,建议先手动点一遍页面里的主要交互,看状态有没有跟着变化;然后再去对照代码,理解每个 @State 字段到底控制了哪一块区域。

如果你想把这个案例改成自己的版本,我建议先做三件事:换掉模拟数据、调整筛选规则、再把重复结构抽成 Builder 或子组件。顺序别反,先改结构往往最容易把自己绕进去。

状态和列表怎么配合

ArkUI 页面写顺手之后,你会发现很多问题其实都不是样式问题,而是状态没放对位置。这个案例里能直接看到哪些数据是输入、哪些是选择、哪些是列表源,读起来会比较顺。

状态字段类型说明
movies MovieInfo[] 记录当前展示状态或页面选择
showTimes ShowTime[] 布尔状态,控制弹层、模式或显示隐藏
selectedMovieId number 当前选中项,决定内容切换、高亮或过滤结果
selectedShowId number 当前选中项,决定内容切换、高亮或过滤结果
step number 记录当前展示状态或页面选择
selectedSeats string[] 当前选中项,决定内容切换、高亮或过滤结果
seats SeatItem_n1sw[] 记录当前展示状态或页面选择

我自己看这类示例时,会先把 @State 和事件函数圈出来,再去看布局。这样不会被大段 Column、Row 搞乱节奏。

核心逻辑拆读

片段 1:aboutToAppear() {

墨水风格信息图表:展示  的关键状态字段映射。列出

这段代码我建议单独看。它通常负责派生数据、交互动作或者局部渲染逻辑,把这部分抽出去之后,页面主结构会干净很多,后续要改规则也更省事。

aboutToAppear() {
this.generateSeats()
}

片段 2:generateSeats() {

这段代码我建议单独看。它通常负责派生数据、交互动作或者局部渲染逻辑,把这部分抽出去之后,页面主结构会干净很多,后续要改规则也更省事。

generateSeats() {
const rows = 8
const cols = 10
const newSeats: SeatItem_n1sw[] = []
for (let r = 1; r <= rows; r++) {
for (let c = 1; c <= cols; c++) {
const rnd = Math.random()
let status = 'available'
if (rnd < 0.25) status = 'sold'
newSeats.push({ row: r, col: c, status })
}
}
this.seats = newSeats
}

必读小结

适合拿来练手的能力:页面拆分、状态联动、条件渲染、交互反馈。

推荐阅读顺序:先看前言 -> 再跑页面 -> 再看状态表 -> 最后啃完整代码
如果只想快速上手,优先改模拟数据和交互函数,收益最高

手绘流程图:座位生成算法  的逻辑。起始节点为 `aboutToA

我会重点检查的几个地方

  • 点击或输入之后,界面有没有立即更新
  • 列表渲染是否依赖了稳定的数据结构
  • 筛选、统计、派生数据是不是单独放进函数里
  • 是否还有重复布局可以继续抽出去
  • 文案、颜色、间距是不是同一套风格

完整代码

// MovieTicketBookingPage – 电影票务 – 影片选择、场次与座位预订

interface MovieInfo {
id: number
title: string
genre: string
duration: number
rating: number
votes: string
starring: string
color: string
isHot: boolean
price: number
}

interface ShowTime {
id: number
movieId: number
time: string
hall: string
availableSeats: number
totalSeats: number
format: string
}

interface SeatItem_n1sw {
row: number
col: number
status: string
}

@Entry
@Component
struct MovieTicketBookingPage {
@State movies: MovieInfo[] = [
{ id: 1, title: '星际探索2', genre: '科幻/冒险', duration: 148, rating: 9.1, votes: '12.5万', starring: '汤姆·汉克斯', color: '#1a237e', isHot: true, price: 45 },
{ id: 2, title: '长安三万里', genre: '动画/历史', duration: 168, rating: 8.3, votes: '8.7万', starring: '谁发现了这首诗', color: '#c62828', isHot: true, price: 40 },
{ id: 3, title: '消失的她', genre: '悬疑/惊悚', duration: 123, rating: 7.8, votes: '20.1万', starring: '朱一龙 倪妮', color: '#1b5e20', isHot: false, price: 38 },
{ id: 4, title: '封神第一部', genre: '神话/动作', duration: 148, rating: 7.6, votes: '15.3万', starring: '费翔 黄渤', color: '#e65100', isHot: false, price: 42 },
]
@State showTimes: ShowTime[] = [
{ id: 1, movieId: 1, time: '10:30', hall: '1号厅 IMAX', availableSeats: 35, totalSeats: 200, format: 'IMAX 3D' },
{ id: 2, movieId: 1, time: '13:20', hall: '2号厅', availableSeats: 88, totalSeats: 120, format: '2D' },
{ id: 3, movieId: 1, time: '16:00', hall: '3号厅 4DX', availableSeats: 15, totalSeats: 80, format: '4DX' },
{ id: 4, movieId: 1, time: '19:30', hall: '1号厅 IMAX', availableSeats: 42, totalSeats: 200, format: 'IMAX 3D' },
{ id: 5, movieId: 1, time: '22:10', hall: '2号厅', availableSeats: 105, totalSeats: 120, format: '2D' },
]
@State selectedMovieId: number = 1
@State selectedShowId: number = 1
@State step: number = 0
@State selectedSeats: string[] = []
@State seats: SeatItem_n1sw[] = []

aboutToAppear() {
this.generateSeats()
}

generateSeats() {
const rows = 8
const cols = 10
const newSeats: SeatItem_n1sw[] = []
for (let r = 1; r <= rows; r++) {
for (let c = 1; c <= cols; c++) {
const rnd = Math.random()
let status = 'available'
if (rnd < 0.25) status = 'sold'
newSeats.push({ row: r, col: c, status })
}
}
this.seats = newSeats
}

get selectedMovie(): MovieInfo | undefined {
return this.movies.find((m: MovieInfo) => m.id === this.selectedMovieId)
}

get selectedShow(): ShowTime | undefined {
return this.showTimes.find((s: ShowTime) => s.id === this.selectedShowId)
}

findSeat(row: number, col: number): SeatItem_n1sw | undefined {
return this.seats.find((s: SeatItem_n1sw) => s.row === row && s.col === col)
}

toggleSeat(row: number, col: number) {
const key = `${row}${col}`
const seat = this.seats.find((s: SeatItem_n1sw) => s.row === row && s.col === col)
if (!seat || seat.status === 'sold') return

if (this.selectedSeats.includes(key)) {
this.selectedSeats = this.selectedSeats.filter((k: string) => k !== key)
this.seats = this.seats.map((s) => { if (s.row === row && s.col === col) { s.status = 'available' } return s })
} else if (this.selectedSeats.length < 4) {
this.selectedSeats = this.selectedSeats.concat([key])
this.seats = this.seats.map((s) => { if (s.row === row && s.col === col) { s.status = 'selected' } return s })
}
}

getSeatColor(status: string): string {
if (status === 'sold') return '#e0e0e0'
if (status === 'selected') return '#1890ff'
return '#f5f5f5'
}

get totalPrice(): number {
return this.selectedSeats.length * (this.selectedMovie?.price ?? 40)
}

@Builder
MovieList() {
Column({ space: 12 }) {
ForEach(this.movies, (movie: MovieInfo) => {
Row({ space: 12 }) {
Column()
.width(80).height(110)
.borderRadius(8)
.linearGradient({ angle: 160, colors: [[movie.color, 0], ['#000000', 1]] })

Column({ space: 6 }) {
Row({ space: 8 }) {
Text(movie.title)
.fontSize(16).fontWeight(FontWeight.Bold).fontColor('#1a1a1a').layoutWeight(1)
if (movie.isHot) {
Text('🔥热映').fontSize(11).fontColor('#ff4d4f')
}
}
Text(movie.genre).fontSize(12).fontColor('#888888')
Row({ space: 6 }) {
Text(`${movie.rating}`).fontSize(13).fontColor('#fa8c16').fontWeight(FontWeight.Bold)
Text(`${movie.votes}人评价`).fontSize(11).fontColor('#aaaaaa')
}
Text(`主演:${movie.starring}`).fontSize(12).fontColor('#666666').maxLines(1)
Row({ space: 8 }) {
Text(`${movie.duration}分钟`).fontSize(12).fontColor('#aaaaaa')
Blank()
Button('选座购票')
.height(32).fontSize(13)
.backgroundColor('#ff4d4f')
.padding({ left: 14, right: 14 })
.onClick(() => {
this.selectedMovieId = movie.id
this.step = 1
})
}
.width('100%')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
}
.padding(14)
.backgroundColor('#ffffff')
.borderRadius(12)
.border({ width: this.selectedMovieId === movie.id ? 2 : 0, color: '#ff4d4f' })
})
}
}

@Builder
ShowTimeList() {
Column({ space: 16 }) {
Text('今日 6月6日(周六)')
.fontSize(15).fontWeight(FontWeight.Bold).fontColor('#1a1a1a').width('100%')

ForEach(this.showTimes.filter((s: ShowTime) => s.movieId === this.selectedMovieId), (show: ShowTime) => {
Row({ space: 12 }) {
Column({ space: 4 }) {
Text(show.time).fontSize(22).fontWeight(FontWeight.Bold).fontColor('#1a1a1a')
Text(show.hall).fontSize(12).fontColor('#888888')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)

Column({ space: 4 }) {
Text(show.format)
.fontSize(12).fontColor('#1890ff')
.backgroundColor('#e8f4ff')
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.borderRadius(8)
Text(`剩余 ${show.availableSeats}`)
.fontSize(12)
.fontColor(show.availableSeats < 20 ? '#ff4d4f' : '#52c41a')
}
.alignItems(HorizontalAlign.End)

Button('选座')
.height(38).fontSize(14)
.backgroundColor(this.selectedShowId === show.id ? '#ff4d4f' : '#fff0f0')
.fontColor(this.selectedShowId === show.id ? '#ffffff' : '#ff4d4f')
.border({ width: 1, color: '#ff4d4f' })
.padding({ left: 16, right: 16 })
.onClick(() => {
this.selectedShowId = show.id
this.step = 2
this.generateSeats()
})
}
.padding(16)
.backgroundColor('#ffffff')
.borderRadius(12)
.border({ width: this.selectedShowId === show.id ? 2 : 0, color: '#ff4d4f20' })
})
}
}

@Builder
SeatMap() {
Column({ space: 16 }) {
// 荧幕
Column() {
Text('银 幕')
.fontSize(13).fontColor('#aaaaaa').fontWeight(FontWeight.Medium)
.width('100%').textAlign(TextAlign.Center)
}
.width('90%')
.height(28)
.backgroundColor('#e0e0e0')
.borderRadius({ topLeft: 40, topRight: 40 })
.margin({ bottom: 20 })
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)

// 座位图
Column({ space: 6 }) {
ForEach([1, 2, 3, 4, 5, 6, 7, 8], (row: number) => {
Row({ space: 4 }) {
Text(`${row}`).fontSize(10).fontColor('#aaaaaa').width(28).textAlign(TextAlign.Center)
ForEach([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], (col: number) => {
Column()
.width(26).height(22)
.backgroundColor(this.findSeat(row, col) ? this.getSeatColor(this.findSeat(row, col)!.status) : '#f5f5f5')
.borderRadius({ topLeft: 4, topRight: 4 })
.border({ width: 1, color: this.findSeat(row, col)?.status === 'selected' ? '#1890ff' : 'transparent' })
.onClick(() => { if (this.step === 2) this.toggleSeat(row, col) })
})
}
.width('100%')
.justifyContent(FlexAlign.Center)
})
}

// 图例
Row({ space: 20 }) {
Row({ space: 6 }) {
Column().width(16).height(14).backgroundColor('#f5f5f5').borderRadius(3)
Text('可选').fontSize(11).fontColor('#888888')
}
Row({ space: 6 }) {
Column().width(16).height(14).backgroundColor('#1890ff').borderRadius(3)
Text('已选').fontSize(11).fontColor('#888888')
}
Row({ space: 6 }) {
Column().width(16).height(14).backgroundColor('#e0e0e0').borderRadius(3)
Text('已售').fontSize(11).fontColor('#888888')
}
}
.width('100%')
.justifyContent(FlexAlign.Center)
.margin({ top: 8 })

// 已选座位
if (this.selectedSeats.length > 0) {
Column({ space: 12 }) {
Row() {
Text(`已选 ${this.selectedSeats.length} 个座位`)
.fontSize(14).fontWeight(FontWeight.Medium).fontColor('#1a1a1a').layoutWeight(1)
Text(`¥${this.totalPrice}`)
.fontSize(20).fontWeight(FontWeight.Bold).fontColor('#ff4d4f')
}
.width('100%')

Button(`确认选座 共${this.totalPrice}`)
.width('100%').height(48).fontSize(15).backgroundColor('#ff4d4f')
}
.padding(16)
.backgroundColor('#ffffff')
.borderRadius(12)
}
}
.alignItems(HorizontalAlign.Center)
}

build() {
Column({ space: 0 }) {
// 顶部
Row() {
if (this.step > 0) {
Text('←').fontSize(22).fontColor('#1a1a1a')
.onClick(() => {
this.step
if (this.step === 0) this.selectedShowId = 1
})
}
Text(this.step === 0 ? '正在热映' : this.step === 1 ? '选择场次' : '选择座位')
.fontSize(18).fontWeight(FontWeight.Bold).fontColor('#1a1a1a').margin({ left: this.step > 0 ? 12 : 0 })
Blank()
}
.width('100%')
.padding({ left: 16, right: 16, top: 16, bottom: 12 })
.backgroundColor('#ffffff')

Scroll() {
Column({ space: 0 }) {
if (this.step === 0) {
this.MovieList()
} else if (this.step === 1) {
this.ShowTimeList()
} else {
this.SeatMap()
}
Column().height(32)
}
.padding({ left: 16, right: 16, top: 12 })
}
.layoutWeight(1)
.backgroundColor('#f5f7fa')
}
.width('100%')
.height('100%')
.backgroundColor('#f5f7fa')
}
}

复盘

这类案例最值得学的,其实不是某一个控件怎么写,而是页面组织方式。把状态放对、把交互函数收好、把布局压简单,后面你接正式项目时会轻松很多。

赞(0)
未经允许不得转载:171主机测评 » HarmonyOS7 ArkUI 电影票务 - 影片选择、场次与座位预订实战:把案例真正写懂
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址