项目演示

在这里插入图片描述

一、项目概述与技术背景

1.1 应用定位与业务场景

本应用是一个基于HarmonyOS ArkUI框架开发的剧本杀桌游玩乐详情页,模拟了娱乐类信息流页面的设计风格,旨在为用户提供沉浸式的剧本杀游戏浏览和预约体验。剧本杀作为近年来流行的社交娱乐方式,其线上预约平台的UI设计直接影响用户的消费决策和体验质量。

核心业务场景分析:

  1. 信息浏览场景:用户进入详情页后,首先看到剧本封面和基本信息,快速了解剧本主题、题材、人数配置等核心要素。
  2. 深度阅读场景:用户通过剧情简介了解故事背景和游戏特色,决定是否感兴趣。
  3. 场次选择场景:用户查看可预约的场次列表,根据时间和余位情况选择合适的场次。
  4. 预约下单场景:用户确认选中的场次后,通过底部操作栏完成预约流程。

目标用户画像:

用户类型 需求特点 行为特征
剧本杀新手 关注剧本难度、题材类型、价格 倾向选择热门推荐、评分高的剧本
资深玩家 关注剧情深度、推理逻辑、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接口是定义数据结构的核心方式,为开发提供类型安全保障。本应用定义了两个核心接口:SlotInfoScriptInfo,分别描述场次信息和剧本信息的数据结构。

接口设计原则:

  1. 类型明确:每个字段都有明确的类型定义
  2. 语义清晰:字段命名准确反映其含义
  3. 可扩展性:预留扩展空间,便于后续功能迭代
  4. 扁平化:避免过深的数据嵌套,便于组件使用

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的缓存,避免重复计算 快速判断场次状态、控制点击交互

设计亮点:

  1. 双重状态标记:同时提供seatsLeftisFull字段,isFull作为计算属性的缓存,提升渲染性能。
  2. 用户友好的日期格式:使用"今日"、"明日"等描述性文本,比"2024-01-15"更符合用户阅读习惯。
  3. 时间格式标准化:统一使用"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.2
  • ratingCount:评价人数,如3427

内容字段:

  • summary:剧情简介,支持多行文本,包含故事背景和游戏特色
  • highlight:热门标记或推荐标签,如'🔥 本周热推 · 沉浸式实景搜证 · 专业NPC演绎'

关联数据字段:

  • availableSlots:可预约场次列表,类型为SlotInfo[]数组

2.4 数据模型的完整性与扩展性评估

当前模型优点:

  1. 字段完整性:覆盖了剧本杀详情页所需的全部核心信息
  2. 类型安全性:使用TypeScript接口提供严格的类型检查
  3. 数据扁平化:避免深层嵌套,便于组件直接访问
  4. Mock数据友好:结构清晰,便于编写测试数据

潜在扩展方向:

扩展方向 新增字段建议 用途说明
图片资源 coverImage: string 剧本封面图片URL
门店信息 storeAddress: string, storeDistance: string 门店地址和距离
评价系统 reviews: ReviewInfo[] 玩家评价列表
NPC信息 npcList: NPCInfo[] NPC角色介绍
道具场景 scenes: SceneInfo[] 场景和道具描述
用户交互 isFavorite: boolean, shareCount: number 收藏状态和分享次数

数据模型优化建议:

当前数据模型中,priceoriginalPrice使用字符串类型,虽然便于展示,但不利于计算。建议增加数值类型的价格字段:

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 },
  ]
};

设计分析:

  1. Mock数据设计:使用完整的Mock数据,便于开发和测试,无需依赖后端API
  2. 数据完整性:包含剧本杀详情页所需的全部信息,支持所有UI组件的渲染
  3. 状态独立性:作为组件内部状态,完全由组件自身管理

数据更新策略:

当前scriptData是静态Mock数据,实际项目中需要从API获取数据:

fetchScriptData(scriptId: string): void {
  // 模拟API请求
  setTimeout(() => {
    this.scriptData = {
      // 从API获取的数据
    };
  }, 1000);
}

3.3 selectedSlotIndex状态变量分析

@State selectedSlotIndex: number = -1;

设计分析:

  1. 索引存储方式:使用索引而非对象引用,便于状态比较和更新
  2. 初始状态:-1表示未选中任何场次
  3. 单一选中:一次只能选中一个场次

状态切换逻辑:

.onClick(() => {
  if (!slot.isFull) {
    this.selectedSlotIndex = index;
  }
})

交互规则:

  • 点击未满员的场次卡片,切换选中状态
  • 点击已满员的场次卡片,无反应
  • 再次点击已选中的场次卡片,可以取消选中(当前实现不支持,需要扩展)

3.4 showFullSummary状态变量分析

@State showFullSummary: boolean = false;

设计分析:

  1. 布尔状态:简单的开关状态,控制简介内容的展开/收起
  2. 初始状态:false表示默认收起,只显示部分内容

状态切换逻辑:

.onClick(() => {
  this.showFullSummary = !this.showFullSummary;
})

交互体验:

  • 点击展开按钮,显示全部简介内容
  • 再次点击,恢复收起状态
  • 按钮文本随状态变化,提供明确的操作指引

3.5 状态联动与响应式更新机制

状态联动关系图:

selectedSlotIndex (number)
    │
    ├──→ BottomActionBar.backgroundColor
    ├──→ BottomActionBar.enabled
    ├──→ BookingSection卡片样式(背景色、文字颜色)
    └──→ BookingSection选中提示(条件渲染)

showFullSummary (boolean)
    │
    ├──→ SummarySection.maxLines
    └──→ SummarySection按钮文本

响应式更新流程:

  1. 用户触发交互(点击场次卡片、点击展开按钮)
  2. 状态变量值改变
  3. ArkUI框架通过响应式系统检测到状态变化
  4. 框架识别出依赖该状态的组件
  5. 自动重新渲染相关组件
  6. UI更新,展示新的状态

性能优化考量:

  1. 最小化状态更新:只更新必要的状态变量
  2. 避免冗余渲染:组件只在依赖的状态变化时重新渲染
  3. 批量更新:多个状态变化合并为一次渲染

四、布局架构: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)的作用:

  1. 左对齐:所有子组件在水平方向上左对齐
  2. 自适应宽度:子组件可以根据内容自动调整宽度(虽然本应用中所有子组件都设置了width(‘100%’))
  3. 布局一致性:保持整体布局的视觉一致性

4.3 Scroll组件的滚动行为优化

Scroll组件配置详解:

Scroll() {
  // 内容区域
}
.width('100%')           // 宽度占满屏幕
.height('100%')          // 高度占满屏幕
.backgroundColor('#F5F0EB') // 暖米色背景
.scrollBar(BarState.Off) // 隐藏滚动条

滚动体验优化策略:

  1. 隐藏滚动条scrollBar(BarState.Off)提升视觉美观度,符合移动端设计趋势
  2. 全屏填充width('100%')height('100%')确保内容区域最大化
  3. 背景色设置:在Scroll上设置背景色,而非每个子组件,减少样式重复

4.4 子组件排列策略与间距控制

间距控制机制:

Column({ space: 0 }) {
  // 子组件之间无默认间距
}

每个子组件通过margin({ top: 1 })添加顶部间距:

.backgroundColor('#FFFFFF')
.margin({ top: 1 })  // 与上一个卡片之间的1px分隔

间距设计原则:

  1. 细微分隔:1px的间距提供视觉区分,又不显得过于松散
  2. 统一规范:所有卡片使用相同的间距规则
  3. 视觉层次:通过间距和背景色对比,形成清晰的视觉层次

内边距规范:

所有卡片式组件使用统一的内边距:

.padding({ left: 16, right: 16, top: 14, bottom: 14 })

这种规范确保了:

  • 内容与边框的距离一致
  • 左右内边距略大于上下内边距,符合阅读习惯
  • 整体视觉整齐统一

4.5 响应式布局适配方案

当前适配策略分析:

  1. 百分比宽度:所有组件使用width('100%'),自动适应屏幕宽度
  2. 固定像素内边距:使用固定的px值,在不同屏幕尺寸下保持一致的视觉效果
  3. 组件自适应:子组件根据内容自动调整高度

潜在改进方向:

  1. 使用vp单位:替代固定像素,实现基于屏幕密度的自适应
  2. 动态字体大小:根据屏幕尺寸调整字体大小
  3. 断点适配:针对不同屏幕尺寸提供不同的布局方案

响应式布局示例:

// 使用vp单位
.padding({ left: 16vp, right: 16vp, top: 14vp, bottom: 14vp })

// 根据屏幕宽度调整字体大小
.fontSize(this.screenWidth > 600 ? 18 : 14)

五、封面区域组件:CoverSection

5.1 组件功能定位与设计目标

功能定位:

封面区域是页面的视觉焦点,承担以下职责:

  1. 吸引注意力:通过视觉冲击力吸引用户注意
  2. 传达主题:展示剧本名称和主题氛围
  3. 突出卖点:通过热门标记展示剧本特色

设计目标:

  1. 视觉吸引力:使用渐变背景和Emoji符号营造氛围
  2. 信息层次:清晰展示剧本名称、英文名和热门标记
  3. 风格统一:与整体暖色调风格保持一致

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高度,红色背景)

嵌套设计原因:

  1. 功能分离:内层Column负责封面展示,外层Column负责整体布局
  2. 样式独立:内层Column有渐变背景,外层Column没有
  3. 布局灵活:可以独立控制内层和外层的对齐方式

5.3 渐变背景与视觉层次构建

渐变配置详解:

.linearGradient({
  direction: GradientDirection.Bottom,  // 从上到下渐变
  colors: [
    ['#1A3A0A', 0.0],   // 顶部:深绿色
    ['#2D5016', 0.5],   // 中间:中绿色
    ['#4A2C0A', 1.0]    // 底部:深棕色
  ]
})

颜色分析:

颜色值 颜色名称 位置 视觉效果
#1A3A0A 深绿色 顶部 营造神秘氛围
#2D5016 中绿色 中间 过渡自然
#4A2C0A 深棕色 底部 增强稳重感

视觉层次构建:

  1. 渐变背景:从绿色到棕色的渐变,营造"青山夜雨"的主题氛围
  2. 文字颜色:使用暖黄色(#FFE4C4)和金色(#C4A882),与深色背景形成对比
  3. 符号装饰:Emoji符号增加视觉层次
  4. 分隔线:金色分隔线增加精致感

5.4 Emoji封面符号的设计考量

Emoji选择分析:

  • 🌲:松树,象征剧本名称中的"青山"
  • 🏮:灯笼,营造古风氛围,暗示故事发生在古代
  • 🌧️:下雨,呼应剧本名称中的"夜雨"

使用Emoji的优势:

  1. 资源轻量化:无需额外的图片资源,减少包体积
  2. 跨平台一致性:Emoji在不同平台上都能正常显示
  3. 渲染性能:文本渲染比图片渲染更快
  4. 维护便捷:修改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)          // 行高等于高度,垂直居中

视觉效果:

  1. 高对比度:红色背景(#C0392B)与暖白色文字(#FFF3E0)形成强烈对比
  2. 视觉突出:位于封面下方,吸引用户注意
  3. 信息密集:包含多个卖点信息,用"·"分隔

内容分析:

'🔥 本周热推 · 沉浸式实景搜证 · 专业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灰色)

分栏策略分析:

  1. 左侧为主:使用layoutWeight(1)占据剩余空间,展示剧本名称
  2. 右侧为辅:固定宽度,展示评分信息
  3. 视觉平衡:左侧信息量大,右侧视觉冲击力强

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 辅助信息

排版原则:

  1. 主次分明:中文名突出,英文名辅助
  2. 对比强烈:通过字体大小、字重、颜色区分层级
  3. 间距适当: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 })    // 微调位置
}

视觉层级分析:

  1. 评分数值:28px橙色粗体,是评分区域的视觉焦点
  2. 单位"分":13px浅棕色,底部对齐,形成视觉平衡
  3. 对齐技巧:使用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 })     // 与星级保持间距

信息价值分析:

  1. 数量感:3427人评价,体现剧本的受欢迎程度
  2. 信任感:大量评价增加用户对评分的信任度
  3. 辅助信息:不干扰主要信息的展示

七、题材标签组件: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 }) // 外边距

标签设计特点:

  1. 圆角设计:14px圆角,柔和现代
  2. 浅色背景:#F5EDE3浅米色,与白色卡片形成对比
  3. #前缀:模拟社交媒体标签风格
  4. 适度间距:8px的左右和底部间距

7.2 题材标签的样式设计

标签内容分析:

剧本的题材标签为['古风', '情感', '沉浸', '还原']

  • 古风:故事背景设定在古代
  • 情感:注重情感体验和角色关系
  • 沉浸:强调代入感和互动体验
  • 还原:需要玩家还原故事真相

样式设计原则:

  1. 视觉一致性:所有标签使用相同的样式
  2. 可点击性:虽然当前实现不可点击,但样式预留了点击空间
  3. 信息密度:标签紧凑排列,信息量大但不拥挤

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,自动填充)
└── 难度标识(右对齐)

表单化设计特点:

  1. 图标辅助:使用👥和⏱️图标增强视觉识别
  2. 分隔清晰:竖线分隔不同信息项
  3. 对齐整齐:垂直居中对齐
  4. 布局平衡:难度标识右对齐,使用占位Row实现

7.4 难度标识的视觉传达

难度标识设计:

Text('⭐⭐⭐ 进阶')
  .fontSize(12)           // 小号字体
  .fontColor('#8D6E63')   // 浅棕色

难度分级系统:

星级 难度描述 适用玩家
⭐⭐ 新手 剧本杀新手
⭐⭐⭐ 进阶 有一定经验的玩家
⭐⭐⭐⭐ 困难 资深玩家
⭐⭐⭐⭐⭐ 地狱 骨灰级玩家

位置策略:

难度标识右对齐,与左侧的人数、时长形成视觉平衡,同时方便用户快速查看。

7.5 桌游表单风格的设计借鉴

桌游表单设计特点:

  1. 信息结构化:将相关信息分组展示
  2. 使用图标:增强视觉识别和趣味性
  3. 清晰分隔:使用分隔线区分不同信息
  4. 注重可读性:信息密度适中,易于阅读

本组件的设计借鉴:

  1. 标签式布局:使用Flex布局展示题材标签,模拟桌游标签
  2. 表单式布局:使用Row和Divider展示人数、时长、难度
  3. 信息层次:标签在上,详细信息在下,层次清晰

八、门店与价格组件: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红色粗体)

权重分配策略:

  1. 门店信息:使用layoutWeight(1)占据主要空间,因为门店名称可能较长
  2. 价格信息:右对齐,固定宽度,突出价格对比

8.2 门店信息的位置标识设计

门店信息设计:

Row() {
  Text('📍')
    .fontSize(16)           // 图标字号
  Text(this.scriptData.storeName)
    .fontSize(15)           // 中等字体
    .fontColor('#2C1810')   // 深棕色
    .fontWeight(FontWeight.Medium) // 中等字重
    .margin({ left: 6 })    // 与图标保持间距
}
.alignItems(VerticalAlign.Center) // 垂直居中

设计特点:

  1. 图标辅助:📍图标直观表示位置信息
  2. 中等字重:Medium字重突出但不过分
  3. 深棕色:主色调,确保可读性

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(粗体) 当前价格

视觉策略:

  1. 原价弱化:使用灰色和划线,降低视觉权重
  2. 现价突出:使用红色和粗体,增强视觉冲击力
  3. 折扣暗示:通过价格对比,暗示折扣力度

8.4 划线价与现价的视觉层次

划线价实现:

.decoration({ type: TextDecorationType.LineThrough })

TextDecorationType.LineThrough会在文本中间添加一条删除线,表示该价格不再有效。

现价实现:

.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#E74C3C')

对齐方式:

.alignItems(VerticalAlign.Bottom)

底部对齐确保划线价和现价在同一水平线上,提升视觉整齐度。

8.5 价格信息的可读性优化

设计考量:

  1. 红色价格:#E74C3C符合电商平台的价格展示惯例,用户容易识别
  2. 粗体大号字体:确保价格在视觉上是焦点
  3. 原价划线:明确折扣信息,避免用户误解
  4. 货币符号:清晰的价格标识(¥168/人)

用户体验优化:

  1. 价格单位明确:"¥168/人"明确表示每人价格
  2. 折扣信息清晰:原价和现价对比,折扣力度一目了然
  3. 视觉层次分明:价格信息与其他信息区分明显

九、剧情简介组件: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;
})

状态切换逻辑:

  1. 收起状态:按钮显示"▼ 展开全部简介",点击后展开
  2. 展开状态:按钮显示"▲ 收起",点击后收起

交互体验优化:

  1. 箭头指示:使用▲和▼箭头明确指示操作方向
  2. 点击区域:padding增加点击区域,便于操作
  3. 居中对齐:按钮居中,视觉平衡

9.4 信息流卡片样式的设计原则

设计特点:

  1. 标题图标:📖图标增强识别性
  2. 内容留白:适当的margin和padding
  3. 按钮独立:展开/收起按钮单独一行
  4. 风格统一:与其他卡片保持一致的设计风格

信息流设计原则:

  1. 信息密度适中:不过度拥挤,也不过于松散
  2. 内容预览:提供内容预览,用户可选择展开查看详情
  3. 交互明显:展开/收起按钮清晰可见
  4. 视觉层次:标题、内容、按钮层次分明

9.5 文本溢出与省略处理策略

省略处理配置:

.textOverflow({ overflow: TextOverflow.Ellipsis })

效果分析:

  1. 省略号提示:明确告知用户有隐藏内容
  2. 阅读体验:避免信息过载,保持页面整洁
  3. 操作引导:用户需要点击按钮查看完整内容

用户体验考量:

  1. 省略位置:在最后一行末尾显示省略号
  2. 内容完整性:确保前5行内容能够传达核心信息
  3. 展开便捷:点击按钮即可查看完整内容

十、拼车场次预约组件: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) => {  // 迭代函数
    // 卡片内容
  }
)

渲染特点:

  1. 动态生成:根据availableSlots数组长度自动生成卡片
  2. 独立绑定:每个卡片独立绑定点击事件和样式
  3. 索引传递: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;
  }
})

交互规则:

  1. 可选中:未满员的场次可以被选中
  2. 不可选中:已满员的场次点击无反应
  3. 单一选中:一次只能选中一个场次

当前实现的不足:

当前实现不支持取消选中,用户一旦选中某个场次,无法取消。建议添加取消选中逻辑:

.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%')
}

设计特点:

  1. 条件显示:只有选中场次后才显示
  2. ✅图标:明确的选中标识
  3. 绿色背景:#E8F5E9表示成功状态
  4. 圆角矩形: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)     // 圆角设计

设计特点:

  1. 固定宽度:100px,保持按钮大小一致
  2. 浅色背景:#F5EDE3作为次要操作,不喧宾夺主
  3. 深色文字:#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)

设计特点:

  1. 自适应宽度:layoutWeight(1)占据剩余空间
  2. 深色背景:#5D4037作为主要操作,视觉突出
  3. 白色文字:高对比度,清晰可读
  4. 箭头符号:→引导用户操作

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 用户操作流程优化

操作流程:

  1. 用户浏览页面,了解剧本信息
  2. 选择合适的场次(点击卡片)
  3. 选中提示出现,确认选择
  4. 预约按钮变为可点击状态
  5. 用户点击预约按钮完成预约

优化点:

  1. 状态自动更新:按钮状态根据选中状态自动切换
  2. 选中提示确认:明确显示已选场次信息
  3. 咨询按钮始终可用:便于用户随时联系客服
  4. 按钮大小适中:48px高度,符合移动端点击标准

十二、UI/UX设计模式与视觉体系

12.1 暖色调配色方案分析

主色调体系:

元素 颜色值 颜色名称 作用
背景色 #F5F0EB 暖米色 页面背景,模拟纸质质感
卡片背景 #FFFFFF 纯白 信息卡片背景
主文字 #2C1810 深棕色 主要内容文字
辅助文字 #8B7355 浅棕色 次要内容文字

强调色体系:

元素 颜色值 颜色名称 作用
价格红色 #E74C3C 珊瑚红 价格、重要提示
评分橙色 #E67E22 橙色 评分、积极信息
星级黄色 #F1C40F 黄色 星级评分
成功绿色 #2E7D32 绿色 成功状态、选中提示
热门标记红色 #C0392B 深红色 热门标记、警告信息

配色设计理念:

  1. 暖色调:营造温馨、舒适的氛围,符合娱乐类应用的定位
  2. 棕色系:复古、沉稳,呼应剧本杀的神秘主题
  3. 高对比度:确保信息可读性,特别是价格和评分

12.2 卡片式分区设计原则

卡片设计特点:

  1. 统一背景:所有卡片使用白色背景(#FFFFFF)
  2. 固定内边距:{ left: 16, right: 16, top: 14, bottom: 14 }
  3. 细微分隔:1px的顶部间距(margin({ top: 1 }))
  4. 独立功能:每个卡片负责一个功能模块

分区策略:

  1. 信息分组:将相关信息放在同一个卡片中
  2. 层次分明:重要信息在上,次要信息在下
  3. 视觉区分:通过背景色和间距区分不同区域

12.3 文字层级与字体规范

字体大小层级:

层级 字体大小 用途 示例
一级标题 28px 封面剧本名称 “青山夜雨”
二级标题 24px 页面剧本名称 “青山夜雨”
三级标题 17px 分区标题 “📖 剧情简介”
正文 14px 主要内容 剧情简介文本
辅助文字 12-13px 次要内容 标签、状态
小提示 11px 提示信息 评价人数、余位

字体样式规范:

样式 用途 示例
Bold 标题、价格、评分 剧本名称、¥168
Medium 门店名称 “迷雾剧场·剧本杀旗舰店”
Lighter 英文名 “Green Mountain Night Rain”

12.4 交互反馈与微动画设计

当前交互反馈:

  1. 点击场次卡片:背景色从#F5F0EB变为#5D4037,文字颜色从深色变为白色
  2. 点击展开按钮:文本行数从5行变为100行,按钮文本从"▼ 展开全部简介"变为"▲ 收起"
  3. 选中场次:底部按钮从灰色变为深棕色,从禁用变为启用

潜在改进方向:

  1. 添加过渡动画:状态切换时添加平滑过渡效果
  2. 点击反馈动画:按钮点击时添加缩放或颜色变化
  3. 展开收起动画:添加高度变化的过渡动画
  4. 列表动画:场次列表进入时添加淡入动画

12.5 用户体验优化策略

信息架构优化:

  1. 重要信息优先:封面、标题、评分优先展示
  2. 信息密度适中:不过度拥挤,也不过于松散
  3. 导航清晰:用户可以快速找到所需信息

交互优化:

  1. 操作流程简化:选择场次 → 点击预约,两步完成
  2. 反馈及时明确:选中状态、按钮状态实时更新
  3. 错误状态友好:已满场次明确标记,不可点击

视觉优化:

  1. 色彩搭配协调:暖色调为主,强调色突出
  2. 字体层级清晰:不同层级使用不同大小和字重
  3. 布局整齐统一:卡片式设计,间距一致

十三、交互逻辑与业务流程

13.1 场次选择交互流程

交互流程图:

用户进入页面
    │
    ▼
查看场次列表(4个场次卡片)
    │
    ├─→ 点击已满场次(如"明日 19:00")
    │       │
    │       ▼
    │   无反应(卡片保持原状,isFull为true)
    │
    └─→ 点击未满场次(如"今日 19:00")
            │
            ▼
        切换选中状态
            │
            ├─→ 卡片背景色变为#5D4037
            ├─→ 卡片文字变为白色
            └─→ 显示选中提示(绿色背景)

交互规则:

  1. 可选场次:seatsLeft > 0,isFull为false
  2. 不可选场次:seatsLeft = 0,isFull为true
  3. 单一选中:一次只能选中一个场次

13.2 简介展开/收起交互流程

交互流程图:

默认状态(收起)
    │
    ├─→ 显示5行文本
    ├─→ 末尾显示省略号(...)
    └─→ 按钮显示"▼ 展开全部简介"
            │
            ▼
        点击按钮
            │
            ├─→ showFullSummary = true
            ├─→ 显示全部文本(最多100行)
            └─→ 按钮显示"▲ 收起"
                    │
                    ▼
                再次点击按钮
                    │
                    ├─→ showFullSummary = false
                    ├─→ 恢复5行显示
                    └─→ 按钮显示"▼ 展开全部简介"

交互体验:

  1. 内容预览:默认显示5行,提供内容预览
  2. 操作指引:按钮文本明确指示操作方向
  3. 状态切换:点击即可切换状态,操作简单

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('[剧本杀] 点击咨询客服');
})

功能预留说明:

当前仅打印日志,后续可扩展为:

  1. 打开客服聊天窗口:跳转到客服对话页面
  2. 拨打电话:直接拨打门店电话
  3. 发送消息:发送咨询消息到后台

扩展实现示例:

.onClick(() => {
  // 方案1:打开客服页面
  router.pushUrl({ url: 'pages/CustomerService' });
  
  // 方案2:拨打电话
  // callPhone(this.scriptData.storePhone);
})

13.5 完整用户旅程分析

用户旅程图:

进入详情页
    │
    ├─→ 浏览封面区域(了解剧本主题、氛围)
    │       │
    │       ├─→ 查看剧本名称和英文名
    │       └─→ 查看热门标记
    │
    ├─→ 查看标题与评分(了解基本信息)
    │       │
    │       ├─→ 确认剧本名称
    │       └─→ 查看评分和评价人数
    │
    ├─→ 查看题材标签(了解剧本类型)
    │       │
    │       ├─→ 查看题材标签(古风、情感等)
    │       └─→ 查看人数配置和时长
    │
    ├─→ 查看门店与价格(了解消费信息)
    │       │
    │       ├─→ 查看门店名称
    │       └─→ 查看价格和折扣
    │
    ├─→ 阅读剧情简介(决定是否感兴趣)
    │       │
    │       ├─→ 阅读5行预览
    │       └─→ 点击展开查看完整简介
    │
    ├─→ 选择拼车场次(选择合适时间)
    │       │
    │       ├─→ 查看所有可预约场次
    │       ├─→ 选择未满员的场次
    │       └─→ 确认选中的场次
    │
    └─→ 点击预约(完成预订)
            │
            ├─→ 确认预约信息
            └─→ 提交预约请求

用户体验评估:

  1. 信息获取效率:用户可以快速获取剧本的核心信息
  2. 决策辅助:评分、评价人数、剧情简介帮助用户做出决策
  3. 操作便捷:选择场次和预约流程简单直接
  4. 视觉体验:暖色调设计,卡片式布局,层次清晰

十四、性能优化与最佳实践

14.1 渲染性能优化策略

当前优化措施:

  1. @Builder复用:使用@Builder装饰器复用UI片段,减少重复代码
  2. 避免不必要的组件嵌套:合理使用Column/Row,避免过深的嵌套
  3. 条件渲染:使用if语句进行条件渲染,减少不必要的组件渲染

潜在优化方向:

  1. LazyForEach:对于大量数据,使用LazyForEach进行懒加载
  2. 组件拆分:将大型组件拆分为小型组件,提高渲染效率
  3. 状态管理优化:减少不必要的状态更新,避免冗余渲染

14.2 状态更新的效率考量

状态更新原则:

  1. 最小化更新范围:只更新必要的状态变量
  2. 避免频繁更新:合并多次状态更新为一次
  3. 使用合适的状态装饰器:根据场景选择@State、@Prop、@Link等

当前实现评估:

  1. 状态变量数量:3个状态变量(scriptData、selectedSlotIndex、showFullSummary),数量合理
  2. 状态更新逻辑:清晰明确,没有冗余更新
  3. 响应式更新:ArkUI框架自动处理,效率较高

14.3 内存管理与资源优化

资源管理策略:

  1. 使用Emoji替代图片:减少图片资源的使用,降低包体积
  2. 避免内存泄漏:及时清理无用的事件监听和定时器
  3. 优化数据结构:使用扁平化的数据结构,便于访问和更新

代码优化建议:

  1. 避免闭包陷阱:在循环中使用闭包时注意变量捕获
  2. 合理使用变量作用域:避免全局变量的滥用
  3. 及时释放资源:组件销毁时清理相关资源

14.4 代码组织与可维护性

代码结构优点:

  1. 组件化设计:每个@Builder组件职责清晰
  2. 代码注释完善:包含详细的注释说明
  3. 命名规范统一:变量和函数命名清晰

可维护性提升建议:

  1. 模块化拆分:将数据模型、工具函数提取到单独文件
  2. 样式统一:创建全局样式变量,避免硬编码
  3. 类型安全:完善TypeScript类型定义

14.5 ArkUI开发最佳实践

组件开发规范:

  1. 使用@Builder复用UI逻辑:减少代码重复
  2. 组件职责单一:每个组件只负责一个功能
  3. 参数传递清晰:通过参数传递数据,避免直接依赖

状态管理规范:

  1. 合理选择状态装饰器:根据场景选择合适的装饰器
  2. 状态变量命名清晰:使用描述性的变量名
  3. 状态更新逻辑集中:将状态更新逻辑集中管理

样式开发规范:

  1. 使用资源文件管理颜色和尺寸:避免硬编码
  2. 样式统一规范:使用一致的样式命名和格式
  3. 响应式设计:考虑不同屏幕尺寸的适配

十五、扩展性与改进建议

15.1 当前架构的扩展空间

数据层扩展:

  1. 对接真实API:当前使用Mock数据,需要对接后端API
  2. 添加数据缓存:缓存剧本信息,减少API请求
  3. 实现数据同步:支持离线数据同步

功能层扩展:

  1. 添加玩家评价模块:展示玩家评价列表,支持点赞和评论
  2. 添加门店详情模块:展示门店地址、营业时间、设施等信息
  3. 添加拼车聊天模块:支持同场次玩家之间的聊天

UI层扩展:

  1. 添加动画效果:状态切换时添加过渡动画
  2. 优化响应式布局:支持不同屏幕尺寸
  3. 支持深色模式:添加深色主题支持

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 代码重构方向

模块化拆分:

  1. 数据模型分离:将ScriptInfo、SlotInfo接口提取到data/types.ts
  2. 工具函数分离:将评分计算等逻辑提取到utils/helpers.ts
  3. 组件分离:将各@Builder组件提取到单独文件

样式统一:

  1. 创建全局样式变量:在resources/base/element/color.json中定义颜色变量
  2. 使用资源引用:使用$color:xxx引用颜色,避免硬编码
  3. 统一间距规范:创建间距常量

类型安全:

  1. 完善类型定义:为所有接口添加完整的类型定义
  2. 使用泛型:在工具函数中使用泛型提升复用性
  3. 添加接口校验:使用TypeScript类型检查确保数据完整性

15.5 未来迭代规划

短期规划(1-2周):

  1. 修复星级评分逻辑错误:当前逻辑错误,需要修正
  2. 添加简单动画效果:状态切换时添加过渡动画
  3. 完善Mock数据:添加更多测试数据

中期规划(1-2月):

  1. 对接真实API:实现数据的获取和提交
  2. 添加评价模块:展示玩家评价列表
  3. 支持分享功能:支持分享剧本到社交平台

长期规划(3-6月):

  1. 实现完整拼车流程:支持多人拼车、组队功能
  2. 添加社交功能:玩家之间可以关注、聊天
  3. 支持多平台适配:适配平板、智慧屏等设备

总结

本项目是一个基于HarmonyOS ArkUI框架开发的剧本杀桌游玩乐详情页应用,展示了完整的UI布局、状态管理和交互逻辑。通过深入分析代码,我们可以看到:

  1. 架构设计合理:采用组件化设计,每个模块职责清晰,使用@Builder装饰器实现UI复用。
  2. 布局模式统一:使用Column+alignItems(Start)实现垂直列表布局,模拟信息流页面风格。
  3. 状态管理简洁:使用@State装饰器管理组件内部状态,状态联动关系清晰。
  4. UI设计美观:暖色调配色方案,卡片式分区设计,视觉层次分明。
  5. 交互逻辑完整:场次选择、简介展开、预约流程等功能完善,用户体验良好。

同时,项目也存在一些可改进的地方:

  1. 星级评分逻辑错误:当前代码使用Math.floor(this.scriptData.rating) - 5,导致所有星都显示为实心。
  2. 缺少动画效果:状态切换时没有过渡动画,用户体验可以进一步提升。
  3. 未对接真实API:当前使用Mock数据,需要对接后端API才能实现完整功能。
  4. 功能模块可以扩展:可以添加评价模块、门店详情模块、社交功能等。

总体来说,这是一个结构完整、设计规范的HarmonyOS应用示例,适合作为学习和参考的案例。通过不断迭代和优化,可以打造一个功能完善、体验优秀的剧本杀预约平台。

Logo

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

更多推荐