[HarmonyOS NEXT 实战案例八] 电影票务网格布局(下)
[HarmonyOS NEXT 实战案例八] 电影票务网格布局(下)
项目已开源,开源地址: https://gitcode.com/nutpi/HarmonyosNextCaseStudyTutorial , 欢迎fork & star
效果演示
1. 概述
在上一篇教程中,我们学习了如何使用GridRow和GridCol组件实现基本的电影票务网格布局。本篇教程将在此基础上,深入探讨如何优化布局、添加交互功能,以及实现更多高级特性,打造一个功能完善的电影票务应用。
本教程将涵盖以下内容:
- 响应式布局设计
- 电影卡片优化
- 电影详情页实现
- 电影筛选和排序功能
- 购票流程设计
2. 响应式布局设计
2.1 使用断点适配不同屏幕尺寸
在实际应用中,我们需要考虑不同屏幕尺寸的设备,如手机、平板等。GridRow组件提供了断点系统,可以根据屏幕宽度自动调整列数:
GridRow({
columns: { xs: 1, sm: 2, md: 3, lg: 4, xl: 4, xxl: 6 },
gutter: { x: 16, y: 16 }
}) {
// 电影网格内容
}
这样配置后,在不同屏幕宽度下,网格列数会自动调整:
- 极小屏幕(xs):1列
- 小屏幕(sm):2列
- 中等屏幕(md):3列
- 大屏幕(lg)和特大屏幕(xl):4列
- 超大屏幕(xxl):6列
2.2 使用GridCol的span属性实现不同大小的卡片
我们可以使用GridCol的span属性,为热门电影创建更大的卡片:
ForEach(this.movies, (movie: MovieType, index: number) => {
GridCol({
span: index === 0 ? { xs: 1, sm: 2, md: 2, lg: 2 } : { xs: 1, sm: 1, md: 1, lg: 1 }
}) {
// 电影卡片内容
}
})
这样配置后,第一部电影(索引为0)的卡片在小屏幕及以上尺寸会占据2列,其他电影卡片占据1列,形成突出热门电影的效果。
3. 电影卡片优化
3.1 添加阴影和悬浮效果
为电影卡片添加阴影和悬浮效果,提升用户体验:
Column() {
// 电影卡片内容
}
.width('100%')
.backgroundColor(Color.White)
.borderRadius(8)
.shadow({
radius: 4,
color: '#1A000000',
offsetX: 0,
offsetY: 2
})
.stateStyles({
pressed: {
scale: { x: 0.95, y: 0.95 },
opacity: 0.8,
translate: { x: 0, y: 2 }
},
normal: {
scale: { x: 1, y: 1 },
opacity: 1,
translate: { x: 0, y: 0 }
}
})
.animation({
duration: 200,
curve: Curve.EaseOut
})
这段代码为电影卡片添加了以下效果:
- 白色背景和圆角
- 轻微的阴影效果
- 按下时的缩放和位移动画
3.2 添加标签和徽章
为电影卡片添加标签和徽章,显示更多信息:
Stack() {
Image(movie.poster)
.width('100%')
.aspectRatio(0.7)
.borderRadius({ topLeft: 8, topRight: 8 })
// 电影类型标签
Text(movie.type)
.fontSize(12)
.fontColor(Color.White)
.backgroundColor('#FF5722')
.borderRadius(4)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.position({ x: 8, y: 8 })
// IMAX标签(如果适用)
if (movie.isImax) {
Text('IMAX')
.fontSize(12)
.fontColor(Color.White)
.backgroundColor('#1976D2')
.borderRadius(4)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.position({ x: 8, y: 36 })
}
// 3D标签(如果适用)
if (movie.is3d) {
Text('3D')
.fontSize(12)
.fontColor(Color.White)
.backgroundColor('#4CAF50')
.borderRadius(4)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.position({ x: movie.isImax ? 64 : 8, y: movie.isImax ? 36 : 36 })
}
}
这段代码使用Stack组件叠加显示电影海报和各种标签,包括电影类型、IMAX和3D标签。
3.3 添加评分星星
使用星星图标替代数字评分,更加直观:
Row() {
ForEach([1, 2, 3, 4, 5], (i: number) => {
Image(i <= Math.floor(movie.rating / 2) ? $r("app.media.star_filled") : $r("app.media.star_outline"))
.width(12)
.height(12)
.margin({ right: 2 })
})
Text(movie.rating.toFixed(1))
.fontSize(12)
.fontColor('#FFB300')
.margin({ left: 4 })
}
这段代码使用ForEach循环创建5个星星图标,根据电影评分决定显示实心星星还是空心星星,并在右侧显示具体评分数字。
4. 电影详情页实现
4.1 添加状态变量和点击事件
首先,添加状态变量和点击事件处理:
@State showDetail: boolean = false;
@State currentMovie: MovieType | null = null;
// 在电影卡片上添加点击事件
Column() {
// 电影卡片内容
}
.onClick(() => {
this.currentMovie = movie;
this.showDetail = true;
})
4.2 实现电影详情页
build() {
Stack() {
Column() {
// 原有的电影网格布局
}
if (this.showDetail && this.currentMovie) {
this.MovieDetailPage()
}
}
.width('100%')
.height('100%')
}
@Builder
private MovieDetailPage() {
Column() {
// 顶部导航栏
Row() {
Image($r("app.media.ic_back"))
.width(24)
.height(24)
.onClick(() => {
this.showDetail = false;
})
Text('电影详情')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.margin({ left: 16 })
Blank()
Image($r("app.media.ic_share"))
.width(24)
.height(24)
}
.width('100%')
.height(56)
.padding({ left: 16, right: 16 })
.backgroundColor(Color.White)
// 电影详情内容
Scroll() {
Column() {
// 电影海报和基本信息
Stack() {
Image(this.currentMovie.poster)
.width('100%')
.height(240)
.objectFit(ImageFit.Cover)
Column() {
Text(this.currentMovie.title)
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
Row() {
Text(this.currentMovie.type)
.fontSize(14)
.fontColor(Color.White)
.opacity(0.8)
Text(`评分:${this.currentMovie.rating.toFixed(1)}`)
.fontSize(14)
.fontColor(Color.White)
.opacity(0.8)
.margin({ left: 16 })
}
.margin({ top: 8 })
}
.width('100%')
.padding(16)
.alignItems(HorizontalAlign.Start)
.justifyContent(FlexAlign.End)
.backgroundImage({
source: $r("app.media.gradient_bg"),
repeat: ImageRepeat.NoRepeat
})
}
// 电影信息卡片
Column() {
// 导演和主演
Row() {
Text('导演:')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
Text(this.getDirector(this.currentMovie.title))
.fontSize(14)
.fontColor('#666666')
}
.width('100%')
.margin({ top: 8 })
Row() {
Text('主演:')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
Text(this.getActors(this.currentMovie.title))
.fontSize(14)
.fontColor('#666666')
}
.width('100%')
.margin({ top: 8 })
// 上映日期和时长
Row() {
Text('上映日期:')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
Text(this.getReleaseDate(this.currentMovie.title))
.fontSize(14)
.fontColor('#666666')
}
.width('100%')
.margin({ top: 8 })
Row() {
Text('时长:')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
Text(this.getDuration(this.currentMovie.title))
.fontSize(14)
.fontColor('#666666')
}
.width('100%')
.margin({ top: 8 })
}
.width('100%')
.padding(16)
.backgroundColor(Color.White)
.borderRadius(8)
.margin({ top: 16 })
// 电影简介
Column() {
Text('剧情简介')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.margin({ bottom: 8 })
Text(this.getPlot(this.currentMovie.title))
.fontSize(14)
.fontColor('#666666')
.lineHeight(24)
}
.width('100%')
.padding(16)
.backgroundColor(Color.White)
.borderRadius(8)
.margin({ top: 16 })
// 场次选择
Column() {
Text('场次选择')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.margin({ bottom: 16 })
// 日期选择
Row() {
ForEach(this.getDateOptions(), (date: string, index: number) => {
Column() {
Text(date.split(' ')[0])
.fontSize(14)
.fontWeight(FontWeight.Medium)
.fontColor(this.selectedDateIndex === index ? '#FF5722' : '#333333')
Text(date.split(' ')[1])
.fontSize(12)
.fontColor(this.selectedDateIndex === index ? '#FF5722' : '#999999')
.margin({ top: 4 })
}
.width(64)
.height(64)
.backgroundColor(this.selectedDateIndex === index ? '#FFF3E0' : Color.White)
.borderRadius(8)
.justifyContent(FlexAlign.Center)
.border({
width: this.selectedDateIndex === index ? 1 : 0,
color: '#FF5722'
})
.onClick(() => {
this.selectedDateIndex = index;
})
})
}
.width('100%')
.margin({ bottom: 16 })
.justifyContent(FlexAlign.SpaceBetween)
// 场次列表
ForEach(this.getShowtimes(), (showtime: ShowtimeType) => {
Row() {
Column() {
Text(showtime.time)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.fontColor('#333333')
Text(`${showtime.endTime} 散场`)
.fontSize(12)
.fontColor('#999999')
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.Start)
Column() {
Text(showtime.hall)
.fontSize(14)
.fontColor('#666666')
Text(showtime.language)
.fontSize(12)
.fontColor('#999999')
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.Start)
Column() {
Text(`¥${showtime.price}`)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#FF5722')
Text('折扣价')
.fontSize(12)
.fontColor('#999999')
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.Start)
Button('购票')
.width(64)
.height(32)
.fontSize(14)
.backgroundColor('#FF5722')
.borderRadius(16)
.onClick(() => {
this.navigateToSeatSelection(showtime);
})
}
.width('100%')
.padding(16)
.backgroundColor(Color.White)
.borderRadius(8)
.margin({ bottom: 12 })
.justifyContent(FlexAlign.SpaceBetween)
})
}
.width('100%')
.padding(16)
.backgroundColor(Color.White)
.borderRadius(8)
.margin({ top: 16, bottom: 16 })
}
.width('100%')
.padding({ left: 16, right: 16 })
}
.scrollBar(BarState.Off)
.scrollable(ScrollDirection.Vertical)
.width('100%')
.layoutWeight(1)
.backgroundColor('#F5F5F5')
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
这段代码实现了一个完整的电影详情页,包括:
- 顶部导航栏,带有返回按钮和分享按钮
- 电影海报和基本信息
- 电影详细信息,包括导演、主演、上映日期和时长
- 剧情简介
- 场次选择,包括日期选择和场次列表
4.3 辅助方法实现
// 获取导演信息
private getDirector(title: string): string {
// 模拟数据,实际应用中应该从服务器获取
const directors = {
'流浪地球3': '郭帆',
'长安三万里': '史涓生',
'消失的她': '陈思诚',
'封神第一部': '乌尔善'
};
return directors[title] || '未知';
}
// 获取主演信息
private getActors(title: string): string {
// 模拟数据,实际应用中应该从服务器获取
const actors = {
'流浪地球3': '吴京、刘德华、李雪健、沙溢',
'长安三万里': '周深、王凯、江疏影、杨玏',
'消失的她': '朱一龙、倪妮、文咏珊、杜江',
'封神第一部': '费翔、李雪健、黄渤、于适'
};
return actors[title] || '未知';
}
// 获取上映日期
private getReleaseDate(title: string): string {
// 模拟数据,实际应用中应该从服务器获取
const dates = {
'流浪地球3': '2023-01-22',
'长安三万里': '2023-07-08',
'消失的她': '2023-06-22',
'封神第一部': '2023-07-20'
};
return dates[title] || '未知';
}
// 获取电影时长
private getDuration(title: string): string {
// 模拟数据,实际应用中应该从服务器获取
const durations = {
'流浪地球3': '173分钟',
'长安三万里': '148分钟',
'消失的她': '128分钟',
'封神第一部': '148分钟'
};
return durations[title] || '未知';
}
// 获取剧情简介
private getPlot(title: string): string {
// 模拟数据,实际应用中应该从服务器获取
const plots = {
'流浪地球3': '太阳即将毁灭,人类在地球表面建造出巨大的推进器,寻找新家园。然而宇宙之路危机四伏,为了拯救地球,流浪地球时代的年轻人再次挺身而出,展开争分夺秒的生死之战。',
'长安三万里': '盛唐时期,高适与李白、杜甫相继结识,并在人生际遇上交错变化。安史之乱爆发,高适与李白、杜甫分处不同地点,面对家国破碎,诗人们选择了不同的抗争方式,最终为时代留下了不朽诗篇。',
'消失的她': '何非拒绝与妻子一起过结婚纪念日,妻子离家出走,何非报警寻人,发现妻子早已消失不见。为查找真相,何非踏上寻找之路,发现一切并不简单。',
'封神第一部': '商王殷寿与妲己相恋,却遭到女娲阻止。妲己为复仇,与九尾狐合体,蛊惑殷寿,使其成为昏君。姬昌带领周国崛起,殷寿派纣军讨伐西岐,阐教与截教弟子介入人间纷争,封神大战一触即发。'
};
return plots[title] || '暂无简介';
}
// 获取日期选项
private getDateOptions(): string[] {
// 模拟数据,实际应用中应该根据当前日期生成
return [
'今天 08/01',
'明天 08/02',
'周三 08/03',
'周四 08/04',
'周五 08/05'
];
}
// 场次类型定义
interface ShowtimeType {
time: string; // 开始时间
endTime: string; // 结束时间
hall: string; // 影厅
language: string; // 语言版本
price: number; // 价格
}
// 获取场次信息
private getShowtimes(): ShowtimeType[] {
// 模拟数据,实际应用中应该从服务器获取
return [
{ time: '10:00', endTime: '12:30', hall: '1号厅', language: '国语2D', price: 39 },
{ time: '13:00', endTime: '15:30', hall: '2号厅', language: '国语IMAX', price: 59 },
{ time: '16:00', endTime: '18:30', hall: '3号厅', language: '国语3D', price: 49 },
{ time: '19:00', endTime: '21:30', hall: 'VIP厅', language: '国语2D', price: 69 }
];
}
// 导航到选座页面
private navigateToSeatSelection(showtime: ShowtimeType): void {
// 实际应用中应该跳转到选座页面
AlertDialog.show({
title: '选座购票',
message: `您选择了${this.currentMovie.title}在${showtime.time}的场次,价格为¥${showtime.price}`,
primaryButton: {
value: '确定',
action: () => {
console.info('用户确认购票');
}
},
secondaryButton: {
value: '取消',
action: () => {
console.info('用户取消购票');
}
}
});
}
这些辅助方法提供了电影详情页所需的各种数据,包括导演、主演、上映日期、时长、剧情简介、日期选项和场次信息。在实际应用中,这些数据应该从服务器获取。
5. 电影筛选和排序功能
5.1 添加筛选选项
// 筛选选项状态变量
@State filterOptions: {
types: string[];
minRating: number;
sortBy: string;
} = {
types: [],
minRating: 0,
sortBy: 'default'
};
// 筛选面板构建器
@Builder
private FilterPanel() {
Column() {
// 标题
Row() {
Text('筛选')
.fontSize(18)
.fontWeight(FontWeight.Bold)
Blank()
Button('重置')
.backgroundColor('transparent')
.fontColor('#666666')
.fontSize(14)
.onClick(() => {
this.resetFilter();
})
}
.width('100%')
.padding({ top: 16, bottom: 16 })
// 电影类型筛选
Text('电影类型')
.fontSize(16)
.fontWeight(FontWeight.Medium)
.margin({ bottom: 12 })
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(['科幻', '动画', '悬疑', '奇幻', '喜剧', '动作', '爱情', '恐怖'], (type: string) => {
Text(type)
.fontSize(14)
.fontColor(this.filterOptions.types.includes(type) ? Color.White : '#666666')
.backgroundColor(this.filterOptions.types.includes(type) ? '#FF5722' : '#F5F5F5')
.borderRadius(16)
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.margin({ right: 8, bottom: 8 })
.onClick(() => {
if (this.filterOptions.types.includes(type)) {
this.filterOptions.types = this.filterOptions.types.filter(t => t !== type);
} else {
this.filterOptions.types.push(type);
}
})
})
}
.margin({ bottom: 16 })
// 最低评分筛选
Text('最低评分')
.fontSize(16)
.fontWeight(FontWeight.Medium)
.margin({ bottom: 12 })
Row() {
Slider({
min: 0,
max: 10,
step: 0.5,
value: this.filterOptions.minRating
})
.blockColor('#FF5722')
.trackColor('#E0E0E0')
.selectedColor('#FF9800')
.showSteps(true)
.showTips(true)
.onChange((value: number) => {
this.filterOptions.minRating = value;
})
.layoutWeight(1)
Text(this.filterOptions.minRating.toFixed(1))
.fontSize(16)
.fontColor('#FF5722')
.margin({ left: 16 })
}
.width('100%')
.margin({ bottom: 16 })
// 排序方式
Text('排序方式')
.fontSize(16)
.fontWeight(FontWeight.Medium)
.margin({ bottom: 12 })
Column() {
this.SortOption('默认排序', 'default')
this.SortOption('评分从高到低', 'rating-desc')
this.SortOption('评分从低到高', 'rating-asc')
}
.margin({ bottom: 16 })
// 底部按钮
Row() {
Button('取消')
.width('48%')
.height(40)
.backgroundColor('#F5F5F5')
.fontColor('#666666')
.borderRadius(20)
.onClick(() => {
this.showFilter = false;
})
Button('确定')
.width('48%')
.height(40)
.backgroundColor('#FF5722')
.fontColor(Color.White)
.borderRadius(20)
.onClick(() => {
this.applyFilter();
this.showFilter = false;
})
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
}
.width('100%')
.padding(16)
.backgroundColor(Color.White)
.borderRadius({ topLeft: 16, topRight: 16 })
}
// 排序选项构建器
@Builder
private SortOption(text: string, value: string) {
Row() {
Text(text)
.fontSize(14)
.fontColor('#666666')
Blank()
Radio({ value: value, group: 'sortBy' })
.checked(this.filterOptions.sortBy === value)
.onChange((isChecked: boolean) => {
if (isChecked) {
this.filterOptions.sortBy = value;
}
})
}
.width('100%')
.height(40)
.padding({ left: 8, right: 8 })
.borderRadius(4)
.backgroundColor(this.filterOptions.sortBy === value ? '#FFF3E0' : 'transparent')
.margin({ bottom: 8 })
}
// 重置筛选选项
private resetFilter(): void {
this.filterOptions = {
types: [],
minRating: 0,
sortBy: 'default'
};
}
// 应用筛选
private applyFilter(): void {
// 筛选逻辑在getFilteredMovies方法中实现
}
// 获取筛选后的电影列表
private getFilteredMovies(): MovieType[] {
let filtered = this.movies;
// 按类型筛选
if (this.filterOptions.types.length > 0) {
filtered = filtered.filter(movie => this.filterOptions.types.includes(movie.type));
}
// 按评分筛选
if (this.filterOptions.minRating > 0) {
filtered = filtered.filter(movie => movie.rating >= this.filterOptions.minRating);
}
// 排序
switch (this.filterOptions.sortBy) {
case 'rating-desc':
filtered.sort((a, b) => b.rating - a.rating);
break;
case 'rating-asc':
filtered.sort((a, b) => a.rating - b.rating);
break;
default:
// 默认排序,保持原顺序
break;
}
return filtered;
}
这段代码实现了电影筛选和排序功能,包括:
- 电影类型筛选:用户可以选择一个或多个电影类型
- 最低评分筛选:用户可以设置最低评分要求
- 排序方式:用户可以选择默认排序、评分从高到低或评分从低到高
6. 购票流程设计
6.1 选座页面实现
@Builder
private SeatSelectionPage() {
Column() {
// 顶部导航栏
Row() {
Image($r("app.media.ic_back"))
.width(24)
.height(24)
.onClick(() => {
this.showSeatSelection = false;
})
Text('选择座位')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.margin({ left: 16 })
Blank()
}
.width('100%')
.height(56)
.padding({ left: 16, right: 16 })
.backgroundColor(Color.White)
// 电影和场次信息
Row() {
Column() {
Text(this.currentMovie.title)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
Text(`${this.selectedShowtime.time} | ${this.selectedShowtime.hall} | ${this.selectedShowtime.language}`)
.fontSize(14)
.fontColor('#666666')
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.Start)
Text(`¥${this.selectedShowtime.price}/张`)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#FF5722')
}
.width('100%')
.padding(16)
.justifyContent(FlexAlign.SpaceBetween)
.backgroundColor(Color.White)
// 银幕提示
Column() {
Text('银幕')
.fontSize(14)
.fontColor('#999999')
Image($r("app.media.ic_screen"))
.width('80%')
.height(24)
.margin({ top: 8 })
}
.width('100%')
.padding({ top: 24, bottom: 24 })
.backgroundColor('#F5F5F5')
// 座位图
Grid() {
ForEach(this.seats, (row: SeatRow, rowIndex: number) => {
ForEach(row.seats, (seat: Seat, colIndex: number) => {
GridItem() {
if (seat.type === 'empty') {
// 空位置,不显示任何内容
} else {
Image(this.getSeatImage(seat))
.width(24)
.height(24)
.onClick(() => {
if (seat.type === 'available') {
this.toggleSeatSelection(rowIndex, colIndex);
}
})
}
}
})
})
}
.columnsTemplate('1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr')
.rowsTemplate('1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr')
.columnsGap(8)
.rowsGap(8)
.width('100%')
.height(320)
.padding(16)
.backgroundColor('#F5F5F5')
// 座位图例
Row() {
this.SeatLegend($r("app.media.seat_available"), '可选')
this.SeatLegend($r("app.media.seat_selected"), '已选')
this.SeatLegend($r("app.media.seat_sold"), '已售')
this.SeatLegend($r("app.media.seat_disabled"), '不可选')
}
.width('100%')
.padding(16)
.justifyContent(FlexAlign.SpaceAround)
.backgroundColor('#F5F5F5')
// 已选座位和价格
Row() {
if (this.selectedSeats.length > 0) {
Text(`已选${this.selectedSeats.length}个座位:${this.getSelectedSeatsText()}`)
.fontSize(14)
.fontColor('#666666')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
} else {
Text('请选择座位')
.fontSize(14)
.fontColor('#666666')
}
Blank()
Text(`¥${this.selectedSeats.length * this.selectedShowtime.price}`)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#FF5722')
}
.width('100%')
.padding(16)
.backgroundColor(Color.White)
// 底部按钮
Button('确认选座')
.width('90%')
.height(48)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.backgroundColor(this.selectedSeats.length > 0 ? '#FF5722' : '#CCCCCC')
.borderRadius(24)
.margin({ top: 16, bottom: 16 })
.enabled(this.selectedSeats.length > 0)
.onClick(() => {
this.confirmSeatSelection();
})
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
// 座位图例构建器
@Builder
private SeatLegend(icon: Resource, text: string) {
Row() {
Image(icon)
.width(16)
.height(16)
Text(text)
.fontSize(12)
.fontColor('#666666')
.margin({ left: 4 })
}
}
// 座位类型定义
interface Seat {
type: 'available' | 'selected' | 'sold' | 'disabled' | 'empty';
row: number;
col: number;
}
// 座位行定义
interface SeatRow {
seats: Seat[];
}
// 获取座位图片
private getSeatImage(seat: Seat): Resource {
switch (seat.type) {
case 'available':
return $r("app.media.seat_available");
case 'selected':
return $r("app.media.seat_selected");
case 'sold':
return $r("app.media.seat_sold");
case 'disabled':
return $r("app.media.seat_disabled");
default:
return $r("app.media.seat_available");
}
}
// 切换座位选择状态
private toggleSeatSelection(rowIndex: number, colIndex: number): void {
const seat = this.seats[rowIndex].seats[colIndex];
if (seat.type === 'available') {
// 如果已选座位数量达到上限,提示用户
if (this.selectedSeats.length >= 4 && !this.selectedSeats.some(s => s.row === rowIndex && s.col === colIndex)) {
AlertDialog.show({
title: '提示',
message: '最多只能选择4个座位',
confirm: {
value: '确定',
action: () => {
console.info('用户确认');
}
}
});
return;
}
// 切换座位状态
if (this.selectedSeats.some(s => s.row === rowIndex && s.col === colIndex)) {
// 取消选择
this.selectedSeats = this.selectedSeats.filter(s => !(s.row === rowIndex && s.col === colIndex));
this.seats[rowIndex].seats[colIndex].type = 'available';
} else {
// 选择座位
this.selectedSeats.push({ row: rowIndex, col: colIndex });
this.seats[rowIndex].seats[colIndex].type = 'selected';
}
}
}
// 获取已选座位文本
private getSelectedSeatsText(): string {
return this.selectedSeats.map(seat => `${seat.row + 1}排${seat.col + 1}座`).join(', ');
}
// 确认选座
private confirmSeatSelection(): void {
// 实际应用中应该跳转到支付页面
AlertDialog.show({
title: '确认订单',
message: `电影:${this.currentMovie.title}\n场次:${this.selectedShowtime.time}\n座位:${this.getSelectedSeatsText()}\n总价:¥${this.selectedSeats.length * this.selectedShowtime.price}`,
primaryButton: {
value: '确认支付',
action: () => {
this.navigateToPayment();
}
},
secondaryButton: {
value: '取消',
action: () => {
console.info('用户取消支付');
}
}
});
}
// 导航到支付页面
private navigateToPayment(): void {
// 实际应用中应该跳转到支付页面
AlertDialog.show({
title: '支付成功',
message: '您的电影票已购买成功,请在"我的订单"中查看详情。',
confirm: {
value: '确定',
action: () => {
this.showSeatSelection = false;
this.showDetail = false;
}
}
});
}
这段代码实现了电影选座页面,包括:
- 顶部导航栏和电影场次信息
- 银幕提示
- 座位图,使用Grid组件实现
- 座位图例,显示不同状态的座位
- 已选座位和价格信息
- 确认选座按钮
7. 完整代码
由于完整代码较长,这里只展示了部分关键代码。完整代码包含了本教程中介绍的所有功能,包括响应式布局设计、电影卡片优化、电影详情页实现、电影筛选和排序功能、购票流程设计等。
8. 总结
本教程详细讲解了如何优化电影票务网格布局,添加交互功能,以及实现更多高级特性。通过使用HarmonyOS NEXT的GridRow和GridCol组件的高级特性,我们实现了响应式布局,使应用能够适应不同屏幕尺寸的设备。同时,我们还添加了电影卡片优化、电影详情页、电影筛选和排序功能、购票流程设计等功能,打造了一个功能完善的电影票务应用。
- 0回答
- 3粉丝
- 0关注
- [HarmonyOS NEXT 实战案例八] 电影票务网格布局(上)
- [HarmonyOS NEXT 实战案例四] 天气应用网格布局(下)
- [HarmonyOS NEXT 实战案例六] 餐饮菜单网格布局(下)
- [HarmonyOS NEXT 实战案例七] 健身课程网格布局(下)
- [HarmonyOS NEXT 实战案例九] 旅游景点网格布局(下)
- [HarmonyOS NEXT 实战案例五] 社交应用照片墙网格布局(下)
- [HarmonyOS NEXT 实战案例一] 电商首页商品网格布局(下)
- [HarmonyOS NEXT 实战案例四] 天气应用网格布局(上)
- [HarmonyOS NEXT 实战案例六] 餐饮菜单网格布局(上)
- [HarmonyOS NEXT 实战案例七] 健身课程网格布局(上)
- 鸿蒙全球电影票房排行榜
- [HarmonyOS NEXT 实战案例九] 旅游景点网格布局(上)
- [HarmonyOS NEXT 实战案例五] 社交应用照片墙网格布局(上)
- [HarmonyOS NEXT 实战案例一] 电商首页商品网格布局(上)
- [HarmonyOS NEXT 实战案例三] 音乐专辑网格展示(下)