鸿蒙原生ArkTS布局实战:从零构建剧本杀桌游玩乐详情页
项目演示

一、项目概述与技术背景
1.1 应用定位与业务场景
本应用是一个基于HarmonyOS ArkUI框架开发的剧本杀桌游玩乐详情页,模拟了娱乐类信息流页面的设计风格,旨在为用户提供沉浸式的剧本杀游戏浏览和预约体验。剧本杀作为近年来流行的社交娱乐方式,其线上预约平台的UI设计直接影响用户的消费决策和体验质量。
核心业务场景分析:
- 信息浏览场景:用户进入详情页后,首先看到剧本封面和基本信息,快速了解剧本主题、题材、人数配置等核心要素。
- 深度阅读场景:用户通过剧情简介了解故事背景和游戏特色,决定是否感兴趣。
- 场次选择场景:用户查看可预约的场次列表,根据时间和余位情况选择合适的场次。
- 预约下单场景:用户确认选中的场次后,通过底部操作栏完成预约流程。
目标用户画像:
| 用户类型 | 需求特点 | 行为特征 |
|---|---|---|
| 剧本杀新手 | 关注剧本难度、题材类型、价格 | 倾向选择热门推荐、评分高的剧本 |
| 资深玩家 | 关注剧情深度、推理逻辑、NPC演绎 | 倾向选择口碑好、有挑战性的剧本 |
| 社交组织者 | 关注人数配置、时间安排、门店位置 | 倾向选择灵活、方便组织的场次 |
1.2 HarmonyOS ArkUI框架核心特性
HarmonyOS ArkUI是华为HarmonyOS生态中的核心UI开发框架,采用声明式编程范式,支持多种开发语言(ArkTS/TypeScript/JavaScript)。其设计理念是"一次开发,多端部署",为开发者提供高效、灵活的跨设备UI开发能力。
声明式UI开发模式:
@Entry
@Component
struct Index {
@State message: string = 'Hello World';
build() {
Column() {
Text(this.message)
.fontSize(20)
}
}
}
这种模式的核心优势在于:
- 描述式编程:开发者只需描述UI的"状态",框架自动处理渲染和更新
- 响应式更新:状态变化时,相关组件自动重新渲染
- 组件化复用:通过@Builder和@Component实现UI片段的复用
组件化架构设计:
ArkUI的组件化架构遵循以下原则:
- 单一职责:每个组件只负责一个功能
- 数据驱动:组件状态由数据决定
- 层级分明:组件间通过参数传递数据,避免直接依赖
状态管理体系:
ArkUI提供多种状态装饰器,满足不同场景的需求:
| 装饰器 | 用途 | 适用场景 |
|---|---|---|
| @State | 组件内部状态 | 组件私有的状态管理 |
| @Prop | 单向数据绑定 | 父组件向子组件传递数据 |
| @Link | 双向数据绑定 | 父子组件共享状态 |
| @Provide/@Consume | 跨层级状态共享 | 祖父组件向子孙组件传递数据 |
1.3 API 24能力边界与技术选型
HarmonyOS API 24(对应HarmonyOS 3.1.0版本)是本项目的目标API版本,提供了丰富的UI组件和开发能力:
核心UI组件:
- Scroll:滚动容器,支持垂直和水平滚动,可自定义滚动条样式
- Column/Row:弹性布局容器,支持灵活的对齐和间距控制
- Flex:更强大的弹性布局,支持自动换行、多行对齐等高级特性
- Text:文本组件,支持多行显示、溢出处理、文本装饰等
- Button:按钮组件,支持状态控制、样式自定义
- Divider:分隔线组件,用于区分不同区域
- ForEach:列表渲染组件,用于动态生成列表项
样式系统:
- linearGradient:线性渐变背景,支持自定义方向和颜色
- borderRadius:圆角设计,支持单独设置四个角的半径
- shadow:阴影效果,支持自定义颜色、模糊半径和偏移
- decoration:文本装饰,支持下划线、删除线等效果
- alignItems/justifyContent:对齐方式,控制子组件的排列
交互能力:
- onClick:点击事件,支持绑定自定义处理函数
- 状态响应式:状态变量变化时自动更新UI
- enabled:组件启用/禁用状态,控制交互可用性
1.4 项目结构与文件组织
本项目采用HarmonyOS标准项目结构,主要文件如下:
e:\MyApplication5\
├── AppScope/ # 应用全局配置
│ ├── resources/ # 全局资源文件
│ │ └── base/element/string.json # 全局字符串资源
│ └── app.json5 # 应用配置文件
├── entry/ # 主模块
│ ├── src/main/
│ │ ├── ets/
│ │ │ ├── entryability/ # 应用入口能力
│ │ │ │ └── EntryAbility.ets
│ │ │ └── pages/ # 页面组件目录
│ │ │ └── Index.ets # 主页面(核心文件)
│ │ ├── resources/ # 模块资源目录
│ │ │ ├── base/element/ # 元素资源
│ │ │ │ ├── color.json # 颜色配置
│ │ │ │ ├── float.json # 浮点数配置
│ │ │ │ └── string.json # 字符串配置
│ │ │ ├── base/media/ # 媒体资源
│ │ │ └── base/profile/ # 配置文件
│ │ │ └── main_pages.json # 页面路由配置
│ │ └── module.json5 # 模块配置文件
│ ├── build-profile.json5 # 构建配置
│ └── oh-package.json5 # 依赖配置
└── hvigorfile.ts # 构建脚本
核心文件说明:
- Index.ets:位于
entry/src/main/ets/pages/目录下,是本应用的核心页面文件,包含577行代码,实现了完整的剧本杀详情页功能。 - module.json5:定义了模块的名称、类型、页面路由和能力配置。
- main_pages.json:配置了应用的页面路由,指定了首页为
pages/Index。
二、数据模型设计:SlotInfo与ScriptInfo接口
2.1 TypeScript接口定义规范
在HarmonyOS ArkUI开发中,TypeScript接口是定义数据结构的核心方式,为开发提供类型安全保障。本应用定义了两个核心接口:SlotInfo和ScriptInfo,分别描述场次信息和剧本信息的数据结构。
接口设计原则:
- 类型明确:每个字段都有明确的类型定义
- 语义清晰:字段命名准确反映其含义
- 可扩展性:预留扩展空间,便于后续功能迭代
- 扁平化:避免过深的数据嵌套,便于组件使用
2.2 SlotInfo场次信息接口深度分析
interface SlotInfo {
date: string; // 日期文本(如"今日"、"明日")
time: string; // 具体时间(如"19:00")
seatsLeft: number; // 剩余座位数
isFull: boolean; // 是否已满
}
字段设计详解:
| 字段名 | 类型 | 设计考量 | 使用场景 |
|---|---|---|---|
| date | string | 使用用户友好的日期描述,如"今日"、“明日”、“周六”,比纯日期格式更直观 | 场次卡片展示 |
| time | string | 使用24小时制时间格式,如"19:00",清晰明确 | 场次卡片展示 |
| seatsLeft | number | 数值类型,便于计算和比较 | 判断是否满员、显示余位数量 |
| isFull | boolean | 布尔类型,作为seatsLeft === 0的缓存,避免重复计算 | 快速判断场次状态、控制点击交互 |
设计亮点:
- 双重状态标记:同时提供
seatsLeft和isFull字段,isFull作为计算属性的缓存,提升渲染性能。 - 用户友好的日期格式:使用"今日"、"明日"等描述性文本,比"2024-01-15"更符合用户阅读习惯。
- 时间格式标准化:统一使用"HH:MM"格式,便于排序和比较。
2.3 ScriptInfo剧本信息接口深度分析
interface ScriptInfo {
title: string; // 剧本名称
englishTitle: string; // 英文名
coverEmoji: string; // 封面氛围符号
genres: string[]; // 题材标签列表
playerCount: string; // 人数配置
duration: string; // 游戏时长
storeName: string; // 门店名称
price: string; // 价格
originalPrice: string; // 原价(划线价)
rating: number; // 评分
ratingCount: number; // 评价数
summary: string; // 剧情简介
highlight: string; // 热门标记
availableSlots: SlotInfo[]; // 可预约场次列表
}
字段分类详解:
基础信息字段:
title:剧本中文名称,作为页面的核心标识englishTitle:剧本英文名称,增加国际化元素和设计美感coverEmoji:使用Emoji符号模拟封面图片,避免图片资源依赖,同时传达剧本氛围
标签与属性字段:
genres:题材标签数组,支持多个标签,如['古风', '情感', '沉浸', '还原']playerCount:人数配置,包含性别分配信息,如'6人(3男3女)'duration:游戏时长,如'4.5小时'
门店与价格字段:
storeName:门店名称,如'迷雾剧场·剧本杀旗舰店'price:当前价格,如'¥168/人'originalPrice:原价,用于展示折扣效果,如'¥228'
评分字段:
rating:评分数值,使用0-10分制,如9.2ratingCount:评价人数,如3427
内容字段:
summary:剧情简介,支持多行文本,包含故事背景和游戏特色highlight:热门标记或推荐标签,如'🔥 本周热推 · 沉浸式实景搜证 · 专业NPC演绎'
关联数据字段:
availableSlots:可预约场次列表,类型为SlotInfo[]数组
2.4 数据模型的完整性与扩展性评估
当前模型优点:
- 字段完整性:覆盖了剧本杀详情页所需的全部核心信息
- 类型安全性:使用TypeScript接口提供严格的类型检查
- 数据扁平化:避免深层嵌套,便于组件直接访问
- Mock数据友好:结构清晰,便于编写测试数据
潜在扩展方向:
| 扩展方向 | 新增字段建议 | 用途说明 |
|---|---|---|
| 图片资源 | coverImage: string | 剧本封面图片URL |
| 门店信息 | storeAddress: string, storeDistance: string | 门店地址和距离 |
| 评价系统 | reviews: ReviewInfo[] | 玩家评价列表 |
| NPC信息 | npcList: NPCInfo[] | NPC角色介绍 |
| 道具场景 | scenes: SceneInfo[] | 场景和道具描述 |
| 用户交互 | isFavorite: boolean, shareCount: number | 收藏状态和分享次数 |
数据模型优化建议:
当前数据模型中,price和originalPrice使用字符串类型,虽然便于展示,但不利于计算。建议增加数值类型的价格字段:
interface ScriptInfo {
// ... 其他字段
price: string; // 价格(展示用)
priceValue: number; // 价格数值(计算用)
originalPrice: string; // 原价(展示用)
originalPriceValue: number; // 原价数值(计算用)
// ... 其他字段
}
三、状态管理体系:@State装饰器的应用
3.1 ArkUI状态管理机制深度解析
ArkUI采用响应式状态管理机制,核心原理是:当状态变量发生变化时,框架自动检测变化并重新渲染依赖该状态的组件。这种机制避免了手动操作DOM,简化了开发流程。
状态管理工作流程:
用户交互 → 状态变化 → 框架检测 → 组件重新渲染 → UI更新
状态装饰器对比:
| 装饰器 | 数据流向 | 作用域 | 适用场景 |
|---|---|---|---|
| @State | 内部双向 | 组件内部 | 组件私有的状态 |
| @Prop | 父→子单向 | 父子组件 | 子组件只读使用父组件数据 |
| @Link | 父子双向 | 父子组件 | 父子组件共享状态 |
| @Provide/@Consume | 祖先→后代单向 | 跨层级 | 深层组件共享状态 |
3.2 scriptData状态变量分析
@State scriptData: ScriptInfo = {
title: '青山夜雨',
englishTitle: 'Green Mountain Night Rain',
coverEmoji: '🌲🏮🌧️',
genres: ['古风', '情感', '沉浸', '还原'],
playerCount: '6人(3男3女)',
duration: '4.5小时',
storeName: '迷雾剧场·剧本杀旗舰店',
price: '¥168/人',
originalPrice: '¥228',
rating: 9.2,
ratingCount: 3427,
summary: '民国三十七年,青山镇接连三日大雨,冲刷出一具无名白骨。' +
'六位故人齐聚"夜雨楼",各怀心事,各藏秘密。窗外雨声不绝,烛火摇曳间,' +
'他们不知道,今晚能走出这座小楼的,只有一人……\n\n' +
'全本阅读量约2.8万字,情感线饱满,推理与沉浸并重。' +
'三轮公投+沉浸演绎,结局多重反转,后劲极大。',
highlight: '🔥 本周热推 · 沉浸式实景搜证 · 专业NPC演绎',
availableSlots: [
{ date: '今日', time: '19:00', seatsLeft: 2, isFull: false },
{ date: '明日', time: '14:30', seatsLeft: 1, isFull: false },
{ date: '明日', time: '19:00', seatsLeft: 0, isFull: true },
{ date: '周六', time: '10:00', seatsLeft: 3, isFull: false },
]
};
设计分析:
- Mock数据设计:使用完整的Mock数据,便于开发和测试,无需依赖后端API
- 数据完整性:包含剧本杀详情页所需的全部信息,支持所有UI组件的渲染
- 状态独立性:作为组件内部状态,完全由组件自身管理
数据更新策略:
当前scriptData是静态Mock数据,实际项目中需要从API获取数据:
fetchScriptData(scriptId: string): void {
// 模拟API请求
setTimeout(() => {
this.scriptData = {
// 从API获取的数据
};
}, 1000);
}
3.3 selectedSlotIndex状态变量分析
@State selectedSlotIndex: number = -1;
设计分析:
- 索引存储方式:使用索引而非对象引用,便于状态比较和更新
- 初始状态:-1表示未选中任何场次
- 单一选中:一次只能选中一个场次
状态切换逻辑:
.onClick(() => {
if (!slot.isFull) {
this.selectedSlotIndex = index;
}
})
交互规则:
- 点击未满员的场次卡片,切换选中状态
- 点击已满员的场次卡片,无反应
- 再次点击已选中的场次卡片,可以取消选中(当前实现不支持,需要扩展)
3.4 showFullSummary状态变量分析
@State showFullSummary: boolean = false;
设计分析:
- 布尔状态:简单的开关状态,控制简介内容的展开/收起
- 初始状态:false表示默认收起,只显示部分内容
状态切换逻辑:
.onClick(() => {
this.showFullSummary = !this.showFullSummary;
})
交互体验:
- 点击展开按钮,显示全部简介内容
- 再次点击,恢复收起状态
- 按钮文本随状态变化,提供明确的操作指引
3.5 状态联动与响应式更新机制
状态联动关系图:
selectedSlotIndex (number)
│
├──→ BottomActionBar.backgroundColor
├──→ BottomActionBar.enabled
├──→ BookingSection卡片样式(背景色、文字颜色)
└──→ BookingSection选中提示(条件渲染)
showFullSummary (boolean)
│
├──→ SummarySection.maxLines
└──→ SummarySection按钮文本
响应式更新流程:
- 用户触发交互(点击场次卡片、点击展开按钮)
- 状态变量值改变
- ArkUI框架通过响应式系统检测到状态变化
- 框架识别出依赖该状态的组件
- 自动重新渲染相关组件
- UI更新,展示新的状态
性能优化考量:
- 最小化状态更新:只更新必要的状态变量
- 避免冗余渲染:组件只在依赖的状态变化时重新渲染
- 批量更新:多个状态变化合并为一次渲染
四、布局架构:Column+alignItems(Start)模式
4.1 垂直列表布局设计理念
本应用采用纵向列表式布局,这是移动端信息流页面的经典布局模式。核心设计理念包括:
信息层级化:
- 重要信息优先展示(封面、标题、评分)
- 次要信息依次排列(题材、门店、简介)
- 操作入口位于底部(预约按钮)
视觉一致性:
- 统一的卡片式设计
- 一致的间距和内边距
- 统一的配色方案
交互流畅性:
- 垂直滚动,符合移动端操作习惯
- 卡片之间有明确的分隔
- 操作区域易于点击
4.2 Column容器与alignItems属性详解
核心布局代码:
build() {
Scroll() {
Column({ space: 0 }) {
this.CoverSection()
this.TitleSection()
this.GenreSection()
this.StoreSection()
this.SummarySection()
this.BookingSection()
this.BottomActionBar()
}
.alignItems(HorizontalAlign.Start)
.width('100%')
}
.width('100%')
.height('100%')
.backgroundColor('#F5F0EB')
.scrollBar(BarState.Off)
}
布局层次分析:
Scroll(滚动容器,全屏)
└── Column(垂直排列容器,宽度100%)
├── CoverSection(封面区,宽度100%)
├── TitleSection(标题区,宽度100%)
├── GenreSection(题材标签区,宽度100%)
├── StoreSection(门店价格区,宽度100%)
├── SummarySection(剧情简介区,宽度100%)
├── BookingSection(拼车预约区,宽度100%)
└── BottomActionBar(底部操作栏,宽度100%)
alignItems(HorizontalAlign.Start)的作用:
- 左对齐:所有子组件在水平方向上左对齐
- 自适应宽度:子组件可以根据内容自动调整宽度(虽然本应用中所有子组件都设置了width(‘100%’))
- 布局一致性:保持整体布局的视觉一致性
4.3 Scroll组件的滚动行为优化
Scroll组件配置详解:
Scroll() {
// 内容区域
}
.width('100%') // 宽度占满屏幕
.height('100%') // 高度占满屏幕
.backgroundColor('#F5F0EB') // 暖米色背景
.scrollBar(BarState.Off) // 隐藏滚动条
滚动体验优化策略:
- 隐藏滚动条:
scrollBar(BarState.Off)提升视觉美观度,符合移动端设计趋势 - 全屏填充:
width('100%')和height('100%')确保内容区域最大化 - 背景色设置:在Scroll上设置背景色,而非每个子组件,减少样式重复
4.4 子组件排列策略与间距控制
间距控制机制:
Column({ space: 0 }) {
// 子组件之间无默认间距
}
每个子组件通过margin({ top: 1 })添加顶部间距:
.backgroundColor('#FFFFFF')
.margin({ top: 1 }) // 与上一个卡片之间的1px分隔
间距设计原则:
- 细微分隔:1px的间距提供视觉区分,又不显得过于松散
- 统一规范:所有卡片使用相同的间距规则
- 视觉层次:通过间距和背景色对比,形成清晰的视觉层次
内边距规范:
所有卡片式组件使用统一的内边距:
.padding({ left: 16, right: 16, top: 14, bottom: 14 })
这种规范确保了:
- 内容与边框的距离一致
- 左右内边距略大于上下内边距,符合阅读习惯
- 整体视觉整齐统一
4.5 响应式布局适配方案
当前适配策略分析:
- 百分比宽度:所有组件使用
width('100%'),自动适应屏幕宽度 - 固定像素内边距:使用固定的px值,在不同屏幕尺寸下保持一致的视觉效果
- 组件自适应:子组件根据内容自动调整高度
潜在改进方向:
- 使用vp单位:替代固定像素,实现基于屏幕密度的自适应
- 动态字体大小:根据屏幕尺寸调整字体大小
- 断点适配:针对不同屏幕尺寸提供不同的布局方案
响应式布局示例:
// 使用vp单位
.padding({ left: 16vp, right: 16vp, top: 14vp, bottom: 14vp })
// 根据屏幕宽度调整字体大小
.fontSize(this.screenWidth > 600 ? 18 : 14)
五、封面区域组件:CoverSection
5.1 组件功能定位与设计目标
功能定位:
封面区域是页面的视觉焦点,承担以下职责:
- 吸引注意力:通过视觉冲击力吸引用户注意
- 传达主题:展示剧本名称和主题氛围
- 突出卖点:通过热门标记展示剧本特色
设计目标:
- 视觉吸引力:使用渐变背景和Emoji符号营造氛围
- 信息层次:清晰展示剧本名称、英文名和热门标记
- 风格统一:与整体暖色调风格保持一致
5.2 双层Column嵌套结构分析
组件代码结构:
@Builder
CoverSection() {
Column() {
// 内层Column:封面背景区域
Column() {
Text(this.scriptData.coverEmoji)
.fontSize(40)
.letterSpacing(12)
.opacity(0.8)
Text(this.scriptData.title)
.fontSize(28)
.fontWeight(FontWeight.Bold)
.fontColor('#FFE4C4')
.margin({ top: 12 })
Text(this.scriptData.englishTitle)
.fontSize(13)
.fontColor('#C4A882')
.margin({ top: 6 })
.fontWeight(FontWeight.Lighter)
Divider()
.width(60)
.height(2)
.color('#C4A882')
.margin({ top: 16 })
.opacity(0.6)
}
.width('100%')
.height(240)
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.linearGradient({...})
// 热门标记横幅
Text(this.scriptData.highlight)
.width('100%')
.height(36)
.fontSize(13)
.fontColor('#FFF3E0')
.textAlign(TextAlign.Center)
.backgroundColor('#C0392B')
.lineHeight(36)
}
.width('100%')
.alignItems(HorizontalAlign.Start)
}
结构层次详解:
外层Column(alignItems: Start)
├── 内层Column(封面背景,240px高度)
│ ├── coverEmoji(氛围符号,40px)
│ ├── title(剧本名称,28px粗体)
│ ├── englishTitle(英文名,13px轻量)
│ └── Divider(装饰分隔线,60px宽)
└── highlight横幅(36px高度,红色背景)
嵌套设计原因:
- 功能分离:内层Column负责封面展示,外层Column负责整体布局
- 样式独立:内层Column有渐变背景,外层Column没有
- 布局灵活:可以独立控制内层和外层的对齐方式
5.3 渐变背景与视觉层次构建
渐变配置详解:
.linearGradient({
direction: GradientDirection.Bottom, // 从上到下渐变
colors: [
['#1A3A0A', 0.0], // 顶部:深绿色
['#2D5016', 0.5], // 中间:中绿色
['#4A2C0A', 1.0] // 底部:深棕色
]
})
颜色分析:
| 颜色值 | 颜色名称 | 位置 | 视觉效果 |
|---|---|---|---|
| #1A3A0A | 深绿色 | 顶部 | 营造神秘氛围 |
| #2D5016 | 中绿色 | 中间 | 过渡自然 |
| #4A2C0A | 深棕色 | 底部 | 增强稳重感 |
视觉层次构建:
- 渐变背景:从绿色到棕色的渐变,营造"青山夜雨"的主题氛围
- 文字颜色:使用暖黄色(#FFE4C4)和金色(#C4A882),与深色背景形成对比
- 符号装饰:Emoji符号增加视觉层次
- 分隔线:金色分隔线增加精致感
5.4 Emoji封面符号的设计考量
Emoji选择分析:
- 🌲:松树,象征剧本名称中的"青山"
- 🏮:灯笼,营造古风氛围,暗示故事发生在古代
- 🌧️:下雨,呼应剧本名称中的"夜雨"
使用Emoji的优势:
- 资源轻量化:无需额外的图片资源,减少包体积
- 跨平台一致性:Emoji在不同平台上都能正常显示
- 渲染性能:文本渲染比图片渲染更快
- 维护便捷:修改Emoji只需修改字符串,无需替换图片
样式处理技巧:
Text(this.scriptData.coverEmoji)
.fontSize(40) // 较大字号,增强视觉冲击力
.letterSpacing(12) // 增加字符间距,营造空间感
.opacity(0.8) // 适当降低透明度,避免过于突兀
5.5 热门标记横幅的交互设计
设计特点:
Text(this.scriptData.highlight)
.width('100%') // 宽度占满
.height(36) // 固定高度
.fontSize(13) // 小号字体
.fontColor('#FFF3E0') // 暖白色文字
.textAlign(TextAlign.Center) // 居中对齐
.backgroundColor('#C0392B') // 红色背景
.lineHeight(36) // 行高等于高度,垂直居中
视觉效果:
- 高对比度:红色背景(#C0392B)与暖白色文字(#FFF3E0)形成强烈对比
- 视觉突出:位于封面下方,吸引用户注意
- 信息密集:包含多个卖点信息,用"·"分隔
内容分析:
'🔥 本周热推 · 沉浸式实景搜证 · 专业NPC演绎'
- 🔥:火焰Emoji,增强视觉吸引力
- “本周热推”:时间限定,制造紧迫感
- “沉浸式实景搜证”:核心玩法特色
- “专业NPC演绎”:品质保证
六、标题与评分组件:TitleSection
6.1 Row布局的左右分栏策略
组件代码结构:
@Builder
TitleSection() {
Row() {
// 左侧:剧本信息
Column() {
Text(this.scriptData.title)
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor('#2C1810')
Text(this.scriptData.englishTitle)
.fontSize(13)
.fontColor('#8B7355')
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
// 右侧:评分信息
Column() {
// 评分数值
Row() {
Text(this.scriptData.rating.toString())
.fontSize(28)
.fontWeight(FontWeight.Bold)
.fontColor('#E67E22')
Text('分')
.fontSize(13)
.fontColor('#8B7355')
.margin({ left: 2 })
.alignSelf(ItemAlign.End)
.padding({ bottom: 4 })
}
// 星级评分
Row() {
ForEach([0, 1, 2, 3, 4], (index: number) => {
Text(index < Math.floor(this.scriptData.rating) - 5 ? '☆' : '★')
.fontSize(14)
.fontColor('#F1C40F')
})
}
// 评价人数
Text(`${this.scriptData.ratingCount}人评价`)
.fontSize(11)
.fontColor('#999')
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.Center)
}
.width('100%')
.padding({ left: 16, right: 16, top: 14, bottom: 10 })
.backgroundColor('#FFFFFF')
}
布局结构详解:
Row(左右分栏)
├── 左侧Column(layoutWeight: 1,占据主要空间)
│ ├── title(剧本名称,24px粗体)
│ └── englishTitle(英文名,13px浅色)
└── 右侧Column(居中对齐,固定宽度)
├── 评分Row
│ ├── rating(评分数值,28px橙色粗体)
│ └── '分'(单位,13px底部对齐)
├── 星级Row(5个星号,14px黄色)
└── ratingCount(评价人数,11px灰色)
分栏策略分析:
- 左侧为主:使用
layoutWeight(1)占据剩余空间,展示剧本名称 - 右侧为辅:固定宽度,展示评分信息
- 视觉平衡:左侧信息量大,右侧视觉冲击力强
6.2 剧本名称与英文名的排版设计
排版策略详解:
Text(this.scriptData.title)
.fontSize(24) // 大号字体,突出显示
.fontWeight(FontWeight.Bold) // 粗体,增强视觉冲击力
.fontColor('#2C1810') // 深棕色,主色调
Text(this.scriptData.englishTitle)
.fontSize(13) // 小号字体,辅助信息
.fontColor('#8B7355') // 浅棕色,次要颜色
.margin({ top: 4 }) // 与中文名保持适当间距
.fontWeight(FontWeight.Lighter) // 轻量字重,不喧宾夺主
字体层级设计:
| 元素 | 字体大小 | 字重 | 颜色 | 作用 |
|---|---|---|---|---|
| 中文名 | 24px | Bold | #2C1810 | 核心标识 |
| 英文名 | 13px | Lighter | #8B7355 | 辅助信息 |
排版原则:
- 主次分明:中文名突出,英文名辅助
- 对比强烈:通过字体大小、字重、颜色区分层级
- 间距适当:4px的间距既保持联系,又有区分
6.3 评分展示的视觉层级
评分展示设计:
Row() {
Text(this.scriptData.rating.toString())
.fontSize(28) // 大号字体,视觉焦点
.fontWeight(FontWeight.Bold) // 粗体,增强冲击力
.fontColor('#E67E22') // 橙色,温暖积极
Text('分')
.fontSize(13) // 小号字体
.fontColor('#8B7355') // 浅棕色
.margin({ left: 2 }) // 与数值保持间距
.alignSelf(ItemAlign.End) // 底部对齐
.padding({ bottom: 4 }) // 微调位置
}
视觉层级分析:
- 评分数值:28px橙色粗体,是评分区域的视觉焦点
- 单位"分":13px浅棕色,底部对齐,形成视觉平衡
- 对齐技巧:使用
alignSelf(ItemAlign.End)和padding({ bottom: 4 })实现数值和单位的视觉对齐
6.4 星级评分的动态生成逻辑
星级评分实现:
Row() {
ForEach([0, 1, 2, 3, 4], (index: number) => {
Text(index < Math.floor(this.scriptData.rating) - 5 ? '☆' : '★')
.fontSize(14)
.fontColor('#F1C40F')
})
}
逻辑分析:
当前代码使用Math.floor(this.scriptData.rating) - 5作为判断条件,对于评分9.2来说,结果为4,导致所有星都显示为实心。这是一个逻辑错误。
正确逻辑应该是:
// 方案1:10分制转5星制
Text(index < Math.floor(this.scriptData.rating / 2) ? '★' : '☆')
// 方案2:直接使用评分值(假设评分范围是0-5)
Text(index < Math.floor(this.scriptData.rating) ? '★' : '☆')
// 方案3:支持半星显示
Text(index < this.scriptData.rating ? (index < Math.floor(this.scriptData.rating) ? '★' : '★½') : '☆')
优化建议:
建议将评分逻辑提取为工具函数,提高代码可读性和可维护性:
getStarDisplay(rating: number, index: number): string {
const normalizedRating = rating / 2; // 10分制转5星制
if (index < Math.floor(normalizedRating)) {
return '★';
} else if (index < normalizedRating) {
return '★½'; // 半星
} else {
return '☆';
}
}
6.5 评价人数的信息呈现
评价人数设计:
Text(`${this.scriptData.ratingCount}人评价`)
.fontSize(11) // 小号字体,辅助信息
.fontColor('#999') // 灰色,不喧宾夺主
.margin({ top: 2 }) // 与星级保持间距
信息价值分析:
- 数量感:3427人评价,体现剧本的受欢迎程度
- 信任感:大量评价增加用户对评分的信任度
- 辅助信息:不干扰主要信息的展示
七、题材标签组件:GenreSection
7.1 Flex布局的自动换行机制
Flex布局配置:
Flex({
direction: FlexDirection.Row, // 水平方向排列
wrap: FlexWrap.Wrap, // 自动换行
alignContent: FlexAlign.Start // 多行内容顶部对齐
}) {
ForEach(this.scriptData.genres, (genre: string) => {
Text(`#${genre}`)
.fontSize(13)
.fontColor('#5D4037')
.padding({ left: 12, right: 12, top: 5, bottom: 5 })
.backgroundColor('#F5EDE3')
.borderRadius(14)
.margin({ right: 8, bottom: 8 })
})
}
.width('100%')
Flex属性详解:
| 属性 | 值 | 作用 |
|---|---|---|
| direction | Row | 子组件水平排列 |
| wrap | Wrap | 超出宽度时自动换行 |
| alignContent | Start | 多行内容顶部对齐 |
标签样式设计:
Text(`#${genre}`)
.fontSize(13) // 小号字体
.fontColor('#5D4037') // 深棕色文字
.padding({ left: 12, right: 12, top: 5, bottom: 5 }) // 内边距
.backgroundColor('#F5EDE3') // 浅米色背景
.borderRadius(14) // 圆角矩形
.margin({ right: 8, bottom: 8 }) // 外边距
标签设计特点:
- 圆角设计:14px圆角,柔和现代
- 浅色背景:#F5EDE3浅米色,与白色卡片形成对比
- #前缀:模拟社交媒体标签风格
- 适度间距:8px的左右和底部间距
7.2 题材标签的样式设计
标签内容分析:
剧本的题材标签为['古风', '情感', '沉浸', '还原']:
- 古风:故事背景设定在古代
- 情感:注重情感体验和角色关系
- 沉浸:强调代入感和互动体验
- 还原:需要玩家还原故事真相
样式设计原则:
- 视觉一致性:所有标签使用相同的样式
- 可点击性:虽然当前实现不可点击,但样式预留了点击空间
- 信息密度:标签紧凑排列,信息量大但不拥挤
7.3 人数与时长信息的表单化展示
表单化布局设计:
Row() {
// 人数
Row() {
Text('👥')
.fontSize(16)
Text(`${this.scriptData.playerCount}`)
.fontSize(14)
.fontColor('#3E2723')
.margin({ left: 6 })
}
.padding({ right: 20 })
.alignItems(VerticalAlign.Center)
// 分隔竖线
Divider()
.width(1)
.height(20)
.color('#DDD')
// 时长
Row() {
Text('⏱️')
.fontSize(16)
Text(`约${this.scriptData.duration}`)
.fontSize(14)
.fontColor('#3E2723')
.margin({ left: 6 })
}
.padding({ left: 20 })
.alignItems(VerticalAlign.Center)
// 难度标识
Row()
.layoutWeight(1)
Text('⭐⭐⭐ 进阶')
.fontSize(12)
.fontColor('#8D6E63')
}
布局结构详解:
Row
├── 人数Row(带👥图标,右内边距20)
├── Divider(竖线分隔,1px宽,20px高)
├── 时长Row(带⏱️图标,左内边距20)
├── 占位Row(layoutWeight: 1,自动填充)
└── 难度标识(右对齐)
表单化设计特点:
- 图标辅助:使用👥和⏱️图标增强视觉识别
- 分隔清晰:竖线分隔不同信息项
- 对齐整齐:垂直居中对齐
- 布局平衡:难度标识右对齐,使用占位Row实现
7.4 难度标识的视觉传达
难度标识设计:
Text('⭐⭐⭐ 进阶')
.fontSize(12) // 小号字体
.fontColor('#8D6E63') // 浅棕色
难度分级系统:
| 星级 | 难度描述 | 适用玩家 |
|---|---|---|
| ⭐⭐ | 新手 | 剧本杀新手 |
| ⭐⭐⭐ | 进阶 | 有一定经验的玩家 |
| ⭐⭐⭐⭐ | 困难 | 资深玩家 |
| ⭐⭐⭐⭐⭐ | 地狱 | 骨灰级玩家 |
位置策略:
难度标识右对齐,与左侧的人数、时长形成视觉平衡,同时方便用户快速查看。
7.5 桌游表单风格的设计借鉴
桌游表单设计特点:
- 信息结构化:将相关信息分组展示
- 使用图标:增强视觉识别和趣味性
- 清晰分隔:使用分隔线区分不同信息
- 注重可读性:信息密度适中,易于阅读
本组件的设计借鉴:
- 标签式布局:使用Flex布局展示题材标签,模拟桌游标签
- 表单式布局:使用Row和Divider展示人数、时长、难度
- 信息层次:标签在上,详细信息在下,层次清晰
八、门店与价格组件:StoreSection
8.1 信息分区与权重分配
组件代码结构:
@Builder
StoreSection() {
Row() {
// 门店信息
Column() {
Text('门店')
.fontSize(12)
.fontColor('#999')
.margin({ bottom: 4 })
Row() {
Text('📍')
.fontSize(16)
Text(this.scriptData.storeName)
.fontSize(15)
.fontColor('#2C1810')
.fontWeight(FontWeight.Medium)
.margin({ left: 6 })
}
.alignItems(VerticalAlign.Center)
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
// 价格信息
Column() {
Text('价格')
.fontSize(12)
.fontColor('#999')
.margin({ bottom: 4 })
Row() {
Text(this.scriptData.originalPrice)
.fontSize(14)
.fontColor('#B0B0B0')
.decoration({ type: TextDecorationType.LineThrough })
.margin({ right: 8 })
Text(this.scriptData.price)
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#E74C3C')
}
.alignItems(VerticalAlign.Bottom)
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
.padding({ left: 16, right: 16, top: 14, bottom: 14 })
.backgroundColor('#FFFFFF')
.margin({ top: 1 })
}
布局结构详解:
Row(左右分栏)
├── 左侧Column(layoutWeight: 1)
│ ├── '门店'标签(12px灰色)
│ └── 门店名称Row(📍图标 + 名称)
└── 右侧Column(右对齐)
├── '价格'标签(12px灰色)
└── 价格对比Row
├── originalPrice(划线价,14px灰色)
└── price(现价,20px红色粗体)
权重分配策略:
- 门店信息:使用
layoutWeight(1)占据主要空间,因为门店名称可能较长 - 价格信息:右对齐,固定宽度,突出价格对比
8.2 门店信息的位置标识设计
门店信息设计:
Row() {
Text('📍')
.fontSize(16) // 图标字号
Text(this.scriptData.storeName)
.fontSize(15) // 中等字体
.fontColor('#2C1810') // 深棕色
.fontWeight(FontWeight.Medium) // 中等字重
.margin({ left: 6 }) // 与图标保持间距
}
.alignItems(VerticalAlign.Center) // 垂直居中
设计特点:
- 图标辅助:📍图标直观表示位置信息
- 中等字重:Medium字重突出但不过分
- 深棕色:主色调,确保可读性
8.3 价格对比展示策略
价格对比设计:
Row() {
Text(this.scriptData.originalPrice)
.fontSize(14)
.fontColor('#B0B0B0')
.decoration({ type: TextDecorationType.LineThrough })
.margin({ right: 8 })
Text(this.scriptData.price)
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#E74C3C')
}
.alignItems(VerticalAlign.Bottom)
价格信息对比:
| 元素 | 字体大小 | 颜色 | 装饰 | 作用 |
|---|---|---|---|---|
| 原价 | 14px | #B0B0B0(浅灰色) | LineThrough(划线) | 参考价格 |
| 现价 | 20px | #E74C3C(红色) | Bold(粗体) | 当前价格 |
视觉策略:
- 原价弱化:使用灰色和划线,降低视觉权重
- 现价突出:使用红色和粗体,增强视觉冲击力
- 折扣暗示:通过价格对比,暗示折扣力度
8.4 划线价与现价的视觉层次
划线价实现:
.decoration({ type: TextDecorationType.LineThrough })
TextDecorationType.LineThrough会在文本中间添加一条删除线,表示该价格不再有效。
现价实现:
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#E74C3C')
对齐方式:
.alignItems(VerticalAlign.Bottom)
底部对齐确保划线价和现价在同一水平线上,提升视觉整齐度。
8.5 价格信息的可读性优化
设计考量:
- 红色价格:#E74C3C符合电商平台的价格展示惯例,用户容易识别
- 粗体大号字体:确保价格在视觉上是焦点
- 原价划线:明确折扣信息,避免用户误解
- 货币符号:清晰的价格标识(¥168/人)
用户体验优化:
- 价格单位明确:"¥168/人"明确表示每人价格
- 折扣信息清晰:原价和现价对比,折扣力度一目了然
- 视觉层次分明:价格信息与其他信息区分明显
九、剧情简介组件:SummarySection
9.1 可折叠内容的交互模式
组件代码结构:
@Builder
SummarySection() {
Column() {
// 分区标题
Row() {
Text('📖 剧情简介')
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor('#2C1810')
}
.width('100%')
// 简介内容
Text(this.scriptData.summary)
.fontSize(14)
.fontColor('#4E342E')
.lineHeight(24)
.width('100%')
.margin({ top: 10 })
.maxLines(this.showFullSummary ? 100 : 5)
.textOverflow({ overflow: TextOverflow.Ellipsis })
// 展开/收起按钮
Row() {
Text(this.showFullSummary ? '▲ 收起' : '▼ 展开全部简介')
.fontSize(13)
.fontColor('#8D6E63')
.padding({ top: 6, bottom: 6 })
}
.width('100%')
.justifyContent(FlexAlign.Center)
.margin({ top: 4 })
.onClick(() => {
this.showFullSummary = !this.showFullSummary;
})
}
.width('100%')
.padding({ left: 16, right: 16, top: 14, bottom: 14 })
.backgroundColor('#FFFFFF')
.margin({ top: 1 })
}
布局结构详解:
Column
├── 标题Row(📖 剧情简介,17px粗体)
├── Text(简介内容,14px,根据状态控制行数)
└── 展开/收起按钮Row(居中,13px)
9.2 Text组件的行数控制机制
行数控制核心代码:
Text(this.scriptData.summary)
.fontSize(14) // 适合阅读的字体大小
.fontColor('#4E342E') // 深棕色,清晰可读
.lineHeight(24) // 舒适的行高
.width('100%')
.margin({ top: 10 })
.maxLines(this.showFullSummary ? 100 : 5) // 行数控制
.textOverflow({ overflow: TextOverflow.Ellipsis }) // 溢出处理
行数控制逻辑:
| 状态 | maxLines值 | 效果 |
|---|---|---|
| 收起(showFullSummary: false) | 5 | 最多显示5行 |
| 展开(showFullSummary: true) | 100 | 最多显示100行(几乎不限制) |
溢出处理:
textOverflow({ overflow: TextOverflow.Ellipsis })会在文本末尾添加省略号(…),表示还有更多内容。
9.3 展开/收起按钮的状态切换
按钮设计:
Row() {
Text(this.showFullSummary ? '▲ 收起' : '▼ 展开全部简介')
.fontSize(13) // 小号字体
.fontColor('#8D6E63') // 浅棕色
.padding({ top: 6, bottom: 6 }) // 增加点击区域
}
.width('100%')
.justifyContent(FlexAlign.Center) // 居中对齐
.margin({ top: 4 })
.onClick(() => {
this.showFullSummary = !this.showFullSummary;
})
状态切换逻辑:
- 收起状态:按钮显示"▼ 展开全部简介",点击后展开
- 展开状态:按钮显示"▲ 收起",点击后收起
交互体验优化:
- 箭头指示:使用▲和▼箭头明确指示操作方向
- 点击区域:padding增加点击区域,便于操作
- 居中对齐:按钮居中,视觉平衡
9.4 信息流卡片样式的设计原则
设计特点:
- 标题图标:📖图标增强识别性
- 内容留白:适当的margin和padding
- 按钮独立:展开/收起按钮单独一行
- 风格统一:与其他卡片保持一致的设计风格
信息流设计原则:
- 信息密度适中:不过度拥挤,也不过于松散
- 内容预览:提供内容预览,用户可选择展开查看详情
- 交互明显:展开/收起按钮清晰可见
- 视觉层次:标题、内容、按钮层次分明
9.5 文本溢出与省略处理策略
省略处理配置:
.textOverflow({ overflow: TextOverflow.Ellipsis })
效果分析:
- 省略号提示:明确告知用户有隐藏内容
- 阅读体验:避免信息过载,保持页面整洁
- 操作引导:用户需要点击按钮查看完整内容
用户体验考量:
- 省略位置:在最后一行末尾显示省略号
- 内容完整性:确保前5行内容能够传达核心信息
- 展开便捷:点击按钮即可查看完整内容
十、拼车场次预约组件:BookingSection
10.1 表格风格的场次布局设计
组件代码结构:
@Builder
BookingSection() {
Column() {
// 分区标题 + 说明
Row() {
Text('🚗 拼车场次')
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor('#2C1810')
Text('(余位紧张,请尽早预约)')
.fontSize(12)
.fontColor('#E74C3C')
.margin({ left: 10 })
.alignSelf(ItemAlign.End)
.padding({ bottom: 2 })
}
.width('100%')
// 场次列表
Row() {
ForEach(this.scriptData.availableSlots, (slot: SlotInfo, index: number) => {
Column() {
Text(slot.date)
.fontSize(13)
.fontColor(this.selectedSlotIndex === index ? '#FFFFFF' : '#5D4037')
.fontWeight(FontWeight.Medium)
Divider()
.width(24)
.height(1)
.color(this.selectedSlotIndex === index ? '#FFFFFF' : '#DDD')
.margin({ top: 6, bottom: 6 })
.opacity(0.5)
Text(slot.time)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(this.selectedSlotIndex === index ? '#FFFFFF' : '#2C1810')
if (slot.isFull) {
Text('已满')
.fontSize(11)
.fontColor(this.selectedSlotIndex === index ? '#FFD54F' : '#E74C3C')
.margin({ top: 6 })
.fontWeight(FontWeight.Medium)
} else {
Text(`余${slot.seatsLeft}位`)
.fontSize(11)
.fontColor(this.selectedSlotIndex === index ? '#A5D6A7' : '#66BB6A')
.margin({ top: 6 })
.fontWeight(FontWeight.Medium)
}
}
.width(78)
.padding({ top: 10, bottom: 10 })
.borderRadius(8)
.backgroundColor(this.selectedSlotIndex === index ? '#5D4037' : '#F5F0EB')
.margin({ right: 8 })
.alignItems(HorizontalAlign.Center)
.onClick(() => {
if (!slot.isFull) {
this.selectedSlotIndex = index;
}
})
})
}
.width('100%')
.margin({ top: 10 })
.alignItems(VerticalAlign.Top)
// 选中提示
if (this.selectedSlotIndex >= 0) {
Text(`✅ 已选:${this.scriptData.availableSlots[this.selectedSlotIndex].date} ` +
`${this.scriptData.availableSlots[this.selectedSlotIndex].time} · ` +
`共¥${this.scriptData.price}`)
.fontSize(13)
.fontColor('#2E7D32')
.margin({ top: 10 })
.padding({ left: 12, right: 12, top: 8, bottom: 8 })
.backgroundColor('#E8F5E9')
.borderRadius(6)
.width('100%')
}
}
.width('100%')
.padding({ left: 16, right: 16, top: 14, bottom: 14 })
.backgroundColor('#FFFFFF')
.margin({ top: 1 })
}
布局结构详解:
Column
├── 标题Row(🚗 拼车场次 + 余位紧张提示)
├── 场次列表Row(水平排列的卡片)
│ ├── 场次卡片1(日期、时间、状态)
│ ├── 场次卡片2
│ ├── 场次卡片3
│ └── 场次卡片4
└── 选中提示Text(条件渲染,绿色背景)
10.2 ForEach循环渲染机制
ForEach参数详解:
ForEach(
this.scriptData.availableSlots, // 数据源:场次信息数组
(slot: SlotInfo, index: number) => { // 迭代函数
// 卡片内容
}
)
渲染特点:
- 动态生成:根据availableSlots数组长度自动生成卡片
- 独立绑定:每个卡片独立绑定点击事件和样式
- 索引传递:index参数用于标识当前卡片的位置
性能优化:
对于少量数据(如4个场次),ForEach的性能足够。对于大量数据,建议使用LazyForEach进行懒加载。
10.3 场次卡片的选中状态管理
选中状态样式:
.backgroundColor(this.selectedSlotIndex === index ? '#5D4037' : '#F5F0EB')
文字颜色变化:
.fontColor(this.selectedSlotIndex === index ? '#FFFFFF' : '#5D4037')
选中逻辑:
.onClick(() => {
if (!slot.isFull) {
this.selectedSlotIndex = index;
}
})
交互规则:
- 可选中:未满员的场次可以被选中
- 不可选中:已满员的场次点击无反应
- 单一选中:一次只能选中一个场次
当前实现的不足:
当前实现不支持取消选中,用户一旦选中某个场次,无法取消。建议添加取消选中逻辑:
.onClick(() => {
if (!slot.isFull) {
this.selectedSlotIndex = this.selectedSlotIndex === index ? -1 : index;
}
})
10.4 满员与余位状态的差异化展示
满员状态:
if (slot.isFull) {
Text('已满')
.fontSize(11)
.fontColor(this.selectedSlotIndex === index ? '#FFD54F' : '#E74C3C')
.margin({ top: 6 })
.fontWeight(FontWeight.Medium)
}
余位状态:
else {
Text(`余${slot.seatsLeft}位`)
.fontSize(11)
.fontColor(this.selectedSlotIndex === index ? '#A5D6A7' : '#66BB6A')
.margin({ top: 6 })
.fontWeight(FontWeight.Medium)
}
视觉区分策略:
| 状态 | 文字内容 | 颜色 | 含义 |
|---|---|---|---|
| 满员 | 已满 | #E74C3C(红色) | 不可选 |
| 余位 | 余X位 | #66BB6A(绿色) | 可选 |
| 选中满员 | 已满 | #FFD54F(黄色) | 已选中但已满(异常状态) |
| 选中余位 | 余X位 | #A5D6A7(浅绿色) | 已选中且可选 |
10.5 选中提示的动态呈现
条件渲染:
if (this.selectedSlotIndex >= 0) {
Text(`✅ 已选:${this.scriptData.availableSlots[this.selectedSlotIndex].date} ` +
`${this.scriptData.availableSlots[this.selectedSlotIndex].time} · ` +
`共¥${this.scriptData.price}`)
.fontSize(13)
.fontColor('#2E7D32')
.margin({ top: 10 })
.padding({ left: 12, right: 12, top: 8, bottom: 8 })
.backgroundColor('#E8F5E9')
.borderRadius(6)
.width('100%')
}
设计特点:
- 条件显示:只有选中场次后才显示
- ✅图标:明确的选中标识
- 绿色背景:#E8F5E9表示成功状态
- 圆角矩形:6px圆角,柔和美观
信息内容:
- 已选日期和时间
- 价格信息
- 完整的预约摘要
十一、底部操作栏组件:BottomActionBar
11.1 固定底部布局的实现方式
组件代码结构:
@Builder
BottomActionBar() {
Row() {
// 咨询客服按钮
Button() {
Text('📞 咨询')
.fontSize(15)
.fontColor('#5D4037')
}
.width(100)
.height(48)
.backgroundColor('#F5EDE3')
.borderRadius(24)
// 分隔占位
Row()
.layoutWeight(1)
// 立即预约主按钮
Button() {
Text('立即预约拼车 →')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
}
.height(48)
.layoutWeight(1)
.backgroundColor(this.selectedSlotIndex >= 0 ? '#5D4037' : '#BCAAA4')
.borderRadius(24)
.enabled(this.selectedSlotIndex >= 0)
.onClick(() => {
if (this.selectedSlotIndex >= 0) {
console.info('[剧本杀] 预约成功:场次=' +
`${this.scriptData.availableSlots[this.selectedSlotIndex].date} ` +
`${this.scriptData.availableSlots[this.selectedSlotIndex].time}`);
}
})
}
.width('100%')
.padding({ left: 16, right: 16, top: 12, bottom: 12 })
.backgroundColor('#FFFFFF')
.borderRadius({ topLeft: 16, topRight: 16 })
.shadow({
radius: 8,
color: '#1A000000',
offsetY: -2
})
}
布局结构详解:
Row(底部操作栏)
├── 咨询按钮(固定宽度100px,浅色背景)
├── 占位Row(layoutWeight: 1,自动填充)
└── 预约按钮(自适应宽度,深色背景)
11.2 双按钮的功能分区设计
咨询按钮设计:
Button() {
Text('📞 咨询')
.fontSize(15)
.fontColor('#5D4037')
}
.width(100) // 固定宽度
.height(48) // 固定高度
.backgroundColor('#F5EDE3') // 浅色背景
.borderRadius(24) // 圆角设计
设计特点:
- 固定宽度:100px,保持按钮大小一致
- 浅色背景:#F5EDE3作为次要操作,不喧宾夺主
- 深色文字:#5D4037确保可读性
预约按钮设计:
Button() {
Text('立即预约拼车 →')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
}
.height(48) // 固定高度
.layoutWeight(1) // 自适应宽度
.backgroundColor(this.selectedSlotIndex >= 0 ? '#5D4037' : '#BCAAA4')
.borderRadius(24) // 圆角设计
.enabled(this.selectedSlotIndex >= 0)
设计特点:
- 自适应宽度:layoutWeight(1)占据剩余空间
- 深色背景:#5D4037作为主要操作,视觉突出
- 白色文字:高对比度,清晰可读
- 箭头符号:→引导用户操作
11.3 按钮状态与选中状态的联动
预约按钮状态控制:
.backgroundColor(this.selectedSlotIndex >= 0 ? '#5D4037' : '#BCAAA4')
.enabled(this.selectedSlotIndex >= 0)
状态联动逻辑:
| 状态 | selectedSlotIndex | backgroundColor | enabled |
|---|---|---|---|
| 未选中 | -1 | #BCAAA4(灰色) | false |
| 已选中 | >= 0 | #5D4037(深棕色) | true |
交互效果:
- 禁用状态:按钮变暗,不可点击,提示用户需要先选择场次
- 启用状态:按钮正常,可点击,执行预约操作
11.4 阴影效果与圆角设计
圆角设计:
.borderRadius({ topLeft: 16, topRight: 16 })
- 顶部圆角:16px,柔和美观
- 底部直角:与屏幕边缘对齐,无缝衔接
阴影效果:
.shadow({
radius: 8,
color: '#1A000000',
offsetY: -2
})
- 模糊半径:8px,柔和的阴影效果
- 阴影颜色:#1A000000半透明黑色
- Y轴偏移:-2px,向上偏移,营造悬浮感
视觉效果:
- 圆角顶部:现代感、柔和感
- 阴影效果:悬浮感,与内容区域区分
- 整体风格:底部导航栏常见设计
11.5 用户操作流程优化
操作流程:
- 用户浏览页面,了解剧本信息
- 选择合适的场次(点击卡片)
- 选中提示出现,确认选择
- 预约按钮变为可点击状态
- 用户点击预约按钮完成预约
优化点:
- 状态自动更新:按钮状态根据选中状态自动切换
- 选中提示确认:明确显示已选场次信息
- 咨询按钮始终可用:便于用户随时联系客服
- 按钮大小适中:48px高度,符合移动端点击标准
十二、UI/UX设计模式与视觉体系
12.1 暖色调配色方案分析
主色调体系:
| 元素 | 颜色值 | 颜色名称 | 作用 |
|---|---|---|---|
| 背景色 | #F5F0EB | 暖米色 | 页面背景,模拟纸质质感 |
| 卡片背景 | #FFFFFF | 纯白 | 信息卡片背景 |
| 主文字 | #2C1810 | 深棕色 | 主要内容文字 |
| 辅助文字 | #8B7355 | 浅棕色 | 次要内容文字 |
强调色体系:
| 元素 | 颜色值 | 颜色名称 | 作用 |
|---|---|---|---|
| 价格红色 | #E74C3C | 珊瑚红 | 价格、重要提示 |
| 评分橙色 | #E67E22 | 橙色 | 评分、积极信息 |
| 星级黄色 | #F1C40F | 黄色 | 星级评分 |
| 成功绿色 | #2E7D32 | 绿色 | 成功状态、选中提示 |
| 热门标记红色 | #C0392B | 深红色 | 热门标记、警告信息 |
配色设计理念:
- 暖色调:营造温馨、舒适的氛围,符合娱乐类应用的定位
- 棕色系:复古、沉稳,呼应剧本杀的神秘主题
- 高对比度:确保信息可读性,特别是价格和评分
12.2 卡片式分区设计原则
卡片设计特点:
- 统一背景:所有卡片使用白色背景(#FFFFFF)
- 固定内边距:{ left: 16, right: 16, top: 14, bottom: 14 }
- 细微分隔:1px的顶部间距(margin({ top: 1 }))
- 独立功能:每个卡片负责一个功能模块
分区策略:
- 信息分组:将相关信息放在同一个卡片中
- 层次分明:重要信息在上,次要信息在下
- 视觉区分:通过背景色和间距区分不同区域
12.3 文字层级与字体规范
字体大小层级:
| 层级 | 字体大小 | 用途 | 示例 |
|---|---|---|---|
| 一级标题 | 28px | 封面剧本名称 | “青山夜雨” |
| 二级标题 | 24px | 页面剧本名称 | “青山夜雨” |
| 三级标题 | 17px | 分区标题 | “📖 剧情简介” |
| 正文 | 14px | 主要内容 | 剧情简介文本 |
| 辅助文字 | 12-13px | 次要内容 | 标签、状态 |
| 小提示 | 11px | 提示信息 | 评价人数、余位 |
字体样式规范:
| 样式 | 用途 | 示例 |
|---|---|---|
| Bold | 标题、价格、评分 | 剧本名称、¥168 |
| Medium | 门店名称 | “迷雾剧场·剧本杀旗舰店” |
| Lighter | 英文名 | “Green Mountain Night Rain” |
12.4 交互反馈与微动画设计
当前交互反馈:
- 点击场次卡片:背景色从#F5F0EB变为#5D4037,文字颜色从深色变为白色
- 点击展开按钮:文本行数从5行变为100行,按钮文本从"▼ 展开全部简介"变为"▲ 收起"
- 选中场次:底部按钮从灰色变为深棕色,从禁用变为启用
潜在改进方向:
- 添加过渡动画:状态切换时添加平滑过渡效果
- 点击反馈动画:按钮点击时添加缩放或颜色变化
- 展开收起动画:添加高度变化的过渡动画
- 列表动画:场次列表进入时添加淡入动画
12.5 用户体验优化策略
信息架构优化:
- 重要信息优先:封面、标题、评分优先展示
- 信息密度适中:不过度拥挤,也不过于松散
- 导航清晰:用户可以快速找到所需信息
交互优化:
- 操作流程简化:选择场次 → 点击预约,两步完成
- 反馈及时明确:选中状态、按钮状态实时更新
- 错误状态友好:已满场次明确标记,不可点击
视觉优化:
- 色彩搭配协调:暖色调为主,强调色突出
- 字体层级清晰:不同层级使用不同大小和字重
- 布局整齐统一:卡片式设计,间距一致
十三、交互逻辑与业务流程
13.1 场次选择交互流程
交互流程图:
用户进入页面
│
▼
查看场次列表(4个场次卡片)
│
├─→ 点击已满场次(如"明日 19:00")
│ │
│ ▼
│ 无反应(卡片保持原状,isFull为true)
│
└─→ 点击未满场次(如"今日 19:00")
│
▼
切换选中状态
│
├─→ 卡片背景色变为#5D4037
├─→ 卡片文字变为白色
└─→ 显示选中提示(绿色背景)
交互规则:
- 可选场次:seatsLeft > 0,isFull为false
- 不可选场次:seatsLeft = 0,isFull为true
- 单一选中:一次只能选中一个场次
13.2 简介展开/收起交互流程
交互流程图:
默认状态(收起)
│
├─→ 显示5行文本
├─→ 末尾显示省略号(...)
└─→ 按钮显示"▼ 展开全部简介"
│
▼
点击按钮
│
├─→ showFullSummary = true
├─→ 显示全部文本(最多100行)
└─→ 按钮显示"▲ 收起"
│
▼
再次点击按钮
│
├─→ showFullSummary = false
├─→ 恢复5行显示
└─→ 按钮显示"▼ 展开全部简介"
交互体验:
- 内容预览:默认显示5行,提供内容预览
- 操作指引:按钮文本明确指示操作方向
- 状态切换:点击即可切换状态,操作简单
13.3 预约按钮的状态控制
状态控制流程:
selectedSlotIndex = -1(未选中)
│
├─→ 预约按钮背景色:#BCAAA4(灰色)
├─→ 预约按钮状态:禁用(enabled: false)
└─→ 点击无反应
│
▼
选中场次(selectedSlotIndex >= 0)
│
├─→ 预约按钮背景色:#5D4037(深棕色)
├─→ 预约按钮状态:启用(enabled: true)
└─→ 可点击预约
│
▼
点击预约按钮
│
└─→ 打印预约信息到控制台
当前实现:
当前预约按钮点击后仅打印日志,实际项目中需要调用API完成预约:
.onClick(() => {
if (this.selectedSlotIndex >= 0) {
// 调用预约API
this.submitBooking();
}
})
submitBooking(): void {
const selectedSlot = this.scriptData.availableSlots[this.selectedSlotIndex];
// 构造请求参数
const requestData = {
scriptId: 'xxx',
slotId: `${selectedSlot.date}-${selectedSlot.time}`,
seats: 1
};
// 发送请求
console.info('[剧本杀] 预约成功:场次=' +
`${selectedSlot.date} ${selectedSlot.time}`);
}
13.4 咨询客服的功能预留
当前实现:
.onClick(() => {
console.info('[剧本杀] 点击咨询客服');
})
功能预留说明:
当前仅打印日志,后续可扩展为:
- 打开客服聊天窗口:跳转到客服对话页面
- 拨打电话:直接拨打门店电话
- 发送消息:发送咨询消息到后台
扩展实现示例:
.onClick(() => {
// 方案1:打开客服页面
router.pushUrl({ url: 'pages/CustomerService' });
// 方案2:拨打电话
// callPhone(this.scriptData.storePhone);
})
13.5 完整用户旅程分析
用户旅程图:
进入详情页
│
├─→ 浏览封面区域(了解剧本主题、氛围)
│ │
│ ├─→ 查看剧本名称和英文名
│ └─→ 查看热门标记
│
├─→ 查看标题与评分(了解基本信息)
│ │
│ ├─→ 确认剧本名称
│ └─→ 查看评分和评价人数
│
├─→ 查看题材标签(了解剧本类型)
│ │
│ ├─→ 查看题材标签(古风、情感等)
│ └─→ 查看人数配置和时长
│
├─→ 查看门店与价格(了解消费信息)
│ │
│ ├─→ 查看门店名称
│ └─→ 查看价格和折扣
│
├─→ 阅读剧情简介(决定是否感兴趣)
│ │
│ ├─→ 阅读5行预览
│ └─→ 点击展开查看完整简介
│
├─→ 选择拼车场次(选择合适时间)
│ │
│ ├─→ 查看所有可预约场次
│ ├─→ 选择未满员的场次
│ └─→ 确认选中的场次
│
└─→ 点击预约(完成预订)
│
├─→ 确认预约信息
└─→ 提交预约请求
用户体验评估:
- 信息获取效率:用户可以快速获取剧本的核心信息
- 决策辅助:评分、评价人数、剧情简介帮助用户做出决策
- 操作便捷:选择场次和预约流程简单直接
- 视觉体验:暖色调设计,卡片式布局,层次清晰
十四、性能优化与最佳实践
14.1 渲染性能优化策略
当前优化措施:
- @Builder复用:使用@Builder装饰器复用UI片段,减少重复代码
- 避免不必要的组件嵌套:合理使用Column/Row,避免过深的嵌套
- 条件渲染:使用if语句进行条件渲染,减少不必要的组件渲染
潜在优化方向:
- LazyForEach:对于大量数据,使用LazyForEach进行懒加载
- 组件拆分:将大型组件拆分为小型组件,提高渲染效率
- 状态管理优化:减少不必要的状态更新,避免冗余渲染
14.2 状态更新的效率考量
状态更新原则:
- 最小化更新范围:只更新必要的状态变量
- 避免频繁更新:合并多次状态更新为一次
- 使用合适的状态装饰器:根据场景选择@State、@Prop、@Link等
当前实现评估:
- 状态变量数量:3个状态变量(scriptData、selectedSlotIndex、showFullSummary),数量合理
- 状态更新逻辑:清晰明确,没有冗余更新
- 响应式更新:ArkUI框架自动处理,效率较高
14.3 内存管理与资源优化
资源管理策略:
- 使用Emoji替代图片:减少图片资源的使用,降低包体积
- 避免内存泄漏:及时清理无用的事件监听和定时器
- 优化数据结构:使用扁平化的数据结构,便于访问和更新
代码优化建议:
- 避免闭包陷阱:在循环中使用闭包时注意变量捕获
- 合理使用变量作用域:避免全局变量的滥用
- 及时释放资源:组件销毁时清理相关资源
14.4 代码组织与可维护性
代码结构优点:
- 组件化设计:每个@Builder组件职责清晰
- 代码注释完善:包含详细的注释说明
- 命名规范统一:变量和函数命名清晰
可维护性提升建议:
- 模块化拆分:将数据模型、工具函数提取到单独文件
- 样式统一:创建全局样式变量,避免硬编码
- 类型安全:完善TypeScript类型定义
14.5 ArkUI开发最佳实践
组件开发规范:
- 使用@Builder复用UI逻辑:减少代码重复
- 组件职责单一:每个组件只负责一个功能
- 参数传递清晰:通过参数传递数据,避免直接依赖
状态管理规范:
- 合理选择状态装饰器:根据场景选择合适的装饰器
- 状态变量命名清晰:使用描述性的变量名
- 状态更新逻辑集中:将状态更新逻辑集中管理
样式开发规范:
- 使用资源文件管理颜色和尺寸:避免硬编码
- 样式统一规范:使用一致的样式命名和格式
- 响应式设计:考虑不同屏幕尺寸的适配
十五、扩展性与改进建议
15.1 当前架构的扩展空间
数据层扩展:
- 对接真实API:当前使用Mock数据,需要对接后端API
- 添加数据缓存:缓存剧本信息,减少API请求
- 实现数据同步:支持离线数据同步
功能层扩展:
- 添加玩家评价模块:展示玩家评价列表,支持点赞和评论
- 添加门店详情模块:展示门店地址、营业时间、设施等信息
- 添加拼车聊天模块:支持同场次玩家之间的聊天
UI层扩展:
- 添加动画效果:状态切换时添加过渡动画
- 优化响应式布局:支持不同屏幕尺寸
- 支持深色模式:添加深色主题支持
15.2 API接口对接方案
数据接口设计:
interface ScriptDetailResponse {
code: number;
message: string;
data: ScriptInfo;
}
interface BookingRequest {
scriptId: string;
slotId: string;
userId: string;
count: number;
}
interface BookingResponse {
code: number;
message: string;
data: {
bookingId: string;
status: string;
totalPrice: number;
};
}
接口调用示例:
fetchScriptData(scriptId: string): void {
fetch(`/api/scripts/${scriptId}`)
.then(response => response.json())
.then(result => {
if (result.code === 0) {
this.scriptData = result.data;
}
})
.catch(error => {
console.error('获取剧本信息失败:', error);
});
}
submitBooking(): void {
const selectedSlot = this.scriptData.availableSlots[this.selectedSlotIndex];
const requestData: BookingRequest = {
scriptId: 'xxx',
slotId: `${selectedSlot.date}-${selectedSlot.time}`,
userId: 'currentUserId',
count: 1
};
fetch('/api/bookings', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestData)
})
.then(response => response.json())
.then(result => {
if (result.code === 0) {
console.info('预约成功:', result.data);
}
});
}
15.3 功能模块扩展建议
评价模块扩展:
interface ReviewInfo {
userId: string;
userName: string;
avatar: string;
rating: number;
content: string;
createTime: string;
likes: number;
}
// 评价列表组件
@Builder
ReviewSection() {
Column() {
Text('💬 玩家评价')
.fontSize(17)
.fontWeight(FontWeight.Bold)
ForEach(this.reviews, (review: ReviewInfo) => {
Column() {
Row() {
Text(review.userName)
Text(review.rating + '分')
}
Text(review.content)
}
})
}
}
门店模块扩展:
interface StoreInfo {
name: string;
address: string;
distance: string;
phone: string;
businessHours: string;
}
// 门店详情组件
@Builder
StoreDetailSection() {
Column() {
Text('📍 门店信息')
Text(this.storeInfo.address)
Text('距离:' + this.storeInfo.distance)
Text('营业时间:' + this.storeInfo.businessHours)
}
}
15.4 代码重构方向
模块化拆分:
- 数据模型分离:将ScriptInfo、SlotInfo接口提取到
data/types.ts - 工具函数分离:将评分计算等逻辑提取到
utils/helpers.ts - 组件分离:将各@Builder组件提取到单独文件
样式统一:
- 创建全局样式变量:在
resources/base/element/color.json中定义颜色变量 - 使用资源引用:使用
$color:xxx引用颜色,避免硬编码 - 统一间距规范:创建间距常量
类型安全:
- 完善类型定义:为所有接口添加完整的类型定义
- 使用泛型:在工具函数中使用泛型提升复用性
- 添加接口校验:使用TypeScript类型检查确保数据完整性
15.5 未来迭代规划
短期规划(1-2周):
- 修复星级评分逻辑错误:当前逻辑错误,需要修正
- 添加简单动画效果:状态切换时添加过渡动画
- 完善Mock数据:添加更多测试数据
中期规划(1-2月):
- 对接真实API:实现数据的获取和提交
- 添加评价模块:展示玩家评价列表
- 支持分享功能:支持分享剧本到社交平台
长期规划(3-6月):
- 实现完整拼车流程:支持多人拼车、组队功能
- 添加社交功能:玩家之间可以关注、聊天
- 支持多平台适配:适配平板、智慧屏等设备
总结
本项目是一个基于HarmonyOS ArkUI框架开发的剧本杀桌游玩乐详情页应用,展示了完整的UI布局、状态管理和交互逻辑。通过深入分析代码,我们可以看到:
- 架构设计合理:采用组件化设计,每个模块职责清晰,使用@Builder装饰器实现UI复用。
- 布局模式统一:使用Column+alignItems(Start)实现垂直列表布局,模拟信息流页面风格。
- 状态管理简洁:使用@State装饰器管理组件内部状态,状态联动关系清晰。
- UI设计美观:暖色调配色方案,卡片式分区设计,视觉层次分明。
- 交互逻辑完整:场次选择、简介展开、预约流程等功能完善,用户体验良好。
同时,项目也存在一些可改进的地方:
- 星级评分逻辑错误:当前代码使用
Math.floor(this.scriptData.rating) - 5,导致所有星都显示为实心。 - 缺少动画效果:状态切换时没有过渡动画,用户体验可以进一步提升。
- 未对接真实API:当前使用Mock数据,需要对接后端API才能实现完整功能。
- 功能模块可以扩展:可以添加评价模块、门店详情模块、社交功能等。
总体来说,这是一个结构完整、设计规范的HarmonyOS应用示例,适合作为学习和参考的案例。通过不断迭代和优化,可以打造一个功能完善、体验优秀的剧本杀预约平台。
更多推荐



所有评论(0)