本文深度解析 COMEDY BASE 笑场——一款融合百老汇剧场质感与聚光舞台美学的潮流脱口秀·开放麦俱乐部应用。项目采用 HarmonyOS ArkTS 声明式 UI 框架,单文件架构,无外部依赖,通过 6 大 Tab 页面、6 套弹框组件、5 组属性动画和约 20 个全局纯函数,完整实现演出购票、开放麦报名、演员社区、票夹管理等核心业务闭环。文末附完整可运行源码。

COMEDY BASE 笑场应用首页

引言:当脱口秀遇上鸿蒙生态

在内容消费多元化的今天,线下脱口秀已成为都市年轻人重要的文化娱乐方式。从开放麦的新人试炼到商演的成熟专场,从演员粉丝运营到票务选座管理,一个完整的脱口秀俱乐部需要覆盖演出、演员、观众、票务四大维度的数字化能力。

COMEDY BASE 笑场以"百老汇剧场 × 聚光舞台"为视觉基调,用深邃的幕布蓝(#141021)铺陈底色,用耀眼的聚光金(#E9B84C)点亮焦点,用热烈的幕布红(#A32330)渲染情绪,构建出沉浸式的线上剧场空间。技术层面选择 HarmonyOS ArkTS 声明式 UI 框架,单文件零依赖,复制即运行。


第一章 项目概述与设计理念

1.1 项目定位

COMEDY BASE 笑场定位为"潮流脱口秀·开放麦俱乐部"一体化数字平台,核心用户为 18-35 岁都市年轻群体。应用融合演出购票、开放麦报名、演员社区、票夹管理、观众档案五大业务模块,形成从"发现演出→购票选座→到场观演→发布观后感→关注演员→报名开放麦"的完整用户旅程闭环。

与传统票务 App 不同,COMEDY BASE 笑场更强调"社区感"和"剧场感"——观众可以发现演员、收藏段子、发布弹幕式观后感,新人可以报名开放麦、获得编剧指导、冲击"本月新星"榜单。

1.2 设计理念:百老汇剧场 × 聚光舞台

项目视觉设计灵感来源于百老汇剧院现场体验:

  • 幕布深蓝(#141021):主背景色,模拟剧院昏暗观众席
  • 聚光金(#E9B84C):主强调色,用于标题、按钮、选中态,如同舞台聚光灯
  • 幕布红(#A32330):辅助强调色,用于警示、退票、满场标识
  • 奶油白(#F2E8CE):文字主色,在深色背景上提供舒适阅读体验

这种"剧场感"贯穿到交互细节中——聚光锥呼吸动画、笑声音符上浮渐隐、麦克风左右摆动、奖杯金光脉动、二维码扫动效果,每一处动画都在模拟剧场现场的光影变化。

1.3 技术选型

维度 选型 说明
开发框架 HarmonyOS ArkTS 声明式 UI,原生性能
架构模式 单文件组件化 所有代码在一个 .ets 文件中
状态管理 @State + 组件回调 轻量级,无需额外库
动画方案 属性动画(animateTo) 5 组无限循环,零定时器
外部依赖 纯 ArkTS 内置组件

选择单文件架构是为了降低理解成本和部署门槛——整个应用就是一个 .ets 文件,复制到 DevEco Studio 的 pages 目录下即可运行。


第二章 色彩系统与视觉风格

2.1 色彩体系总览

COMEDY BASE 笑场的色彩体系由 12 个核心色值构成,通过 CBColorPalette 接口定义,以 CB_COLORS 常量对象全局引用:

const CB_COLORS = {
  bg: '#141021', card: '#1F1930', deep: '#0C0916', line: '#352B4A',
  red: '#A32330', redDeep: '#7C1B24', gold: '#E9B84C', goldDeep: '#C99A2E',
  cream: '#F2E8CE', textMain: '#F5EFE0', textSub: '#A89CB8', textHint: '#6E6280'
}

2.2 色彩层级与对比度

深色主题通过四级背景深度和三级文字明度构建清晰的视觉层级:

背景深度四级: deep #0C0916(极深黑,导航栏)→ bg #141021(幕布深蓝,页面背景)→ card #1F1930(深紫卡片,内容容器)→ deep #0C0916(输入框凹陷感)

文字明度三级: textMain #F5EFE0(标题/核心数据)→ textSub #A89CB8(副标题/说明)→ textHint #6E6280(占位符/辅助说明)

2.3 强调色使用策略

聚光金 #E9B84C 是视觉锚点,遵循"少即是多"原则:用于按钮主操作、选中态标识、数据高亮、装饰元素。幕布红 #A32330 作为互补色,用于警示性操作(退票/撤回)、满场标识、热梗"爆"标签。

2.4 渐变与光影效果

项目大量使用线性渐变营造剧场光影氛围:专场横幅渐变(主题色→深色)、聚光锥渐变(金色→透明)、演员卡片渐变(主题色→深黑)、头部横幅渐变(深红→深黑)。这些渐变配合属性动画,营造出动态的、有生命力的剧场光影体验。

COMEDY BASE 笑场色彩系统


三、开发环境搭建

3.1 环境要求

DevEco Studio 5.0+(支持HarmonyOS 6.1.1)、HarmonyOS SDK API 24、Node.js 18+、至少8GB内存。

3.2 安装DevEco Studio

DevEco Studio 安装界面

从华为开发者官网下载最新版本,运行安装包,选择安装路径,勾选创建桌面快捷方式,点击安装。

3.3 配置SDK

SDK 配置界面

首次启动后进入File > Settings > HarmonyOS SDK,勾选API 24及以上版本,点击Apply等待下载完成。

3.4 创建项目

创建项目界面

选择Create Project > Empty Ability,配置项目名称、包名、保存路径,选择Compile SDK为API 24,点击Finish。

3.5 运行应用

运行应用界面

将1316.ets代码替换到entry/src/main/ets/pages/Index.ets,连接HarmonyOS设备或启动模拟器,点击运行按钮预览。

3.6 预览调试

预览调试界面

DevEco Studio提供实时预览功能,编辑器右侧打开Previewer面板,支持多设备尺寸预览、深色模式切换、组件树inspect等调试能力。

第四章 项目架构与文件组织

4.1 单文件组件化架构

COMEDY BASE 笑场采用"单文件组件化"架构——所有代码在一个 .ets 文件中,通过 @Component 装饰器划分为 12 个独立组件(6 Tab + 5 弹框 + 1 根入口)。优势:零配置运行、低理解成本、快速原型。代码量约 1700 行,处于单文件架构合理范围。

4.2 组件层次结构

CBComedyBaseApp (@Entry 根组件)
├── 顶部标题栏(搜索框 + 消息角标)
├── 内容区 Stack
│   ├── 6 个 Tab 内容组件(6 选 1)
│   └── 6 个弹框(按需显示,zIndex 999)
└── 底部 Tab 导航栏(6 项)

根组件管理当前 Tab、6 个弹框显隐状态、弹框间传递数据,通过 contentArea() Builder 分发 Tab 内容和弹框层。每个 Tab 组件负责自身 UI 和局部状态,通过回调函数与根组件通信。

4.3 @Builder 自定义构建函数

项目使用 18 个 @Builder 方法封装可复用 UI 片段:列表行(memeRow/signupRow/sessionRow/rankRow/jokeRow)、卡片(actorCard/ticketCard)、装饰(curtainStrip/qrBlock)、表格(gridHeader/gridRow)、弹框结构(6 个 modal 方法)、内容分发(contentArea)。@Builder 减少重复代码,提高可读性,支持参数传递。

4.4 弹框架构设计

弹框采用"遮罩层 + 内容层"Stack 叠加模式,统一在根组件管理。每个弹框结构:全屏半透明遮罩(点击关闭)+ 内容卡片(居中或底部定位)。弹框间通过根组件状态变量传递数据,如点击购票时设置 buyShowName/buyPrice/buyOldPrice 后显示弹框。


第五章 数据模型与全局状态管理

5.1 接口定义体系

项目定义 19 个 TypeScript 接口描述数据模型:CBColorPalette(色彩)、CBTabItem(导航)、CBShow(演出)、CBSignup(报名)、CBSession(场次)、CBActor(演员)、CBTicket(票)、CBMeme(热梗)、CBJoke(段子)、CBWeekDay/CBPref/CBWeekCell(日程偏好)、CBStat(统计)、CBBadge(徽章)、CBNova(新星)、CBRepreShow(代表专场)、CBSettingItem(设置)。强类型定义确保数据使用的类型安全。

5.2 全局数据常量

18 组全局 const 常量:核心业务数据(CB_SHOWS 6场/CB_SIGNUPS 8组/CB_SESSIONS 8场/CB_ACTORS 10位/CB_TICKETS 6张)、内容数据(CB_MEMES 8条/CB_JOKES 6条/CB_REPRE 4个)、配置数据(CB_TABS/CB_FORMS/CB_WEEK/CB_PREFS/CB_WEEK_GRID/CB_STATS/CB_BADGES/CB_NOVA/CB_SETTINGS)。使用 const 确保数据不可变,所有动态状态通过组件 @State 管理。

5.3 三级状态管理策略

  • 第一级 全局常量(const):业务数据和配置,任何组件可直接读取,运行时不可变
  • 第二级 组件局部状态(@State):组件内交互状态,如 selSeats(已选座位)、stars(星级)、laughIdx(笑点指数),状态变化自动触发 UI 重渲染
  • 第三级 根组件协调状态(@State):跨组件状态,如 activeTab(当前Tab)、6个弹框显隐开关、弹框间传递数据,通过回调传递变更能力

5.4 全局纯函数

约 20 个全局纯函数处理数据转换和业务逻辑:色彩映射(cbCellBg/cbCellTx/cbStatusColor/cbFormBg/cbTicketColor/cbRankColor)、计算(cbDbBar/cbLaughPct/cbHeatBar/cbSeatSold/cbSeatPrice/cbSeatKey/cbSeatsTotal)、标签(cbMemeHeat/cbLaughLabel)。纯函数相同输入永远产生相同输出,无副作用,易于测试和复用。


第六章 六大 Tab 页面深度解析

6.1 剧场 Tab(CBTheaterContent)

剧场 Tab 是首页,承担"发现演出→了解热度→快速购票"转化路径。页面结构:本周爆笑专场大横幅(168vp,双聚光锥呼吸动画+三枚音符上浮动画+专场信息+购票按钮)→ 幕布条纹装饰条 → 本周上座率卡片(环形进度78%+10格色条)→ 本周热梗榜(8条,奖牌色排名)→ 7日场均笑声分贝柱状图(周六最高118dB金色高亮)→ 更多专场列表(5场)。

两组属性动画:聚光锥 opacity 0.9↔0.35(2000ms)、音符 translateY 0↔-22 + opacity 0.8↔0.1(1800ms),共同营造"舞台灯光闪烁+笑声音符飘升"的剧场氛围。

剧场Tab

6.2 开放麦 Tab(CBOpenMicContent)

开放麦 Tab 是社区核心,承载"新人报名→查看报名→编辑/撤回→了解扶持政策"流程。页面结构:开放麦报名头卡(110vp,麦克风±6°摆动动画+报名按钮)→ 本期报名列表(8组,头像+姓名+形式标签+主题+状态标签+编辑按钮,状态三态通过/待审/驳回)→ 新人扶持计划卡(金色边框,免审直通+编剧指导+¥3000奖金池)→ 下期名额进度(18/20红色进度条+抢名额按钮)。

开放麦Tab

6.3 场次 Tab(CBSessionsContent)

场次 Tab 以"周场次表+场次列表"双视图展示演出安排。周场次表:7天×3时段(晚场20:00/下午场15:00/加场17:30),每格显示晚/满/下/休,由 CB_WEEK_GRID 数据驱动,通过 cbCellBg/cbCellTx/cbCellText 三函数计算样式,“下午场"有金色边框区分。场次列表:8场卡片,含名称+日期时间+场地标签+HOT标签+价格+余票状态(<15显示红色"余票紧张”)+购票按钮+阵容头像横滚。

场次Tab

6.4 演员 Tab(CBActorsContent)

演员 Tab 通过"签约演员横滚+热度排行+本月新星"三层结构展示演员生态。签约演员横滚:10位演员卡片(渐变头像+姓名+风格标签+粉丝数+关注按钮,点击查看详情)。热度排行:按场均大笑次数降序,奖牌色排名+笑值进度条(前三名金色)+粉丝数,奇偶行交替背景。本月新星:奖杯scale 1↔1.18脉动动画+新星信息+围观按钮。

演员Tab

6.5 票夹 Tab(CBTicketsContent)

票夹 Tab 展示用户票务和观演统计。电子票列表:6张票,横向卡片左侧票信息(演出名+日期+座位号大号金色+状态标签+区域,未使用票显示退票/写观后感按钮),右侧二维码区(3×3方块模拟二维码,opacity 0.5↔1.0扫动动画+价格),右侧深黑背景模拟票根撕线。历史观演统计:2×2网格(观演数46场/大笑次数1024次/收藏段子68条/连续到场9周)。

票夹Tab

6.6 观众档 Tab(CBAudienceContent)

观众档 Tab 是个人中心,展示用户画像。观众卡:头像+昵称+笑点浓度标签+等级+ID/会籍+6个徽章chips。笑点偏好占比条形图:5项(生活流34%/职场28%/恋爱18%/谐音梗12%/黑色幽默8%),彩色条+百分比。收藏段子列表:6条(段子内容+标签+@演员+点赞数)。设置列表:4项(开票提醒/弹幕显示/票夹指纹锁/清理缓存)。


第七章 六大弹框组件设计

弹框是交互体系核心,6套弹框覆盖报名、购票、编辑、退票、演员详情、观后感发布六大场景,共享"遮罩层+内容层"架构。

弹框体系

7.1 报名开放麦弹框(CBSignOpenMicForm)

表单字段:艺名(文本)、表演形式(3选1chips:单口/漫才/即兴)、表演时长(stepper 3-15分钟默认8)、段子主题(文本)、过往经验(多行文本)。顶部显示下期开放麦时间,底部提示"新人首次报名免审直通"。操作:再想想(灰)+提交登台申请(金)。

7.2 购票选座弹框(CBBuyTicketForm)

核心功能:可视化座位图(5排×8列=40座,前区A-C排¥280/后区D-E排¥180,三态可选/已选/已售,最多选4座)+已选座位展示行(横向滚动chips)+票价明细(早鸟价+原价+已选总价动态计算)。选座逻辑通过4个纯函数实现:cbSeatSold(伪随机已售判断)、cbSeatPrice(分区定价)、cbSeatKey(座位标识)、cbSeatsTotal(总价累加)。

7.3 编辑报名弹框(CBEditOpenMicForm)

底部抽屉式设计(position y:30%),顶部44×5圆角小条模拟抽屉把手。可修改字段:主题、时长、形式。操作按钮:保存修改(金左对齐)+撤回报名(红右对齐)。底部警告"撤回后本季开放麦资格将释放,重新报名需排队"。

7.4 退票确认弹框(内联主入口)

警示性设计:红色2vp边框+顶部⚠️大号图标+确认退票标题(金)。退票规则三条(48小时外全额/48小时内扣20%/开演后不退可转赠)。票信息卡(金边框)显示演出名+日期座位价格+退款金额¥70.4(88×0.8)。操作:保留门票(灰)+确认退票(红)。

7.5 演员详情弹框(CBActorForm)

海报式设计:头部渐变横幅(演员色→深黑)+圆形头像+姓名+风格标签+签约标签+粉丝数。数据三栏(从业年限/个人专场/全网粉丝,竖线分隔)。代表专场横滚(4个彩色卡片+名称+年份)。底部操作:关闭档案(灰)+关注按钮(+关注金/已关注✓灰,followed状态管理)。

7.6 发布观后感弹框(CBReviewForm)

弹幕卡片式设计:顶部飘过的小弹幕装饰(3条:笑到缺氧/东北人自带BGM/谐音梗扣钱)。表单字段:选择场次(4选1chips)、满意度星级(5星点击设置)、笑点指数(10级进度条,≤3微笑/≤7大笑/>7笑劈叉,动态标签通过cbLaughLabel计算)、短评(20字内文本)、匿名发布(Switch默认开)。操作:不发了(灰)+发射弹幕(金)——用"发射弹幕"而非"发布"强化社区互动感。


第八章 属性动画与交互体验

8.1 ArkTS 属性动画机制

ArkTS 通过 animateTo() 实现属性动画,基本用法:

this.getUIContext().animateTo({
  duration: 2000, iterations: -1,
  playMode: PlayMode.Alternate, curve: Curve.EaseInOut
}, () => { this.stateVar = targetValue })

iterations: -1 表示无限循环,PlayMode.Alternate 表示到达目标值后反向执行回初始值,形成往返效果。配合使用可实现持续循环动画,无需 setInterval 定时器。框架自动处理中间帧和缓动曲线,开发者只需声明初始值和目标值。

8.2 五组属性动画详解

动画 组件 属性 时长 效果
聚光锥呼吸 剧场Tab spotOp(opacity) 2000ms 双聚光锥0.9↔0.35
音符上浮 剧场Tab noteY(translate)+noteOp(opacity) 1800ms 音符上移22vp+渐隐
麦克风摆动 开放麦Tab micAngle(rotate) 1200ms -6°↔6°
奖杯脉动 演员Tab trophyScale(scale) 1400ms 1↔1.18
二维码扫动 票夹Tab qrOp(opacity) 1600ms 0.5↔1.0

聚光锥呼吸模拟舞台灯光明暗变化,EaseInOut曲线使开始结束更平缓。音符上浮模拟笑声飘升,EaseOut曲线符合物体上浮减速物理直觉。麦克风摆动模拟支架晃动,1200ms较短时长更活泼。奖杯脉动模拟金光闪烁,18%缩放量明显但不夸张。二维码扫动通过部分方块透明度变化营造扫描线移动错觉。

8.3 无定时器循环动画的优势

传统循环动画使用 setInterval 逐帧更新,需手动管理定时器创建销毁、频繁状态更新可能导致性能问题。ArkTS 属性动画通过 iterations:-1 + PlayMode.Alternate 实现零定时器循环动画:框架底层自动调度动画帧、组件销毁时动画自动停止、多个动画可同时运行互不干扰。COMEDY BASE 笑场5组动画全部采用此方式,确保流畅性和性能。

8.4 交互反馈设计

项目通过多种方式提供即时交互反馈:按钮点击后立即触发状态变更(显示弹框/切换选中态),状态变更通过响应式机制自动触发UI更新。选中态有明确视觉变化(颜色/背景/边框)。Tab切换提供图标透明度+文字颜色+底部指示条三重反馈。星级评分点击后前N颗立即变金。座位选择点击后立即变金色选中态。已选座位总价实时计算更新。这些即时反馈确保用户操作的"确定性",提升交互流畅度和愉悦感。


第九章 选座系统与票务逻辑

9.1 选座系统架构

选座系统是技术复杂度最高的模块,处理座位布局渲染、状态管理、价格计算、交互反馈。采用"无状态"设计——不使用专门数据接口描述座位,通过行列索引(r,c)动态计算属性。已选座位通过 @State selSeats:string[] 管理,存储座位标识字符串(如"1排3号")。

选择座位使用不可变更新(concat/filter而非push/splice),因为 ArkTS @State 对数组变更检测需要新数组引用。选择逻辑:未选且<4座时 concat 添加,已选时 filter 移除。

9.2 伪随机已售算法

function cbSeatSold(r: number, c: number): boolean {
  return (r * 7 + c * 3) % 5 === 2
}

对行列号线性组合 r7+c3 后取模5,值等于2时标记已售。7和3互质产生均匀分布,取模5意味着约20%座位已售。优势:无需存储已售数据、每次渲染结果一致无闪烁、分布均匀自然符合真实票务场景。

9.3 分区定价与总价计算

座位价格按排号分区:前区A-C排(r<3)¥280,后区D-E排(r≥3)¥180。总价通过 cbSeatsTotal 动态计算:遍历已选座位数组,取座位标识首字符转为行号,减1得索引,通过 cbSeatPrice 获取单价累加。由于 selSeats 是 @State 变量,每次选择/取消后总价实时更新。

9.4 票务状态流转

票生命周期三态:未使用(金标签,可退票/写观后感)→已核销(绿标签,仅查看,到场扫码后);未使用→已过期(灰标签,演出结束未使用)。退票规则:开演前48小时外全额、48小时内扣20%手续费、开演后不退可转赠。退票时弹确认框显示规则和退款金额。


第十章 开放麦报名与审核流程

10.1 报名表单设计

报名表单包含:艺名(文本必填)、表演形式(3选1:单口/漫才/即兴,不同形式不同色标签)、表演时长(stepper 3-15分钟默认8)、段子主题(文本必填)、过往经验(多行文本选填)。时长下限3分钟确保完整段子,上限15分钟确保一场容纳6-10组。

10.2 审核状态管理

报名审核三态:通过(绿,已排入阵容)、待审(金,等待审核)、驳回(红,需修改重提)。状态颜色通过 cbStatusColor 映射。本期8组报名分布:通过3/待审3/驳回2。已报名用户可通过编辑弹框修改主题/时长/形式,或撤回报名(底部警告"撤回后资格释放,重新报名需排队")。

10.3 新人扶持与名额紧迫感

新人扶持计划三项政策:首次报名免审直通、免费编剧指导1v1、爆梗奖金池¥3000。卡片金色边框高亮,报名弹框底部也提示免审直通。下期名额进度卡显示18/20(红色进度条90%)+仅剩2名额+抢名额按钮,通过稀缺性和紧迫感促进转化。


第十一章 演员社区与热度排行

11.1 演员数据与风格覆盖

10位签约演员覆盖脱口秀主要流派:东北生活流、女性视角、职场吐槽、职场黑话、恋爱观察、黑色幽默、东北漫才、谐音梗、情感毒舌、合家欢。每位演员数据:emoji头像、主题色、风格标签、粉丝数(万)、场均大笑次数、从业年限、个人专场数。

11.2 热度排行算法

按"场均大笑次数"降序排列,这是喜剧演员搞笑能力的直接量化指标。可视化:排名数字(1-3名奖牌色金银铜)、演员信息(emoji+姓名+风格)、笑值进度条(laughs/45×100%,前三名金色其他红色)、粉丝数(右对齐,前三名金色加粗)。前三名:东北跑火车(42次/场28.6万粉)、阿慧不emo(38次/场22.3万粉)、大鹏不挪(35次/场19.8万粉)。

11.3 演员详情与本月新星

演员详情弹框采用海报式设计:头部渐变横幅+圆形头像+姓名+风格/签约标签+粉丝数、数据三栏(年限/专场/粉丝)、代表专场横滚(4个彩色卡片)、关注按钮(+关注金/已关注✓灰)。本月新星卡突出潜力新人:奖杯scale脉动动画(1↔1.18)+RISING STAR标签+新星信息+说明"开放麦黑马三连爆梗炸场,下月签约候选"+围观按钮。"开放麦→本月新星→签约演员"晋升通道为新人提供成长路径。


第十二章 性能优化与代码规范

12.1 性能优化策略

  1. 合理使用@State:只有需触发UI更新的状态用@State,静态数据用const,减少响应式开销
  2. 列表渲染优化:使用ForEach框架自动diff复用,列表项固定尺寸避免布局抖动
  3. 动画性能:使用属性动画而非逐帧动画,动画属性限制在opacity/translate/rotate/scale等合成层属性避免重排,无限循环用iterations:-1+Alternate零定时器
  4. 减少重渲染:弹框条件渲染未显示不渲染,避免build()中复杂计算
  5. 资源优化:使用emoji作图标头像无需图片,渐变背景用linearGradient无需图片

12.2 代码规范

命名规范:组件名大驼峰(CBTheaterContent)、变量函数小驼峰(activeTab)、常量大写下划线(CB_COLORS)、接口大驼峰(CBActor)、统一CB前缀。文件结构:接口→全局常量→纯函数→弹框组件→Tab组件→根入口。代码风格:2空格缩进、语句末尾不加分号、对象字面量每行一属性。

12.3 可维护性与可扩展性

组件化拆分12个独立组件职责单一。数据与视图分离(数据在const常量,UI只读渲染)。逻辑与视图分离(业务逻辑封装在约20个纯函数中)。配置化设计(Tab/表演形式/周场次表/色彩均通过配置数据驱动)。扩展方向:数据层可平滑迁移到HTTP请求、功能层可增加演出详情/演员动态/社区互动/会员体系、设备适配可扩展平板折叠屏和深浅主题。


第十三章 扩展方向与未来规划

13.1 功能扩展

演出票务深化:演出详情页、选座缩放拖拽、票务转赠、套票年卡、在线改签。开放麦生态:在线投稿点评、排练预约、观众评分、新人成长体系、编剧工坊。演员社区:演员动态短视频、粉丝群、多维度榜单、线下见面会、周边商城。观众体验:个性化推荐、完整观演记录、社交约人同看、更多成就徽章、演出期间弹幕互动。

13.2 技术演进与商业化

短期(1-3月):对接后端API替换硬编码、引入状态管理库、增加本地存储、完善错误处理。中期(3-6月):引入路由管理、推送通知开票提醒、接入支付SDK实现在线购票、性能优化懒加载、适配平板折叠屏。长期(6-12月):短视频播放、实时聊天弹幕、地图服务剧场导航、AR试座预览座位视野、跨设备协同。

商业化路径:票务收入(抽成/会员费/溢价座位)、内容收入(付费直播回放/演员专栏/独家订阅)、电商收入(周边/联名款/套餐)、广告收入(开屏/推荐位/品牌合作)、线下收入(报名费/培训费/活动门票)。

13.3 鸿蒙生态融合

原子化服务:将购票/选座/报名封装为原子化服务,无需安装即用,支持跨设备流转。万能卡片:提供本周演出/我的票夹/开放麦名额卡片,桌面实时查看。多设备协同:手机浏览购票、平板大屏选座、智慧屏观影、手表验票提醒。小艺语音:语音查询演出/购票选座/提醒时间。


第十四章 总结与展望

14.1 项目总结

COMEDY BASE 笑场是融合百老汇剧场质感与聚光舞台美学的潮流脱口秀·开放麦俱乐部概念应用。设计价值:首创"百老汇剧场×聚光舞台"视觉风格,5组无定时器属性动画让界面"活"起来,6套差异化弹框每种场景有专属交互语言。技术价值:单文件ArkTS零依赖复制即运行,19接口+18常量+约20纯函数构建完整类型系统,数据驱动UI渲染,三级状态管理简单高效,选座/票务核心逻辑完整实现。业务价值:覆盖发现→购票→观演→观后感→关注→报名完整用户旅程,开放麦全流程管理,演员社区生态提升粘性,票夹管理完善个人资产。

14.2 技术亮点

  1. 无定时器循环动画:animateTo+iterations:-1+Alternate实现5组持续动画,零定时器高性能自动生命周期
  2. 可视化选座系统:5×8座位图三态管理,伪随机已售分布,分区定价动态总价
  3. 数据驱动周场次表:7天×3时段完全由数据驱动,3纯函数计算单元格样式
  4. 集中式弹框管理:6弹框统一在根组件Stack管理,布尔变量控制显隐,状态传递数据
  5. @Builder UI复用:18个@Builder方法封装可复用UI片段,大幅减少重复代码

14.3 对鸿蒙开发的启示

ArkTS声明式UI范式使开发者用简洁代码构建复杂界面。animateTo属性动画是一大亮点,声明式指定目标值框架自动处理中间帧,零代码实现持续循环动画。单文件架构在概念验证/教学演示/快速原型场景有独特优势。纯函数封装业务逻辑是被低估的最佳实践,无副作用易测试可复用。随着鸿蒙生态完善,未来脱口秀应用可通过原子化服务、万能卡片、多设备协同、AI能力、5G+AR实现更创新的体验。

COMEDY BASE笑场总结


附录:完整源码

以下是 COMEDY BASE 笑场应用的完整 ArkTS 源码(去缩进版),可直接复制到 DevEco Studio 的 pages/Index.ets 文件中运行。

// ============================================================
// COMEDY BASE 笑场 —— 潮流脱口秀·开放麦俱乐部
// 百老汇剧场 × 聚光舞台风(得物App风格:演出购票+演员社区+开放麦报名)
// ArkTS 单文件 · 无 import · 无定时器
// ============================================================

// ============ 配色(幕布深蓝 × 聚光金 × 幕布红) ============
interface CBColorPalette {
  bg: string
  card: string
  deep: string
  line: string
  red: string
  redDeep: string
  gold: string
  goldDeep: string
  cream: string
  textMain: string
  textSub: string
  textHint: string
}

const CB_COLORS: CBColorPalette = {
  bg: '#141021',
  card: '#1F1930',
  deep: '#0C0916',
  line: '#352B4A',
  red: '#A32330',
  redDeep: '#7C1B24',
  gold: '#E9B84C',
  goldDeep: '#C99A2E',
  cream: '#F2E8CE',
  textMain: '#F5EFE0',
  textSub: '#A89CB8',
  textHint: '#6E6280'
}

// ============ Tab 定义 ============
enum CBTab {
  Theater = 0,
  OpenMic = 1,
  Sessions = 2,
  Actors = 3,
  Tickets = 4,
  Me = 5
}

interface CBTabItem {
  tab: CBTab
  icon: string
  label: string
}

const CB_TABS: CBTabItem[] = [
  { tab: CBTab.Theater, icon: '🎭', label: '剧场' },
  { tab: CBTab.OpenMic, icon: '🎤', label: '开放麦' },
  { tab: CBTab.Sessions, icon: '📅', label: '场次' },
  { tab: CBTab.Actors, icon: '🌟', label: '演员' },
  { tab: CBTab.Tickets, icon: '🎟️', label: '票夹' },
  { tab: CBTab.Me, icon: '👤', label: '观众档' }
]

// ============ 数据接口 ============
interface CBShow {
  id: number
  name: string
  host: string
  grad1: string
  grad2: string
  price: number
  oldPrice: number
  remain: number
  total: number
  date: string
  tag: string
}

interface CBSignup {
  id: number
  name: string
  color: string
  form: string
  minutes: number
  topic: string
  status: string
}

interface CBSession {
  id: number
  name: string
  date: string
  time: string
  venue: string
  price: number
  remain: number
  avatars: string[]
  hot: boolean
}

interface CBActor {
  id: number
  name: string
  emoji: string
  color: string
  style: string
  fans: number
  laughs: number
  years: number
  shows: number
}

interface CBTicket {
  id: number
  show: string
  date: string
  seat: string
  price: number
  status: string
  zone: string
}

interface CBMeme {
  rank: number
  name: string
  actor: string
  heat: number
}

interface CBJoke {
  id: number
  text: string
  actor: string
  likes: number
  tag: string
}

interface CBWeekDay {
  day: string
  db: number
}

interface CBPref {
  name: string
  pct: number
  color: string
}

interface CBWeekCell {
  day: string
  s1: string
  s2: string
  s3: string
}

interface CBStat {
  label: string
  value: string
  sub: string
  color: string
}

interface CBBadge {
  name: string
  emoji: string
}

interface CBNova {
  name: string
  style: string
  desc: string
  emoji: string
}

interface CBRepreShow {
  name: string
  tag: string
  color: string
}

interface CBSettingItem {
  icon: string
  label: string
  value: string
}

// ============ 全局硬编码数据 ============
const CB_SHOWS: CBShow[] = [
  { id: 1, name: '周一 emo 治愈专场', host: '东北跑火车 × 阿慧不emo', grad1: '#A32330', grad2: '#7C1B24', price: 120, oldPrice: 150, remain: 23, total: 180, date: '08-31 周一 20:00', tag: '治愈系' },
  { id: 2, name: '打工人的班味净化之夜', host: '大鹏不挪 × Tony马', grad1: '#7C1B24', grad2: '#141021', price: 99, oldPrice: 128, remain: 56, total: 240, date: '09-01 周二 20:00', tag: '职场吐槽' },
  { id: 3, name: '周三谐音梗大赛', host: '谐音梗老段 × 二棉裤', grad1: '#C99A2E', grad2: '#7C1B24', price: 88, oldPrice: 110, remain: 8, total: 120, date: '09-02 周三 19:30', tag: '谐音梗' },
  { id: 4, name: '恋爱脑观察室', host: '小鹿乱撞 × 情感导师阿紫', grad1: '#A32330', grad2: '#352B4A', price: 108, oldPrice: 138, remain: 41, total: 200, date: '09-03 周四 20:00', tag: '恋爱观察' },
  { id: 5, name: '深夜黑色幽默剧场', host: '冷面Kiki × 神秘嘉宾', grad1: '#141021', grad2: '#0C0916', price: 128, oldPrice: 158, remain: 12, total: 100, date: '09-04 周五 22:00', tag: '黑色幽默' },
  { id: 6, name: '周末全家欢乐场', host: '大棉袄 × 二棉裤', grad1: '#E9B84C', grad2: '#C99A2E', price: 80, oldPrice: 100, remain: 120, total: 300, date: '09-05 周六 15:00', tag: '合家欢' }
]

const CB_SIGNUPS: CBSignup[] = [
  { id: 1, name: '东北跑火车', color: '#A32330', form: '单口', minutes: 8, topic: '我妈的广场舞江湖', status: '通过' },
  { id: 2, name: '阿慧不emo', color: '#E9B84C', form: '单口', minutes: 7, topic: '减肥失败的一万种理由', status: '通过' },
  { id: 3, name: '大鹏不挪', color: '#C99A2E', form: '漫才', minutes: 12, topic: '老板的一百个心眼', status: '待审' },
  { id: 4, name: 'Tony马', color: '#7C1B24', form: '即兴', minutes: 10, topic: '职场黑话十级现场', status: '待审' },
  { id: 5, name: '小鹿乱撞', color: '#352B4A', form: '单口', minutes: 6, topic: '相亲奇遇记', status: '驳回' },
  { id: 6, name: '冷面Kiki', color: '#6E6280', form: '单口', minutes: 9, topic: '冷场也是一种艺术', status: '通过' },
  { id: 7, name: '二棉裤', color: '#F2E8CE', form: '漫才', minutes: 12, topic: '东北澡堂风云', status: '待审' },
  { id: 8, name: '谐音梗老段', color: '#CD8A4E', form: '单口', minutes: 5, topic: '谐音梗扣钱实录', status: '驳回' }
]

const CB_SESSIONS: CBSession[] = [
  { id: 1, name: '周一 emo 治愈专场', date: '08-31', time: '周一 20:00', venue: '小剧场', price: 120, remain: 23, avatars: ['🎭', '🎤', '🚂', '🦋'], hot: true },
  { id: 2, name: '班味净化之夜', date: '09-01', time: '周二 20:00', venue: '大舞台', price: 99, remain: 56, avatars: ['🐧', '💇', '🧑‍💼'], hot: true },
  { id: 3, name: '周三谐音梗大赛', date: '09-02', time: '周三 19:30', venue: '小剧场', price: 88, remain: 8, avatars: ['🀄', '🧣', '🎭'], hot: true },
  { id: 4, name: '恋爱脑观察室', date: '09-03', time: '周四 20:00', venue: '大舞台', price: 108, remain: 41, avatars: ['🦌', '💜', '🎤'], hot: false },
  { id: 5, name: '深夜黑色幽默剧场', date: '09-04', time: '周五 22:00', venue: '小剧场', price: 128, remain: 12, avatars: ['🐈', '🎭', '🌑'], hot: true },
  { id: 6, name: '周末全家欢乐场', date: '09-05', time: '周六 15:00', venue: '大舞台', price: 80, remain: 120, avatars: ['🧥', '🧣', '🎪', '👨‍👩‍👧'], hot: false },
  { id: 7, name: '新人开放麦汇报场', date: '09-06', time: '周日 19:00', venue: '小剧场', price: 39, remain: 35, avatars: ['🌱', '🎤', '🎤'], hot: false },
  { id: 8, name: '女性专场·姐妹夜', date: '09-07', time: '周一 20:00', venue: '大舞台', price: 118, remain: 27, avatars: ['🦋', '💜', '🌟'], hot: true }
]

const CB_ACTORS: CBActor[] = [
  { id: 1, name: '东北跑火车', emoji: '🚂', color: '#A32330', style: '东北生活流', fans: 28.6, laughs: 42, years: 8, shows: 6 },
  { id: 2, name: '阿慧不emo', emoji: '🦋', color: '#E9B84C', style: '女性视角', fans: 22.3, laughs: 38, years: 5, shows: 4 },
  { id: 3, name: '大鹏不挪', emoji: '🐧', color: '#C99A2E', style: '职场吐槽', fans: 19.8, laughs: 35, years: 6, shows: 5 },
  { id: 4, name: 'Tony马', emoji: '💇', color: '#7C1B24', style: '职场黑话', fans: 15.2, laughs: 31, years: 4, shows: 3 },
  { id: 5, name: '小鹿乱撞', emoji: '🦌', color: '#352B4A', style: '恋爱观察', fans: 12.7, laughs: 27, years: 3, shows: 2 },
  { id: 6, name: '冷面Kiki', emoji: '🐈', color: '#6E6280', style: '黑色幽默', fans: 11.4, laughs: 26, years: 5, shows: 4 },
  { id: 7, name: '二棉裤', emoji: '🧣', color: '#F2E8CE', style: '东北漫才', fans: 9.8, laughs: 24, years: 3, shows: 2 },
  { id: 8, name: '谐音梗老段', emoji: '🀄', color: '#CD8A4E', style: '谐音梗', fans: 8.6, laughs: 22, years: 7, shows: 5 },
  { id: 9, name: '情感导师阿紫', emoji: '💜', color: '#A32330', style: '情感毒舌', fans: 7.9, laughs: 20, years: 4, shows: 2 },
  { id: 10, name: '大棉袄', emoji: '🧥', color: '#E9B84C', style: '合家欢', fans: 6.4, laughs: 18, years: 6, shows: 3 }
]

const CB_TICKETS: CBTicket[] = [
  { id: 1, show: '周三谐音梗大赛', date: '09-02 19:30', seat: 'A区3排8号', price: 88, status: '未使用', zone: 'A' },
  { id: 2, show: '周一 emo 治愈专场', date: '08-24 20:00', seat: 'B区5排12号', price: 120, status: '已核销', zone: 'B' },
  { id: 3, show: '班味净化之夜', date: '08-18 20:00', seat: 'A区2排5号', price: 99, status: '已核销', zone: 'A' },
  { id: 4, show: '恋爱脑观察室', date: '08-11 20:00', seat: 'C区8排3号', price: 108, status: '已过期', zone: 'C' },
  { id: 5, show: '深夜黑色幽默剧场', date: '08-04 22:00', seat: 'B区6排9号', price: 128, status: '已过期', zone: 'B' },
  { id: 6, show: '周末全家欢乐场', date: '07-28 15:00', seat: 'A区1排2号', price: 80, status: '已核销', zone: 'A' }
]

const CB_MEMES: CBMeme[] = [
  { rank: 1, name: '「班味」到底是个什么味', actor: '东北跑火车', heat: 9820 },
  { rank: 2, name: '我妈说我是捡来的Pro Max', actor: '阿慧不emo', heat: 8710 },
  { rank: 3, name: '老板的「简单改改」', actor: '大鹏不挪', heat: 7640 },
  { rank: 4, name: '职场黑话翻译机', actor: 'Tony马', heat: 6520 },
  { rank: 5, name: '相亲对象的房贷哲学', actor: '小鹿乱撞', heat: 5480 },
  { rank: 6, name: '冷场才是最高级的幽默', actor: '冷面Kiki', heat: 4730 },
  { rank: 7, name: '东北澡堂社交学', actor: '二棉裤', heat: 3910 },
  { rank: 8, name: '谐音梗扣钱实录', actor: '谐音梗老段', heat: 3260 }
]

const CB_JOKES: CBJoke[] = [
  { id: 1, text: '我妈跳广场舞跳出了派系斗争,她现在是C位争夺战的主力选手。', actor: '东北跑火车', likes: 1204, tag: '生活流' },
  { id: 2, text: '减肥失败的理由我有一万个,但吃饭的理由只需要一个:饿了。', actor: '阿慧不emo', likes: 986, tag: '生活流' },
  { id: 3, text: '老板说这个需求很简单,翻译过来就是:今晚别走了。', actor: '大鹏不挪', likes: 875, tag: '职场' },
  { id: 4, text: '对齐一下颗粒度,赋能一下抓手,闭环一下——就是说人话会死吗?', actor: 'Tony马', likes: 743, tag: '职场' },
  { id: 5, text: '相亲对象问我有没有房,我说有啊,移动的,随套餐赠送。', actor: '小鹿乱撞', likes: 662, tag: '恋爱' },
  { id: 6, text: '冷场不可怕,可怕的是观众在冷场里笑出了声。', actor: '冷面Kiki', likes: 528, tag: '黑色幽默' }
]

const CB_WEEK: CBWeekDay[] = [
  { day: '周一', db: 78 },
  { day: '周二', db: 92 },
  { day: '周三', db: 85 },
  { day: '周四', db: 96 },
  { day: '周五', db: 104 },
  { day: '周六', db: 118 },
  { day: '周日', db: 88 }
]

const CB_PREFS: CBPref[] = [
  { name: '生活流', pct: 34, color: '#E9B84C' },
  { name: '职场', pct: 28, color: '#A32330' },
  { name: '恋爱', pct: 18, color: '#C99A2E' },
  { name: '谐音梗', pct: 12, color: '#7C1B24' },
  { name: '黑色幽默', pct: 8, color: '#6E6280' }
]

const CB_WEEK_GRID: CBWeekCell[] = [
  { day: '周一', s1: '晚', s2: '下', s3: '休' },
  { day: '周二', s1: '满', s2: '下', s3: '休' },
  { day: '周三', s1: '晚', s2: '满', s3: '休' },
  { day: '周四', s1: '晚', s2: '下', s3: '休' },
  { day: '周五', s1: '满', s2: '晚', s3: '满' },
  { day: '周六', s1: '满', s2: '满', s3: '晚' },
  { day: '周日', s1: '晚', s2: '满', s3: '下' }
]

const CB_GRID_ROWS: string[] = ['晚场 20:00', '下午场 15:00', '加场 17:30']

const CB_STATS: CBStat[] = [
  { label: '观演数', value: '46', sub: '场', color: '#E9B84C' },
  { label: '大笑次数', value: '1024', sub: '次', color: '#A32330' },
  { label: '收藏段子', value: '68', sub: '条', color: '#C99A2E' },
  { label: '连续到场', value: '9', sub: '周', color: '#7C1B24' }
]

const CB_BADGES: CBBadge[] = [
  { name: '夜场常客', emoji: '🌙' },
  { name: '段子收藏家', emoji: '📚' },
  { name: '全勤之星', emoji: '⭐' },
  { name: '敢笑先锋', emoji: '😤' },
  { name: '弹幕嘴替', emoji: '💬' },
  { name: '周三钉子户', emoji: '📌' }
]

const CB_NOVA: CBNova = {
  name: '铁锅炖自己',
  style: '东北漫才 · 新人',
  desc: '本月开放麦黑马,三连爆梗直接炸场,下月火速签约候选。',
  emoji: '🍲'
}

const CB_REPRE: CBRepreShow[] = [
  { name: '火车开进大剧院', tag: '2025 巡演', color: '#A32330' },
  { name: '广场舞之巅', tag: '2024 专场', color: '#E9B84C' },
  { name: '澡堂宇宙', tag: '2023 专场', color: '#7C1B24' },
  { name: '东北往事哈哈版', tag: '2022 专场', color: '#C99A2E' }
]

const CB_SETTINGS: CBSettingItem[] = [
  { icon: '🔔', label: '开票提醒', value: '已开启' },
  { icon: '💬', label: '弹幕显示', value: '开' },
  { icon: '🔒', label: '票夹指纹锁', value: '关' },
  { icon: '🧹', label: '清理缓存', value: '23.6MB' }
]

const CB_FORMS: string[] = ['单口', '漫才', '即兴']

// ============ 全局纯函数 ============
function cbCellBg(s: string): string {
  if (s === '晚') {
    return '#E9B84C'
  }
  if (s === '满') {
    return '#A32330'
  }
  if (s === '下') {
    return '#1F1930'
  }
  return '#0C0916'
}

function cbCellTx(s: string): string {
  if (s === '晚') {
    return '#141021'
  }
  if (s === '满') {
    return '#F5EFE0'
  }
  if (s === '下') {
    return '#E9B84C'
  }
  return '#6E6280'
}

function cbCellText(s: string): string {
  if (s === '晚') {
    return '晚'
  }
  if (s === '满') {
    return '满'
  }
  if (s === '下') {
    return '下'
  }
  return '休'
}

function cbStatusColor(s: string): string {
  if (s === '通过') {
    return '#3E9E5F'
  }
  if (s === '待审') {
    return '#E9B84C'
  }
  return '#D94848'
}

function cbFormBg(f: string): string {
  if (f === '单口') {
    return '#352B4A'
  }
  if (f === '漫才') {
    return '#7C1B24'
  }
  return '#C99A2E'
}

function cbTicketColor(s: string): string {
  if (s === '未使用') {
    return '#E9B84C'
  }
  if (s === '已核销') {
    return '#3E9E5F'
  }
  return '#6E6280'
}

function cbRankColor(r: number): string {
  if (r === 1) {
    return '#E9B84C'
  }
  if (r === 2) {
    return '#D8D8E8'
  }
  if (r === 3) {
    return '#CD8A4E'
  }
  return '#A89CB8'
}

function cbDbBar(db: number): string {
  let h: number = db / 120 * 96
  return h.toFixed(0) + 'vp'
}

function cbLaughPct(l: number): string {
  let p: number = l / 45 * 100
  return p.toFixed(0) + '%'
}

function cbHeatBar(h: number): string {
  let p: number = h / 9820 * 100
  return p.toFixed(0) + '%'
}

function cbSeatSold(r: number, c: number): boolean {
  return (r * 7 + c * 3) % 5 === 2
}

function cbSeatPrice(r: number): number {
  if (r < 3) {
    return 280
  }
  return 180
}

function cbSeatKey(r: number, c: number): string {
  return (r + 1) + '排' + (c + 1) + '号'
}

function cbSeatsTotal(seats: string[]): number {
  let t: number = 0
  for (let i = 0; i < seats.length; i++) {
    t = t + cbSeatPrice(Number(seats[i].substring(0, 1)) - 1)
  }
  return t
}

function cbMemeHeat(h: number): string {
  if (h > 8000) {
    return '爆'
  }
  if (h > 5000) {
    return '热'
  }
  return '温'
}

// ============ 弹框组件1:报名开放麦(舞台申请式) ============
@Component
struct CBSignOpenMicForm {
  @State micName: string = ''
  @State pickForm: string = '单口'
  @State minutes: number = 8
  @State topic: string = ''
  @State exp: string = ''
  onConfirm: () => void = () => {}
  onClose: () => void = () => {}

  build() {
    Column() {
      Row() {
        Column() {
          Text('🎤 申请登台').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#F5EFE0')
          Text('下期开放麦 · 09-06 周日 19:00').fontSize(10).fontColor('#A89CB8').margin({ top: 2 })
        }
        .alignItems(HorizontalAlign.Start)
        Column().layoutWeight(1)
        Text('✕').fontSize(18).fontColor('#6E6280')
          .onClick(() => { this.onClose() })
      }
      .width('100%').padding({ left: 20, right: 20, top: 18, bottom: 10 })

      Scroll() {
        Column() {
          Text('艺名').fontSize(12).fontColor('#A89CB8').width('100%').margin({ top: 6 })
          TextInput({ placeholder: '如:东北跑火车' })
            .placeholderColor('#6E6280').fontColor('#F5EFE0').fontSize(14).width('100%')
            .backgroundColor('#141021').borderRadius(10)
            .margin({ top: 6 })
            .onChange((v: string) => { this.micName = v })

          Text('表演形式').fontSize(12).fontColor('#A89CB8').width('100%').margin({ top: 14 })
          Row() {
            ForEach(CB_FORMS, (f: string) => {
              if (this.pickForm === f) {
                Text(f)
                  .fontSize(12).fontWeight(FontWeight.Bold).fontColor('#141021').backgroundColor('#E9B84C')
                  .padding({ left: 16, right: 16, top: 7, bottom: 7 }).borderRadius(16)
                  .margin({ left: 6 })
              } else {
                Text(f)
                  .fontSize(12).fontColor('#A89CB8').backgroundColor('#141021')
                  .padding({ left: 16, right: 16, top: 7, bottom: 7 }).borderRadius(16)
                  .margin({ left: 6 })
                  .onClick(() => { this.pickForm = f })
              }
            })
          }
          .width('100%').margin({ top: 8 })

          Text('表演时长(分钟)').fontSize(12).fontColor('#A89CB8').width('100%').margin({ top: 14 })
          Row() {
            Text('−').fontSize(20).fontColor('#141021').backgroundColor('#E9B84C')
              .width(34).height(34).borderRadius(17).textAlign(TextAlign.Center)
              .onClick(() => { if (this.minutes > 3) { this.minutes = this.minutes - 1 } })
            Text(this.minutes.toString() + ' 分钟')
              .fontSize(18).fontWeight(FontWeight.Bold).fontColor('#E9B84C')
              .margin({ left: 20, right: 20 })
            Text('+').fontSize(18).fontColor('#141021').backgroundColor('#E9B84C')
              .width(34).height(34).borderRadius(17).textAlign(TextAlign.Center)
              .onClick(() => { if (this.minutes < 15) { this.minutes = this.minutes + 1 } })
            Column().layoutWeight(1)
            Text('≤ 15 分钟')
              .fontSize(10).fontColor('#6E6280')
          }
          .width('100%').margin({ top: 8 })

          Text('段子主题').fontSize(12).fontColor('#A89CB8').width('100%').margin({ top: 14 })
          TextInput({ placeholder: '如:我妈的广场舞江湖' })
            .placeholderColor('#6E6280').fontColor('#F5EFE0').fontSize(14).width('100%')
            .backgroundColor('#141021').borderRadius(10)
            .margin({ top: 6 })
            .onChange((v: string) => { this.topic = v })

          Text('过往经验').fontSize(12).fontColor('#A89CB8').width('100%').margin({ top: 14 })
          TextArea({ placeholder: '上过几次开放麦?炸过场还是冷过场?写写看...' })
            .placeholderColor('#6E6280').fontColor('#F5EFE0').fontSize(13).width('100%').height(74)
            .backgroundColor('#141021').borderRadius(10)
            .margin({ top: 6 })
            .onChange((v: string) => { this.exp = v })

          Row() {
            Text('💡 新人首次报名免审直通').fontSize(10).fontColor('#6E6280')
          }
          .width('100%').margin({ top: 10 })
        }
        .padding({ left: 20, right: 20, bottom: 10 })
        .alignItems(HorizontalAlign.Start)
      }
      .scrollable(ScrollDirection.Vertical)
      .layoutWeight(1).scrollBar(BarState.Off)
      .constraintSize({ maxHeight: '52%' })

      Row() {
        Text('再想想').fontSize(14).fontColor('#A89CB8')
          .backgroundColor('#141021').borderRadius(20)
          .padding({ left: 26, right: 26, top: 10, bottom: 10 })
          .onClick(() => { this.onClose() })
        Text('提交登台申请').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#141021')
          .backgroundColor('#E9B84C').borderRadius(20)
          .padding({ left: 24, right: 24, top: 10, bottom: 10 })
          .margin({ left: 12 })
          .onClick(() => { this.onConfirm() })
      }
      .width('100%').justifyContent(FlexAlign.Center)
      .padding({ top: 12, bottom: 16 })
    }
    .width('100%').backgroundColor('#1F1930').borderRadius(16)
  }
}

// ============ 弹框组件2:购票选座(选座图式) ============
@Component
struct CBBuyTicketForm {
  showName: string = '周一 emo 治愈专场'
  price: number = 120
  oldPrice: number = 150
  @State selSeats: string[] = []
  onConfirm: () => void = () => {}
  onClose: () => void = () => {}

  build() {
    Column() {
      Row() {
        Column() {
          Text('🎟️ 选座购票').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#F5EFE0')
          Text(this.showName).fontSize(10).fontColor('#A89CB8').margin({ top: 2 }).maxLines(1)
        }
        .alignItems(HorizontalAlign.Start)
        Column().layoutWeight(1)
        Text('✕').fontSize(18).fontColor('#6E6280')
          .onClick(() => { this.onClose() })
      }
      .width('100%').padding({ left: 20, right: 20, top: 18, bottom: 8 })

      Scroll() {
        Column() {
          // ===== 座位区块图 =====
          Text('━━━━━━ 🎭 舞 台 ━━━━━━').fontSize(10).fontColor('#E9B84C')
            .letterSpacing(2).margin({ top: 4 })

          Text('前区 A-C 排(¥280/座)').fontSize(10).fontColor('#A89CB8')
            .width('100%').margin({ top: 12 })
          Column() {
            ForEach([0, 1, 2], (r: number) => {
              Row() {
                ForEach([0, 1, 2, 3, 4, 5, 6, 7], (c: number) => {
                  if (cbSeatSold(r, c)) {
                    Column()
                      .width(26).height(22).backgroundColor('#352B4A')
                      .borderRadius(5).margin({ left: 3, right: 3 })
                  } else {
                    if (this.selSeats.indexOf(cbSeatKey(r, c)) >= 0) {
                      Column()
                        .width(26).height(22).backgroundColor('#E9B84C')
                        .border({ width: 2, color: '#F5EFE0' })
                        .borderRadius(5).margin({ left: 3, right: 3 })
                        .onClick(() => {
                          this.selSeats = this.selSeats.filter((s: string) => s !== cbSeatKey(r, c))
                        })
                    } else {
                      Column()
                        .width(26).height(22).backgroundColor('#141021')
                        .border({ width: 1, color: '#6E6280' })
                        .borderRadius(5).margin({ left: 3, right: 3 })
                        .onClick(() => {
                          if (this.selSeats.length < 4) {
                            this.selSeats = this.selSeats.concat([cbSeatKey(r, c)])
                          }
                        })
                    }
                  }
                })
              }
              .width('100%').justifyContent(FlexAlign.Center)
              .margin({ top: 5 })
            })
          }
          .width('100%')

          Text('后区 D-E 排(¥180/座)').fontSize(10).fontColor('#A89CB8')
            .width('100%').margin({ top: 12 })
          Column() {
            ForEach([3, 4], (r: number) => {
              Row() {
                ForEach([0, 1, 2, 3, 4, 5, 6, 7], (c: number) => {
                  if (cbSeatSold(r, c)) {
                    Column()
                      .width(26).height(22).backgroundColor('#352B4A')
                      .borderRadius(5).margin({ left: 3, right: 3 })
                  } else {
                    if (this.selSeats.indexOf(cbSeatKey(r, c)) >= 0) {
                      Column()
                        .width(26).height(22).backgroundColor('#E9B84C')
                        .border({ width: 2, color: '#F5EFE0' })
                        .borderRadius(5).margin({ left: 3, right: 3 })
                        .onClick(() => {
                          this.selSeats = this.selSeats.filter((s: string) => s !== cbSeatKey(r, c))
                        })
                    } else {
                      Column()
                        .width(26).height(22).backgroundColor('#141021')
                        .border({ width: 1, color: '#6E6280' })
                        .borderRadius(5).margin({ left: 3, right: 3 })
                        .onClick(() => {
                          if (this.selSeats.length < 4) {
                            this.selSeats = this.selSeats.concat([cbSeatKey(r, c)])
                          }
                        })
                    }
                  }
                })
              }
              .width('100%').justifyContent(FlexAlign.Center)
              .margin({ top: 5 })
            })
          }
          .width('100%')

          Row() {
            Column().width(12).height(10).backgroundColor('#E9B84C').borderRadius(3)
            Text('可选').fontSize(9).fontColor('#A89CB8').margin({ left: 4 })
            Column().width(12).height(10).backgroundColor('#352B4A').borderRadius(3).margin({ left: 12 })
            Text('已售').fontSize(9).fontColor('#A89CB8').margin({ left: 4 })
            Column().width(12).height(10).backgroundColor('#141021')
              .border({ width: 1, color: '#6E6280' }).borderRadius(3).margin({ left: 12 })
            Text('空位').fontSize(9).fontColor('#A89CB8').margin({ left: 4 })
            Column().layoutWeight(1)
            Text('最多选 4 座').fontSize(9).fontColor('#6E6280')
          }
          .width('100%').justifyContent(FlexAlign.Center)
          .margin({ top: 10 })
        }
        .padding({ left: 14, right: 14, bottom: 8 })
        .alignItems(HorizontalAlign.Start)
      }
      .scrollable(ScrollDirection.Vertical)
      .scrollBar(BarState.Off)
      .constraintSize({ maxHeight: '46%' })

      // ===== 所选座位展示行 =====
      Row() {
        Text('已选').fontSize(11).fontColor('#A89CB8')
        Scroll() {
          Row() {
            ForEach(this.selSeats, (s: string) => {
              Text(s)
                .fontSize(10).fontColor('#141021').backgroundColor('#E9B84C')
                .padding({ left: 8, right: 8, top: 4, bottom: 4 }).borderRadius(10)
                .margin({ left: 4 })
            })
          }
        }
        .scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off)
        .layoutWeight(1).margin({ left: 6 })
        if (this.selSeats.length === 0) {
          Text('点击座位图选座').fontSize(10).fontColor('#6E6280').margin({ left: 6 })
        }
      }
      .width('100%').padding({ left: 20, right: 20, top: 8, bottom: 6 })

      Divider().color('#352B4A').margin({ left: 20, right: 20 })

      // ===== 票价明细 =====
      Row() {
        Column() {
          Text('早鸟价 ¥' + this.price.toString()).fontSize(18).fontWeight(FontWeight.Bold).fontColor('#E9B84C')
          Text('原价 ¥' + this.oldPrice.toString()).fontSize(11).fontColor('#6E6280')
            .decoration({ type: TextDecorationType.LineThrough })
            .margin({ top: 2 })
        }
        .alignItems(HorizontalAlign.Start)
        Column().layoutWeight(1)
        Column() {
          Text('¥' + cbSeatsTotal(this.selSeats).toString()).fontSize(20).fontWeight(FontWeight.Bold).fontColor('#F5EFE0')
          Text(this.selSeats.length.toString() + ' 张 · 含茶水').fontSize(9).fontColor('#6E6280').margin({ top: 2 })
        }
        .alignItems(HorizontalAlign.End)
      }
      .width('100%').padding({ left: 20, right: 20, top: 8 })

      Row() {
        Text('放弃').fontSize(14).fontColor('#A89CB8')
          .backgroundColor('#141021').borderRadius(20)
          .padding({ left: 28, right: 28, top: 10, bottom: 10 })
          .onClick(() => { this.onClose() })
        Text('确认购票').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#141021')
          .backgroundColor('#A32330').borderRadius(20)
          .padding({ left: 30, right: 30, top: 10, bottom: 10 })
          .margin({ left: 12 })
          .onClick(() => { this.onConfirm() })
      }
      .width('100%').justifyContent(FlexAlign.Center)
      .padding({ top: 12, bottom: 16 })
    }
    .width('100%').backgroundColor('#1F1930').borderRadius(16)
  }
}

// ============ 弹框组件3:编辑报名(底部抽屉式) ============
@Component
struct CBEditOpenMicForm {
  signupName: string = '大鹏不挪'
  signupTopic: string = '老板的一百个心眼'
  signupForm: string = '漫才'
  @State newTopic: string = ''
  @State newForm: string = ''
  @State minutes: number = 8
  onSave: () => void = () => {}
  onWithdraw: () => void = () => {}
  onClose: () => void = () => {}

  aboutToAppear() {
    this.newTopic = this.signupTopic
    this.newForm = this.signupForm
    this.minutes = 10
  }

  build() {
    Column() {
      Column()
        .width(44).height(5).borderRadius(3).backgroundColor('#352B4A')
        .margin({ top: 10 })

      Row() {
        Column() {
          Text('✏️ 编辑报名 · ' + this.signupName).fontSize(16).fontWeight(FontWeight.Bold).fontColor('#F5EFE0')
          Text('拖动抽屉上方空白处可关闭').fontSize(9).fontColor('#6E6280').margin({ top: 2 })
        }
        .alignItems(HorizontalAlign.Start)
        Column().layoutWeight(1)
        Text('✕').fontSize(18).fontColor('#6E6280')
          .onClick(() => { this.onClose() })
      }
      .width('100%').padding({ left: 20, right: 20, top: 10, bottom: 8 })

      Text('段子主题').fontSize(12).fontColor('#A89CB8').width('100%')
        .margin({ top: 6 })
      TextInput({ text: this.newTopic, placeholder: '改个更炸的主题' })
        .placeholderColor('#6E6280').fontColor('#F5EFE0').fontSize(14).width('100%')
        .backgroundColor('#141021').borderRadius(10)
        .margin({ top: 6 })
        .onChange((v: string) => { this.newTopic = v })

      Text('表演时长(分钟)').fontSize(12).fontColor('#A89CB8').width('100%').margin({ top: 14 })
      Row() {
        Text('−').fontSize(20).fontColor('#141021').backgroundColor('#E9B84C')
          .width(34).height(34).borderRadius(17).textAlign(TextAlign.Center)
          .onClick(() => { if (this.minutes > 3) { this.minutes = this.minutes - 1 } })
        Text(this.minutes.toString() + ' 分钟')
          .fontSize(18).fontWeight(FontWeight.Bold).fontColor('#E9B84C')
          .margin({ left: 20, right: 20 })
        Text('+').fontSize(18).fontColor('#141021').backgroundColor('#E9B84C')
          .width(34).height(34).borderRadius(17).textAlign(TextAlign.Center)
          .onClick(() => { if (this.minutes < 15) { this.minutes = this.minutes + 1 } })
        Column().layoutWeight(1)
      }
      .width('100%').margin({ top: 8 })

      Text('表演形式').fontSize(12).fontColor('#A89CB8').width('100%').margin({ top: 14 })
      Row() {
        ForEach(CB_FORMS, (f: string) => {
          if (this.newForm === f) {
            Text(f)
              .fontSize(12).fontWeight(FontWeight.Bold).fontColor('#141021').backgroundColor('#E9B84C')
              .padding({ left: 16, right: 16, top: 7, bottom: 7 }).borderRadius(16)
              .margin({ left: 6 })
          } else {
            Text(f)
              .fontSize(12).fontColor('#A89CB8').backgroundColor('#141021')
              .padding({ left: 16, right: 16, top: 7, bottom: 7 }).borderRadius(16)
              .margin({ left: 6 })
              .onClick(() => { this.newForm = f })
          }
        })
      }
      .width('100%').margin({ top: 8 })

      Row() {
        Text('保存修改').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#141021')
          .backgroundColor('#E9B84C').borderRadius(20)
          .padding({ left: 30, right: 30, top: 10, bottom: 10 })
          .onClick(() => { this.onSave() })
        Column().layoutWeight(1)
        Text('撤回报名').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#F5EFE0')
          .backgroundColor('#7C1B24').borderRadius(20)
          .padding({ left: 22, right: 22, top: 10, bottom: 10 })
          .onClick(() => { this.onWithdraw() })
      }
      .width('100%').padding({ left: 20, right: 20, top: 18 })

      Column() {
        Text('⚠️ 撤回后本季开放麦资格将释放,重新报名需排队').fontSize(9).fontColor('#6E6280')
      }
      .width('100%').alignItems(HorizontalAlign.Start)
      .padding({ left: 20, bottom: 18, top: 8 })
    }
    .width('100%').backgroundColor('#1F1930')
    .borderRadius({ topLeft: 24, topRight: 24 })
  }
}

// ============ 弹框组件4:演员详情(档案海报式) ============
@Component
struct CBActorForm {
  actor: CBActor = CB_ACTORS[0]
  @State followed: boolean = false
  onConfirm: () => void = () => {}
  onClose: () => void = () => {}

  build() {
    Column() {
      Row() {
        Text('🌟 演员档案').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#F5EFE0')
        Column().layoutWeight(1)
        Text('✕').fontSize(18).fontColor('#6E6280')
          .onClick(() => { this.onClose() })
      }
      .width('100%').padding({ left: 20, right: 20, top: 16, bottom: 8 })

      Scroll() {
        Column() {
          Stack() {
            Column()
              .width('100%').height(120)
              .linearGradient({
                direction: GradientDirection.Right,
                colors: [[this.actor.color, 0], ['#141021', 1]]
              })
              .borderRadius(14)
            Row() {
              Column() {
                Text(this.actor.emoji).fontSize(44)
              }
              .width(84).height(84).backgroundColor('#0C0916')
              .border({ width: 2, color: this.actor.color })
              .borderRadius(20)
              .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
              Column() {
                Text(this.actor.name).fontSize(20).fontWeight(FontWeight.Bold).fontColor('#F5EFE0')
                Row() {
                  Text(this.actor.style).fontSize(10).fontColor('#141021')
                    .backgroundColor('#E9B84C')
                    .padding({ left: 8, right: 8, top: 3, bottom: 3 }).borderRadius(10)
                  Text('签约演员').fontSize(10).fontColor('#F5EFE0')
                    .backgroundColor('#7C1B24')
                    .padding({ left: 8, right: 8, top: 3, bottom: 3 }).borderRadius(10)
                    .margin({ left: 6 })
                }
                .margin({ top: 6 })
                Text(this.actor.fans.toString() + ' 万粉丝关注TA').fontSize(10).fontColor('#A89CB8')
                  .margin({ top: 6 })
              }
              .alignItems(HorizontalAlign.Start).padding({ left: 14 })
            }
            .width('100%').padding({ left: 14, right: 14 })
            .alignItems(VerticalAlign.Center)
          }
          .width('100%').height(120)

          Row() {
            Column() {
              Text(this.actor.years.toString() + ' 年').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#E9B84C')
              Text('从业年限').fontSize(9).fontColor('#A89CB8').margin({ top: 2 })
            }
            .layoutWeight(1).alignItems(HorizontalAlign.Center)
            Column()
              .width(1).height(28).backgroundColor('#352B4A')
            Column() {
              Text(this.actor.shows.toString() + ' 部').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#E9B84C')
              Text('个人专场').fontSize(9).fontColor('#A89CB8').margin({ top: 2 })
            }
            .layoutWeight(1).alignItems(HorizontalAlign.Center)
            Column()
              .width(1).height(28).backgroundColor('#352B4A')
            Column() {
              Text(this.actor.fans.toString() + ' 万').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#E9B84C')
              Text('全网粉丝').fontSize(9).fontColor('#A89CB8').margin({ top: 2 })
            }
            .layoutWeight(1).alignItems(HorizontalAlign.Center)
          }
          .width('100%').padding({ top: 14, bottom: 10 })

          Text('代表专场').fontSize(12).fontColor('#A89CB8').width('100%').margin({ top: 4 })
          Scroll() {
            Row() {
              ForEach(CB_REPRE, (r: CBRepreShow) => {
                Column() {
                  Column()
                    .width(108).height(66)
                    .backgroundColor(r.color).opacity(0.85)
                    .borderRadius(12)
                  Text(r.name).fontSize(11).fontWeight(FontWeight.Bold).fontColor('#F5EFE0')
                    .margin({ top: 6 }).maxLines(1)
                  Text(r.tag).fontSize(9).fontColor('#6E6280').margin({ top: 2 })
                }
                .width(108).alignItems(HorizontalAlign.Start)
                .margin({ left: 8 })
              })
            }
          }
          .scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off)
          .width('100%').margin({ top: 8 })
        }
        .padding({ left: 20, right: 12, bottom: 10 })
        .alignItems(HorizontalAlign.Start)
      }
      .scrollable(ScrollDirection.Vertical)
      .layoutWeight(1).scrollBar(BarState.Off)
      .constraintSize({ maxHeight: '38%' })

      Row() {
        Text('关闭档案').fontSize(14).fontColor('#A89CB8')
          .backgroundColor('#141021').borderRadius(20)
          .padding({ left: 26, right: 26, top: 10, bottom: 10 })
          .onClick(() => { this.onClose() })
        if (this.followed) {
          Text('已关注 ✓').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#141021')
            .backgroundColor('#352B4A').borderRadius(20)
            .padding({ left: 30, right: 30, top: 10, bottom: 10 })
            .margin({ left: 12 })
            .onClick(() => { this.followed = false })
        } else {
          Text('+ 关注').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#141021')
            .backgroundColor('#E9B84C').borderRadius(20)
            .padding({ left: 34, right: 34, top: 10, bottom: 10 })
            .margin({ left: 12 })
            .onClick(() => { this.followed = true })
        }
      }
      .width('100%').justifyContent(FlexAlign.Center)
      .padding({ top: 10, bottom: 16 })
    }
    .width('100%').backgroundColor('#1F1930').borderRadius(16)
  }
}

// ============ 弹框组件5:发布观后感(弹幕卡片式) ============
@Component
struct CBReviewForm {
  @State pickSession: string = '周一 emo 治愈专场'
  @State stars: number = 4
  @State laughIdx: number = 7
  @State comment: string = ''
  @State anonymous: boolean = true
  onConfirm: () => void = () => {}
  onClose: () => void = () => {}

  build() {
    Column() {
      Row() {
        Text('💬 发布观后感').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#F5EFE0')
        Column().layoutWeight(1)
        Text('✕').fontSize(18).fontColor('#6E6280')
          .onClick(() => { this.onClose() })
      }
      .width('100%').padding({ left: 20, right: 20, top: 18, bottom: 8 })

      // ===== 飘过的小弹幕 =====
      Row() {
        Text('"笑到缺氧"').fontSize(9).fontColor('#E9B84C')
          .backgroundColor('#141021').padding({ left: 8, right: 8, top: 4, bottom: 4 }).borderRadius(10)
        Text('"东北人自带BGM"').fontSize(9).fontColor('#A89CB8')
          .backgroundColor('#141021').padding({ left: 8, right: 8, top: 4, bottom: 4 }).borderRadius(10)
          .margin({ left: 6 })
        Text('"谐音梗扣钱!"').fontSize(9).fontColor('#D94848')
          .backgroundColor('#141021').padding({ left: 8, right: 8, top: 4, bottom: 4 }).borderRadius(10)
          .margin({ left: 6 })
      }
      .width('100%').padding({ left: 20, right: 20, top: 4, bottom: 8 })

      Scroll() {
        Column() {
          Text('选择场次').fontSize(12).fontColor('#A89CB8').width('100%')
          Column() {
            ForEach(CB_SESSIONS, (s: CBSession) => {
              if (s.id <= 4) {
                if (this.pickSession === s.name) {
                  Text(s.name)
                    .fontSize(11).fontWeight(FontWeight.Bold).fontColor('#141021').backgroundColor('#E9B84C')
                    .padding({ left: 10, right: 10, top: 6, bottom: 6 }).borderRadius(12)
                    .margin({ top: 6 }).alignSelf(ItemAlign.Start)
                } else {
                  Text(s.name)
                    .fontSize(11).fontColor('#A89CB8').backgroundColor('#141021')
                    .padding({ left: 10, right: 10, top: 6, bottom: 6 }).borderRadius(12)
                    .margin({ top: 6 }).alignSelf(ItemAlign.Start)
                    .onClick(() => { this.pickSession = s.name })
                }
              }
            })
          }
          .width('100%')

          Text('满意度星级').fontSize(12).fontColor('#A89CB8').width('100%').margin({ top: 12 })
          Row() {
            ForEach([1, 2, 3, 4, 5], (n: number) => {
              if (n <= this.stars) {
                Text('★').fontSize(26).fontColor('#E9B84C')
                  .margin({ right: 6 })
                  .onClick(() => { this.stars = n })
              } else {
                Text('☆').fontSize(26).fontColor('#352B4A')
                  .margin({ right: 6 })
                  .onClick(() => { this.stars = n })
              }
            })
            Column().layoutWeight(1)
            Text(this.stars.toString() + '.0 分').fontSize(12).fontColor('#E9B84C')
          }
          .width('100%').margin({ top: 6 })

          Text('笑点指数').fontSize(12).fontColor('#A89CB8').width('100%').margin({ top: 12 })
          Column() {
            Row() {
              ForEach([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], (n: number) => {
                if (n < this.laughIdx) {
                  Column()
                    .layoutWeight(1).height(12).backgroundColor('#A32330')
                    .margin({ left: 2, right: 2 }).borderRadius(3)
                    .onClick(() => { this.laughIdx = n + 1 })
                } else {
                  Column()
                    .layoutWeight(1).height(12).backgroundColor('#352B4A')
                    .margin({ left: 2, right: 2 }).borderRadius(3)
                    .onClick(() => { this.laughIdx = n + 1 })
                }
              })
            }
            .width('100%')
            Row() {
              Text('微笑').fontSize(9).fontColor('#6E6280')
              Column().layoutWeight(1)
              Text(cbLaughLabel(this.laughIdx)).fontSize(10).fontColor('#E9B84C')
              Column().layoutWeight(1)
              Text('笑劈叉').fontSize(9).fontColor('#6E6280')
            }
            .width('100%').margin({ top: 4 })
          }
          .width('100%').margin({ top: 6 })

          Text('短评').fontSize(12).fontColor('#A89CB8').width('100%').margin({ top: 12 })
          TextInput({ placeholder: '一句锐评,全场狂喜(20字内)' })
            .placeholderColor('#6E6280').fontColor('#F5EFE0').fontSize(14).width('100%')
            .backgroundColor('#141021').borderRadius(10)
            .margin({ top: 6 })
            .onChange((v: string) => { this.comment = v })

          Row() {
            Text('匿名发布').fontSize(12).fontColor('#A89CB8')
            Column().layoutWeight(1)
            Toggle({ type: ToggleType.Switch, isOn: this.anonymous })
              .selectedColor('#E9B84C')
              .onChange((on: boolean) => { this.anonymous = on })
          }
          .width('100%').margin({ top: 14, bottom: 6 })
        }
        .padding({ left: 20, right: 20, bottom: 10 })
        .alignItems(HorizontalAlign.Start)
      }
      .scrollable(ScrollDirection.Vertical)
      .layoutWeight(1).scrollBar(BarState.Off)
      .constraintSize({ maxHeight: '44%' })

      Row() {
        Text('不发了').fontSize(14).fontColor('#A89CB8')
          .backgroundColor('#141021').borderRadius(20)
          .padding({ left: 26, right: 26, top: 10, bottom: 10 })
          .onClick(() => { this.onClose() })
        Text('发射弹幕').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#141021')
          .backgroundColor('#E9B84C').borderRadius(20)
          .padding({ left: 28, right: 28, top: 10, bottom: 10 })
          .margin({ left: 12 })
          .onClick(() => { this.onConfirm() })
      }
      .width('100%').justifyContent(FlexAlign.Center)
      .padding({ top: 10, bottom: 16 })
    }
    .width('100%').backgroundColor('#1F1930').borderRadius(16)
  }
}

// 笑点指数标签(全局纯函数,UI 内不可声明变量)
function cbLaughLabel(idx: number): string {
  if (idx <= 3) {
    return '微笑指数 ' + idx.toString() + '/10'
  }
  if (idx <= 7) {
    return '大笑指数 ' + idx.toString() + '/10'
  }
  return '笑劈叉指数 ' + idx.toString() + '/10'
}

// ============ Tab1:剧场 ============
@Component
struct CBTheaterContent {
  @State spotOp: number = 0.9
  @State noteY: number = 0
  @State noteOp: number = 0.8
  onBuyTicket: (showName: string) => void = (showName: string) => {}

  aboutToAppear() {
    // 聚光锥形色块呼吸
    this.getUIContext().animateTo({
      duration: 2000,
      iterations: -1,
      playMode: PlayMode.Alternate,
      curve: Curve.EaseInOut
    }, () => {
      this.spotOp = 0.35
    })
    // 笑声音符上浮渐隐
    this.getUIContext().animateTo({
      duration: 1800,
      iterations: -1,
      playMode: PlayMode.Alternate,
      curve: Curve.EaseOut
    }, () => {
      this.noteY = -22
      this.noteOp = 0.1
    })
  }

  @Builder curtainStrip() {
    Row() {
      ForEach([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], (i: number) => {
        Column()
          .layoutWeight(1)
          .height(10)
          .backgroundColor(i % 2 === 0 ? '#A32330' : '#7C1B24')
      })
    }
    .width('100%').height(10)
  }

  @Builder memeRow(m: CBMeme) {
    Row() {
      Text(m.rank.toString()).fontSize(20).fontWeight(FontWeight.Bold)
        .fontColor(cbRankColor(m.rank)).width(34).textAlign(TextAlign.Center)
      Column() {
        Text(m.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor('#F5EFE0')
          .maxLines(1).width('100%')
        Text('来自 ' + m.actor).fontSize(10).fontColor('#A89CB8').margin({ top: 2 })
      }
      .layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 6 })
      Column() {
        Text('🔥' + m.heat.toString()).fontSize(12).fontWeight(FontWeight.Bold).fontColor('#E9B84C')
        Text(cbMemeHeat(m.heat)).fontSize(9).fontColor('#A89CB8').margin({ top: 2 })
      }
      .alignItems(HorizontalAlign.End)
    }
    .width('100%').padding({ top: 10, bottom: 10 })
    .border({ width: { bottom: 1 }, color: '#352B4A' })
  }

  build() {
    Column() {
      Scroll() {
        Column() {
          // ===== 本周爆笑专场大横幅 =====
          Stack() {
            Column()
              .width('100%').height(168)
              .linearGradient({
                direction: GradientDirection.RightBottom,
                colors: [[CB_SHOWS[0].grad1, 0], [CB_SHOWS[0].grad2, 1]]
              })
              .borderRadius(16)
            // 聚光锥
            Column()
              .width(120).height(150)
              .linearGradient({
                direction: GradientDirection.Bottom,
                colors: [['#E9B84C', 0], ['#E9B84C00', 1]]
              })
              .rotate({ x: 0, y: 0, z: 1, angle: -18 })
              .opacity(this.spotOp)
              .position({ x: 18, y: 0 })
            Column()
              .width(120).height(150)
              .linearGradient({
                direction: GradientDirection.Bottom,
                colors: [['#F2E8CE', 0], ['#F2E8CE00', 1]]
              })
              .rotate({ x: 0, y: 0, z: 1, angle: 18 })
              .opacity(this.spotOp)
              .position({ x: 225, y: 0 })
            // 上浮音符
            Text('😂').fontSize(16).translate({ y: this.noteY }).opacity(this.noteOp)
              .position({ x: 268, y: 26 })
            Text('🎵').fontSize(14).translate({ y: this.noteY }).opacity(this.noteOp)
              .position({ x: 300, y: 48 })
            Text('😆').fontSize(15).translate({ y: this.noteY }).opacity(this.noteOp)
              .position({ x: 30, y: 120 })

            Column() {
              Text('THIS WEEK · 爆笑专场').fontSize(9).fontColor('#E9B84C').letterSpacing(2)
              Text(CB_SHOWS[0].name).fontSize(21).fontWeight(FontWeight.Bold).fontColor('#F5EFE0')
                .margin({ top: 4 })
              Text(CB_SHOWS[0].host).fontSize(11).fontColor('#F2E8CE').margin({ top: 4 })
              Row() {
                Text(CB_SHOWS[0].date).fontSize(10).fontColor('#A89CB8')
                Text(CB_SHOWS[0].tag).fontSize(9).fontColor('#141021').backgroundColor('#E9B84C')
                  .padding({ left: 6, right: 6, top: 2, bottom: 2 }).borderRadius(8)
                  .margin({ left: 8 })
              }
              .width('100%').margin({ top: 6 })
              Row() {
                Column() {
                  Text('¥' + CB_SHOWS[0].price.toString()).fontSize(18).fontWeight(FontWeight.Bold).fontColor('#E9B84C')
                  Text('余票 ' + CB_SHOWS[0].remain.toString() + '/' + CB_SHOWS[0].total.toString())
                    .fontSize(9).fontColor('#F2E8CE').margin({ top: 2 })
                }
                .alignItems(HorizontalAlign.Start)
                Column().layoutWeight(1)
                Text('立即购票').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#141021')
                  .backgroundColor('#E9B84C').borderRadius(18)
                  .padding({ left: 18, right: 18, top: 8, bottom: 8 })
                  .onClick(() => { this.onBuyTicket(CB_SHOWS[0].name) })
              }
              .width('100%').alignItems(VerticalAlign.Bottom)
              .margin({ top: 8 })
            }
            .width('100%').padding({ left: 16, right: 16, top: 14, bottom: 14 })
            .alignItems(HorizontalAlign.Start)
          }
          .width('100%').height(168)
          .margin({ top: 10 })

          this.curtainStrip()

          // ===== 上座率环形进度 + 色条 =====
          Row() {
            Stack() {
              Progress({ value: 78, total: 100, type: ProgressType.Ring })
                .width(104).height(104)
                .style({ strokeWidth: 11 })
                .color('#E9B84C')
              Column() {
                Text('78%').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#E9B84C')
                Text('本周上座率').fontSize(9).fontColor('#A89CB8')
              }
              .alignItems(HorizontalAlign.Center)
            }
            .width(104).height(104)
            Column() {
              Text('全场 1,140 / 1,460 座已售').fontSize(12).fontWeight(FontWeight.Bold).fontColor('#F5EFE0')
              Row() {
                ForEach([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], (i: number) => {
                  if (i < 8) {
                    Column().layoutWeight(1).height(12).backgroundColor('#E9B84C')
                      .margin({ left: 2, right: 2 }).borderRadius(3)
                  } else {
                    Column().layoutWeight(1).height(12).backgroundColor('#352B4A')
                      .margin({ left: 2, right: 2 }).borderRadius(3)
                  }
                })
              }
              .width('100%').margin({ top: 8 })
              Text('金 = 已售 · 灰 = 空余 · 周六全场告急').fontSize(9).fontColor('#6E6280')
                .margin({ top: 6 })
            }
            .layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 16 })
          }
          .width('100%').padding(14).backgroundColor('#1F1930').borderRadius(16)
          .margin({ top: 10 })

          // ===== 本周热梗榜 =====
          Column() {
            Row() {
              Text('🔥 本周热梗榜').fontSize(15).fontWeight(FontWeight.Bold).fontColor('#F5EFE0')
              Column().layoutWeight(1)
              Text('剧场实时统计').fontSize(9).fontColor('#6E6280')
            }
            .width('100%').padding({ top: 12, bottom: 2 })
            Column() {
              ForEach(CB_MEMES, (m: CBMeme) => {
                this.memeRow(m)
              })
            }
            .width('100%').padding({ bottom: 10 })
          }
          .width('100%').backgroundColor('#1F1930').borderRadius(16)
          .padding({ left: 14, right: 14 }).margin({ top: 12 })

          // ===== 7 日场均笑声分贝柱状图 =====
          Column() {
            Text('📊 7 日场均笑声分贝(dB)').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#F5EFE0')
              .width('100%').padding({ top: 12, bottom: 8 })
            Row() {
              ForEach(CB_WEEK, (d: CBWeekDay, idx: number) => {
                Column() {
                  Text(d.db.toString()).fontSize(9)
                    .fontColor(idx === 5 ? '#E9B84C' : '#A89CB8')
                    .margin({ bottom: 3 })
                  Column()
                    .width(22)
                    .height(cbDbBar(d.db))
                    .backgroundColor(idx === 5 ? '#E9B84C' : (idx === 4 ? '#A32330' : '#7C1B24'))
                    .opacity(idx === 5 ? 1.0 : 0.75)
                    .borderRadius({ topLeft: 4, topRight: 4 })
                  Text(d.day).fontSize(9).fontColor('#A89CB8').margin({ top: 4 })
                }
                .layoutWeight(1).alignItems(HorizontalAlign.Center)
              })
            }
            .width('100%').padding({ left: 4, right: 4, bottom: 12 })
            .alignItems(VerticalAlign.Bottom)
          }
          .width('100%').backgroundColor('#1F1930').borderRadius(16)
          .padding({ left: 14, right: 14 }).margin({ top: 12 })

          // ===== 其他专场速购 =====
          Text('🎭 更多专场').fontSize(15).fontWeight(FontWeight.Bold).fontColor('#F5EFE0')
            .width('100%').padding({ top: 14, bottom: 4 })
          Column() {
            ForEach(CB_SHOWS, (s: CBShow) => {
              if (s.id > 1) {
                Row() {
                  Column()
                    .width(6).height(44).backgroundColor(s.grad1).borderRadius(3)
                  Column() {
                    Text(s.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor('#F5EFE0').maxLines(1)
                    Text(s.date + ' · ' + s.host).fontSize(10).fontColor('#A89CB8')
                      .margin({ top: 3 }).maxLines(1)
                  }
                  .layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 10 })
                  Column() {
                    Text('¥' + s.price.toString()).fontSize(15).fontWeight(FontWeight.Bold).fontColor('#E9B84C')
                    Text('余 ' + s.remain.toString()).fontSize(9).fontColor('#A89CB8').margin({ top: 2 })
                  }
                  .alignItems(HorizontalAlign.End)
                  Text('购票').fontSize(11).fontColor('#141021').backgroundColor('#E9B84C')
                    .padding({ left: 12, right: 12, top: 6, bottom: 6 }).borderRadius(13)
                    .margin({ left: 10 })
                    .onClick(() => { this.onBuyTicket(s.name) })
                }
                .width('100%').padding(12).backgroundColor('#1F1930').borderRadius(14)
                .margin({ top: 8 })
              }
            })
          }
          .width('100%').padding({ bottom: 16 })
        }
        .padding({ left: 12, right: 12 })
      }
      .scrollable(ScrollDirection.Vertical)
      .layoutWeight(1).scrollBar(BarState.Off)
    }
    .width('100%').height('100%')
  }
}

// ============ Tab2:开放麦 ============
@Component
struct CBOpenMicContent {
  @State micAngle: number = -6
  onSignOpenMic: () => void = () => {}
  onEditSignup: (name: string, topic: string, form: string) => void = (name: string, topic: string, form: string) => {}

  aboutToAppear() {
    // 麦克风 ±6° 摆动
    this.getUIContext().animateTo({
      duration: 1200,
      iterations: -1,
      playMode: PlayMode.Alternate,
      curve: Curve.EaseInOut
    }, () => {
      this.micAngle = 6
    })
  }

  @Builder signupRow(s: CBSignup) {
    Row() {
      Column() {
        Text(s.name.substring(0, 1)).fontSize(20).fontWeight(FontWeight.Bold)
          .fontColor('#F5EFE0')
      }
      .width(48).height(48).backgroundColor(s.color).opacity(0.85)
      .borderRadius(14)
      .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
      Column() {
        Row() {
          Text(s.name).fontSize(14).fontWeight(FontWeight.Bold).fontColor('#F5EFE0').maxLines(1)
          Text(s.form).fontSize(9).fontColor('#F5EFE0')
            .backgroundColor(cbFormBg(s.form))
            .padding({ left: 6, right: 6, top: 2, bottom: 2 }).borderRadius(7)
            .margin({ left: 8 })
        }
        .width('100%')
        Text('《' + s.topic + '》').fontSize(11).fontColor('#A89CB8').margin({ top: 3 }).maxLines(1)
        Row() {
          Text(s.status).fontSize(9).fontColor('#141021')
            .backgroundColor(cbStatusColor(s.status))
            .padding({ left: 6, right: 6, top: 2, bottom: 2 }).borderRadius(7)
          Text(s.minutes.toString() + ' 分钟').fontSize(9).fontColor('#6E6280').margin({ left: 8 })
          Text('编辑').fontSize(10).fontColor('#E9B84C')
            .padding({ left: 8, right: 8, top: 3, bottom: 3 })
            .border({ width: 1, color: '#E9B84C' }).borderRadius(10)
            .margin({ left: 10 })
            .onClick(() => { this.onEditSignup(s.name, s.topic, s.form) })
        }
        .width('100%').margin({ top: 5 })
      }
      .layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 10 })
    }
    .width('100%').padding(12).backgroundColor('#1F1930').borderRadius(14)
    .margin({ top: 8 })
  }

  build() {
    Column() {
      Scroll() {
        Column() {
          // ===== 开放麦报名头卡 =====
          Stack() {
            Column()
              .width('100%').height(110)
              .linearGradient({
                direction: GradientDirection.Right,
                colors: [['#7C1B24', 0], ['#141021', 1]]
              })
              .borderRadius(16)
            Text('🎤').fontSize(46)
              .rotate({ angle: this.micAngle })
              .position({ x: 22, y: 30 })
            Column() {
              Text('OPEN MIC · 开放麦').fontSize(9).fontColor('#E9B84C').letterSpacing(2)
              Text('人人都能上台炸一次').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#F5EFE0')
                .margin({ top: 3 })
              Text('新梗试炼场 · 观众免审 · 报名即演').fontSize(10).fontColor('#A89CB8')
                .margin({ top: 4 })
            }
            .alignItems(HorizontalAlign.Start)
            .padding({ left: 96, right: 14, top: 20 })
            Text('报名').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#141021')
              .backgroundColor('#E9B84C').borderRadius(18)
              .padding({ left: 16, right: 16, top: 7, bottom: 7 })
              .position({ x: 96, y: 72 })
              .onClick(() => { this.onSignOpenMic() })
          }
          .width('100%').height(110)
          .margin({ top: 10 })

          // ===== 报名列表 =====
          Row() {
            Text('📋 本期报名(' + CB_SIGNUPS.length.toString() + ' 组)')
              .fontSize(15).fontWeight(FontWeight.Bold).fontColor('#F5EFE0')
            Column().layoutWeight(1)
            Text('待审 3 · 通过 3 · 驳回 2').fontSize(9).fontColor('#6E6280')
          }
          .width('100%').padding({ top: 14, bottom: 4 })
          Column() {
            ForEach(CB_SIGNUPS, (s: CBSignup) => {
              this.signupRow(s)
            })
          }
          .width('100%')

          // ===== 新人扶持计划卡 =====
          Column() {
            Row() {
              Text('🌱').fontSize(30)
              Column() {
                Text('新人扶持计划').fontSize(15).fontWeight(FontWeight.Bold).fontColor('#E9B84C')
                Text('首次报名直通 + 免费编剧指导 1v1 + 爆梗奖金池 ¥3,000,新人炸场即上榜「本月新星」。')
                  .fontSize(10).fontColor('#A89CB8').margin({ top: 4 }).lineHeight(15)
              }
              .layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 12 })
            }
            .width('100%')
          }
          .width('100%').padding(14)
          .backgroundColor('#1F1930').border({ width: 1, color: '#E9B84C' }).borderRadius(16)
          .margin({ top: 12 })

          // ===== 下期名额进度条 =====
          Column() {
            Row() {
              Text('🎫 下期开放麦名额').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#F5EFE0')
              Column().layoutWeight(1)
              Text('18 / 20').fontSize(12).fontWeight(FontWeight.Bold).fontColor('#E9B84C')
            }
            .width('100%').padding({ top: 12, bottom: 8 })
            Progress({ value: 18, total: 20, type: ProgressType.Linear })
              .width('100%').color('#A32330')
            Text('仅剩 2 个名额,本周日 19:00 小剧场开演').fontSize(9).fontColor('#6E6280')
              .margin({ top: 6 }).width('100%')
            Row() {
              Text('抢最后名额').fontSize(12).fontWeight(FontWeight.Bold).fontColor('#F5EFE0')
                .backgroundColor('#A32330').borderRadius(16)
                .padding({ left: 16, right: 16, top: 7, bottom: 7 })
                .margin({ top: 10 })
                .onClick(() => { this.onSignOpenMic() })
            }
            .width('100%').justifyContent(FlexAlign.Center)
            .padding({ bottom: 14 })
          }
          .width('100%').backgroundColor('#1F1930').borderRadius(16)
          .padding({ left: 14, right: 14 }).margin({ top: 12, bottom: 16 })
        }
        .padding({ left: 12, right: 12 })
      }
      .scrollable(ScrollDirection.Vertical)
      .layoutWeight(1).scrollBar(BarState.Off)
    }
    .width('100%').height('100%')
  }
}

// ============ Tab3:场次 ============
@Component
struct CBSessionsContent {
  onBuyTicket: (showName: string) => void = (showName: string) => {}

  @Builder gridHeader() {
    Row() {
      Column() {
        Text('时段').fontSize(9).fontColor('#6E6280')
      }
      .width(52).alignItems(HorizontalAlign.Center)
      ForEach(CB_WEEK_GRID, (c: CBWeekCell) => {
        Text(c.day.replace('周', ''))
          .fontSize(10).fontColor('#F5EFE0').layoutWeight(1).textAlign(TextAlign.Center)
      })
    }
    .width('100%').padding({ top: 10, bottom: 6 })
  }

  @Builder gridRow(rowIdx: number) {
    Row() {
      Column() {
        Text(CB_GRID_ROWS[rowIdx]).fontSize(8).fontColor('#A89CB8')
      }
      .width(52).alignItems(HorizontalAlign.Center)
      ForEach(CB_WEEK_GRID, (c: CBWeekCell) => {
        Column() {
          Text(cbCellText(rowIdx === 0 ? c.s1 : (rowIdx === 1 ? c.s2 : c.s3)))
            .fontSize(10).fontWeight(FontWeight.Bold)
            .fontColor(cbCellTx(rowIdx === 0 ? c.s1 : (rowIdx === 1 ? c.s2 : c.s3)))
        }
        .layoutWeight(1).height(30).margin({ left: 2, right: 2 })
        .backgroundColor(cbCellBg(rowIdx === 0 ? c.s1 : (rowIdx === 1 ? c.s2 : c.s3)))
        .border({
          width: rowIdx === 0 ? (c.s1 === '下' ? 1 : 0) : (rowIdx === 1 ? (c.s2 === '下' ? 1 : 0) : (c.s3 === '下' ? 1 : 0)),
          color: '#E9B84C'
        })
        .borderRadius(6)
        .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
      })
    }
    .width('100%').margin({ top: 4 })
  }

  @Builder sessionRow(s: CBSession) {
    Column() {
      Row() {
        Column() {
          Text(s.name).fontSize(14).fontWeight(FontWeight.Bold).fontColor('#F5EFE0').maxLines(1)
          Row() {
            Text(s.date + ' ' + s.time).fontSize(10).fontColor('#A89CB8')
            Text(s.venue).fontSize(9).fontColor('#E9B84C')
              .padding({ left: 6, right: 6, top: 2, bottom: 2 }).borderRadius(7)
              .border({ width: 1, color: '#E9B84C' })
              .margin({ left: 8 })
            if (s.hot) {
              Text('HOT').fontSize(8).fontColor('#141021').backgroundColor('#E9B84C')
                .padding({ left: 5, right: 5, top: 2, bottom: 2 }).borderRadius(6)
                .margin({ left: 6 })
            }
          }
          .width('100%').margin({ top: 4 })
        }
        .layoutWeight(1).alignItems(HorizontalAlign.Start)
        Column() {
          Text('¥' + s.price.toString()).fontSize(16).fontWeight(FontWeight.Bold).fontColor('#E9B84C')
          if (s.remain < 15) {
            Text('余票紧张 ' + s.remain.toString()).fontSize(9).fontColor('#D94848').margin({ top: 2 })
          } else {
            Text('余票 ' + s.remain.toString()).fontSize(9).fontColor('#A89CB8').margin({ top: 2 })
          }
        }
        .alignItems(HorizontalAlign.End)
        Text('购票').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#141021')
          .backgroundColor('#E9B84C').borderRadius(13)
          .padding({ left: 12, right: 12, top: 6, bottom: 6 })
          .margin({ left: 10 })
          .onClick(() => { this.onBuyTicket(s.name) })
      }
      .width('100%')
      // 阵容头像横滚
      Row() {
        Text('阵容').fontSize(9).fontColor('#6E6280')
        Scroll() {
          Row() {
            ForEach(s.avatars, (a: string) => {
              Text(a).fontSize(16)
                .width(30).height(30).backgroundColor('#141021').borderRadius(15)
                .textAlign(TextAlign.Center)
                .margin({ left: 5 })
            })
          }
        }
        .scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off)
        .layoutWeight(1).margin({ left: 6 })
      }
      .width('100%').margin({ top: 8 })
    }
    .width('100%').padding(12).backgroundColor('#1F1930').borderRadius(14)
    .margin({ top: 8 })
  }

  build() {
    Column() {
      Scroll() {
        Column() {
          Text('📅 本周场次时段表').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#F5EFE0')
            .width('100%').padding({ top: 12, bottom: 4 })
          // ===== 周场次时段表 =====
          Column() {
            Row() {
              Column().width(10).height(10).backgroundColor('#E9B84C').borderRadius(3)
              Text('晚场').fontSize(9).fontColor('#A89CB8').margin({ left: 4 })
              Column().width(10).height(10).backgroundColor('#1F1930')
                .border({ width: 1, color: '#E9B84C' }).borderRadius(3).margin({ left: 12 })
              Text('下午场').fontSize(9).fontColor('#A89CB8').margin({ left: 4 })
              Column().width(10).height(10).backgroundColor('#A32330').borderRadius(3).margin({ left: 12 })
              Text('满场').fontSize(9).fontColor('#A89CB8').margin({ left: 4 })
              Column().width(10).height(10).backgroundColor('#0C0916').borderRadius(3).margin({ left: 12 })
              Text('休场').fontSize(9).fontColor('#A89CB8').margin({ left: 4 })
              Column().layoutWeight(1)
            }
            .width('100%').padding({ top: 10, left: 10 })
            this.gridHeader()
            this.gridRow(0)
            this.gridRow(1)
            this.gridRow(2)
          }
          .width('100%').backgroundColor('#1F1930').borderRadius(16)
          .padding({ left: 10, right: 10, bottom: 14 }).margin({ top: 8 })

          // ===== 场次列表 =====
          Row() {
            Text('🎬 全部场次(' + CB_SESSIONS.length.toString() + ' 场)')
              .fontSize(15).fontWeight(FontWeight.Bold).fontColor('#F5EFE0')
            Column().layoutWeight(1)
            Text('小剧场 4 · 大舞台 4').fontSize(9).fontColor('#6E6280')
          }
          .width('100%').padding({ top: 14, bottom: 4 })
          Column() {
            ForEach(CB_SESSIONS, (s: CBSession) => {
              this.sessionRow(s)
            })
          }
          .width('100%').padding({ bottom: 16 })
        }
        .padding({ left: 12, right: 12 })
      }
      .scrollable(ScrollDirection.Vertical)
      .layoutWeight(1).scrollBar(BarState.Off)
    }
    .width('100%').height('100%')
  }
}

// ============ Tab4:演员 ============
@Component
struct CBActorsContent {
  @State trophyScale: number = 1
  @State followedId: number = -1
  onOpenActor: (actor: CBActor) => void = (actor: CBActor) => {}

  aboutToAppear() {
    // 奖杯金光脉动
    this.getUIContext().animateTo({
      duration: 1400,
      iterations: -1,
      playMode: PlayMode.Alternate,
      curve: Curve.EaseInOut
    }, () => {
      this.trophyScale = 1.18
    })
  }

  @Builder actorCard(a: CBActor) {
    Column() {
      Stack() {
        Column()
          .width(108).height(108)
          .linearGradient({
            direction: GradientDirection.RightBottom,
            colors: [[a.color, 0], ['#141021', 1]]
          })
          .borderRadius(16)
        Text(a.emoji).fontSize(44)
      }
      .width(108).height(108)
      Text(a.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor('#F5EFE0')
        .margin({ top: 8 }).maxLines(1)
      Text(a.style).fontSize(9).fontColor('#E9B84C')
        .padding({ left: 7, right: 7, top: 2, bottom: 2 })
        .backgroundColor('#141021').borderRadius(9).margin({ top: 4 })
      Text(a.fans.toString() + '万粉').fontSize(9).fontColor('#A89CB8').margin({ top: 4 })
      if (this.followedId === a.id) {
        Text('已关注 ✓').fontSize(10).fontColor('#141021')
          .backgroundColor('#352B4A').borderRadius(12)
          .padding({ left: 13, right: 13, top: 5, bottom: 5 }).margin({ top: 6 })
          .onClick(() => { this.followedId = -1 })
      } else {
        Text('+ 关注').fontSize(10).fontWeight(FontWeight.Bold).fontColor('#141021')
          .backgroundColor('#E9B84C').borderRadius(12)
          .padding({ left: 16, right: 16, top: 5, bottom: 5 }).margin({ top: 6 })
          .onClick(() => { this.followedId = a.id })
      }
    }
    .width(118).padding(10).backgroundColor('#1F1930').borderRadius(16)
    .margin({ left: 8 })
    .alignItems(HorizontalAlign.Center)
    .onClick(() => { this.onOpenActor(a) })
  }

  @Builder rankRow(a: CBActor, rank: number) {
    Row() {
      Text(rank.toString()).fontSize(18).fontWeight(FontWeight.Bold)
        .fontColor(cbRankColor(rank)).width(32).textAlign(TextAlign.Center)
      Text(a.emoji).fontSize(20)
      Column() {
        Text(a.name).fontSize(12).fontWeight(FontWeight.Bold).fontColor('#F5EFE0')
        Text(a.style).fontSize(9).fontColor('#6E6280').margin({ top: 2 })
      }
      .layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 8 })
      Column() {
        Column()
          .width(cbLaughPct(a.laughs)).height(6)
          .backgroundColor(rank <= 3 ? '#E9B84C' : '#A32330')
          .borderRadius(3)
        Text(a.laughs.toString() + ' 次/场').fontSize(9).fontColor('#A89CB8').margin({ top: 3 })
      }
      .width(80).alignItems(HorizontalAlign.Start)
      Text(a.fans.toString() + '万').fontSize(11).fontWeight(FontWeight.Bold)
        .fontColor(rank <= 3 ? '#E9B84C' : '#A89CB8')
        .width(44).textAlign(TextAlign.End)
    }
    .width('100%').padding({ top: 9, bottom: 9, left: 8, right: 8 })
    .backgroundColor(rank % 2 === 1 ? '#1F1930' : '#141021')
    .borderRadius(10)
    .onClick(() => { this.onOpenActor(a) })
  }

  build() {
    Column() {
      Scroll() {
        Column() {
          // ===== 签约演员横滚 =====
          Row() {
            Text('🌟 签约演员').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#F5EFE0')
            Column().layoutWeight(1)
            Text('点击卡片看档案').fontSize(9).fontColor('#6E6280')
          }
          .width('100%').padding({ top: 12, bottom: 8 })
          Scroll() {
            Row() {
              ForEach(CB_ACTORS, (a: CBActor) => {
                this.actorCard(a)
              })
            }
            .padding({ right: 12 })
          }
          .scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off)
          .width('100%')

          // ===== 演员热度排行表格 =====
          Column() {
            Row() {
              Text('🏆 演员热度排行').fontSize(15).fontWeight(FontWeight.Bold).fontColor('#F5EFE0')
              Column().layoutWeight(1)
              Text('按场均大笑排序').fontSize(9).fontColor('#6E6280')
            }
            .width('100%').padding({ top: 12, bottom: 6 })
            Row() {
              Text('排名').fontSize(9).fontColor('#6E6280').width(32).textAlign(TextAlign.Center)
              Text('演员').fontSize(9).fontColor('#6E6280').layoutWeight(1)
              Text('风格').fontSize(9).fontColor('#6E6280').width(64)
              Text('场均大笑').fontSize(9).fontColor('#6E6280').width(80)
              Text('粉丝').fontSize(9).fontColor('#6E6280').width(44).textAlign(TextAlign.End)
            }
            .width('100%').padding({ bottom: 6 })
            .border({ width: { bottom: 1 }, color: '#352B4A' })
            Column() {
              ForEach(CB_ACTORS, (a: CBActor, idx: number) => {
                this.rankRow(a, idx + 1)
              })
            }
            .width('100%').padding({ bottom: 10 })
          }
          .width('100%').backgroundColor('#1F1930').borderRadius(16)
          .padding({ left: 12, right: 12 }).margin({ top: 12 })

          // ===== 本月新星卡 =====
          Column() {
            Row() {
              Stack() {
                Column()
                  .width(72).height(72)
                  .linearGradient({
                    direction: GradientDirection.RightBottom,
                    colors: [['#E9B84C', 0], ['#7C1B24', 1]]
                  })
                  .borderRadius(18)
                Text('🏆').fontSize(34)
                  .scale({ x: this.trophyScale, y: this.trophyScale })
              }
              .width(72).height(72)
              Column() {
                Text('本月新星 · RISING STAR').fontSize(9).fontColor('#E9B84C').letterSpacing(1)
                Text(CB_NOVA.name).fontSize(17).fontWeight(FontWeight.Bold).fontColor('#F5EFE0')
                  .margin({ top: 3 })
                Text(CB_NOVA.style).fontSize(10).fontColor('#E9B84C').margin({ top: 3 })
              }
              .layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 14 })
              Text(CB_NOVA.emoji).fontSize(40)
            }
            .width('100%')
            Text(CB_NOVA.desc).fontSize(10).fontColor('#A89CB8').margin({ top: 10 }).lineHeight(15)
            Row() {
              Text('围观新星').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#141021')
                .backgroundColor('#E9B84C').borderRadius(14)
                .padding({ left: 14, right: 14, top: 6, bottom: 6 })
                .margin({ top: 8 })
                .onClick(() => { this.onOpenActor(CB_ACTORS[6]) })
            }
            .width('100%').justifyContent(FlexAlign.Center)
            .padding({ bottom: 14 })
          }
          .width('100%').padding(14)
          .backgroundColor('#1F1930').border({ width: 1, color: '#E9B84C' }).borderRadius(16)
          .margin({ top: 12, bottom: 16 })
        }
        .padding({ left: 12, right: 12 })
      }
      .scrollable(ScrollDirection.Vertical)
      .layoutWeight(1).scrollBar(BarState.Off)
    }
    .width('100%').height('100%')
  }
}

// ============ Tab5:票夹 ============
@Component
struct CBTicketsContent {
  @State qrOp: number = 0.5
  onRefund: (show: string) => void = (show: string) => {}
  onReview: () => void = () => {}

  aboutToAppear() {
    // 二维码金光扫动
    this.getUIContext().animateTo({
      duration: 1600,
      iterations: -1,
      playMode: PlayMode.Alternate,
      curve: Curve.EaseInOut
    }, () => {
      this.qrOp = 1.0
    })
  }

  @Builder qrBlock() {
    Column() {
      Row() {
        ForEach([0, 1, 2], (i: number) => {
          if (i % 2 === 0) {
            Column().width(9).height(9).backgroundColor('#141021').margin(1)
          } else {
            Column().width(9).height(9).backgroundColor('#E9B84C').opacity(this.qrOp).margin(1)
          }
        })
      }
      Row() {
        ForEach([0, 1, 2], (i: number) => {
          if (i % 2 === 1) {
            Column().width(9).height(9).backgroundColor('#141021').margin(1)
          } else {
            Column().width(9).height(9).backgroundColor('#C99A2E').opacity(this.qrOp).margin(1)
          }
        })
      }
      Row() {
        ForEach([0, 1, 2], (i: number) => {
          if (i % 2 === 0) {
            Column().width(9).height(9).backgroundColor('#C99A2E').margin(1)
          } else {
            Column().width(9).height(9).backgroundColor('#141021').margin(1)
          }
        })
      }
    }
    .width(42).height(42)
    .backgroundColor('#F2E8CE').borderRadius(6)
    .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
  }

  @Builder ticketCard(t: CBTicket) {
    Row() {
      Column() {
        Text(t.show).fontSize(14).fontWeight(FontWeight.Bold).fontColor('#F5EFE0').maxLines(1)
        Text(t.date).fontSize(10).fontColor('#A89CB8').margin({ top: 4 })
        Text(t.seat).fontSize(19).fontWeight(FontWeight.Bold).fontColor('#E9B84C')
          .margin({ top: 6 })
        Row() {
          Text(t.status).fontSize(9).fontColor('#141021')
            .backgroundColor(cbTicketColor(t.status))
            .padding({ left: 7, right: 7, top: 2, bottom: 2 }).borderRadius(7)
          Text(t.zone + '区').fontSize(9).fontColor('#A89CB8').margin({ left: 8 })
        }
        .margin({ top: 8 })
        if (t.status === '未使用') {
          Row() {
            Text('退票').fontSize(10).fontColor('#D94848')
              .padding({ left: 10, right: 10, top: 4, bottom: 4 })
              .border({ width: 1, color: '#D94848' }).borderRadius(11)
              .onClick(() => { this.onRefund(t.show) })
            Text('写观后感').fontSize(10).fontColor('#E9B84C')
              .padding({ left: 10, right: 10, top: 4, bottom: 4 })
              .border({ width: 1, color: '#E9B84C' }).borderRadius(11)
              .margin({ left: 8 })
              .onClick(() => { this.onReview() })
          }
          .margin({ top: 8 })
        }
      }
      .layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 14 })
      Column() {
        this.qrBlock()
        Text('¥' + t.price.toString()).fontSize(13).fontWeight(FontWeight.Bold)
          .fontColor('#F5EFE0').margin({ top: 5 })
      }
      .width(84).height('100%')
      .backgroundColor('#141021')
      .borderRadius({ topRight: 14, bottomRight: 14 })
      .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
      .padding({ top: 12, bottom: 12 })
    }
    .width('100%').height(146).backgroundColor('#1F1930').borderRadius(14)
    .margin({ top: 10 })
  }

  @Builder statCell(s: CBStat) {
    Column() {
      Row() {
        Text(s.value).fontSize(20).fontWeight(FontWeight.Bold).fontColor(s.color)
        Text(s.sub).fontSize(10).fontColor('#A89CB8').margin({ left: 3 })
      }
      .alignItems(VerticalAlign.Bottom)
      Text(s.label).fontSize(10).fontColor('#A89CB8').margin({ top: 4 })
    }
    .layoutWeight(1)
    .padding({ top: 14, bottom: 14 })
    .backgroundColor('#1F1930').borderRadius(14)
    .alignItems(HorizontalAlign.Center)
  }

  build() {
    Column() {
      Scroll() {
        Column() {
          Text('🎟️ 我的票夹').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#F5EFE0')
            .width('100%').padding({ top: 12, bottom: 2 })
          Text('未使用 1 · 已核销 3 · 已过期 2').fontSize(10).fontColor('#6E6280')
            .width('100%').padding({ bottom: 4 })

          // ===== 电子票列表 =====
          Column() {
            ForEach(CB_TICKETS, (t: CBTicket) => {
              this.ticketCard(t)
            })
          }
          .width('100%')

          // ===== 历史观演统计四格 =====
          Text('📈 历史观演统计').fontSize(15).fontWeight(FontWeight.Bold).fontColor('#F5EFE0')
            .width('100%').padding({ top: 16, bottom: 8 })
          Row() {
            this.statCell(CB_STATS[0])
            this.statCell(CB_STATS[1])
          }
          .width('100%')
          Row() {
            this.statCell(CB_STATS[2])
            this.statCell(CB_STATS[3])
          }
          .width('100%').margin({ top: 10 })

          Row() {
            Text('🎁 观演 50 场解锁「笑场终身票根」').fontSize(10).fontColor('#6E6280')
          }
          .width('100%').justifyContent(FlexAlign.Center)
          .padding({ top: 14, bottom: 16 })
        }
        .padding({ left: 12, right: 12 })
      }
      .scrollable(ScrollDirection.Vertical)
      .layoutWeight(1).scrollBar(BarState.Off)
    }
    .width('100%').height('100%')
  }
}

// ============ Tab6:观众档 ============
@Component
struct CBAudienceContent {
  @State prefWidths: number[] = [34, 28, 18, 12, 8]

  @Builder jokeRow(j: CBJoke) {
    Column() {
      Text('「' + j.text + '」').fontSize(12).fontColor('#F5EFE0').lineHeight(18)
      Row() {
        Text(j.tag).fontSize(9).fontColor('#E9B84C')
          .padding({ left: 6, right: 6, top: 2, bottom: 2 })
          .backgroundColor('#141021').borderRadius(7)
        Text('@' + j.actor).fontSize(10).fontColor('#A89CB8').margin({ left: 8 })
        Column().layoutWeight(1)
        Text('❤ ' + j.likes.toString()).fontSize(10).fontColor('#A89CB8')
      }
      .width('100%').margin({ top: 6 })
    }
    .width('100%').padding(12).backgroundColor('#1F1930').borderRadius(14)
    .margin({ top: 8 })
  }

  @Builder settingRow(s: CBSettingItem) {
    Row() {
      Text(s.icon).fontSize(18)
      Text(s.label).fontSize(13).fontColor('#F5EFE0').margin({ left: 12 })
      Column().layoutWeight(1)
      Text(s.value).fontSize(11).fontColor('#A89CB8')
      Text('›').fontSize(16).fontColor('#6E6280').margin({ left: 8 })
    }
    .width('100%').padding({ top: 14, bottom: 14, left: 14, right: 14 })
    .backgroundColor('#1F1930').borderRadius(14)
    .margin({ top: 8 })
  }

  build() {
    Column() {
      Scroll() {
        Column() {
          // ===== 观众卡 =====
          Column() {
            Row() {
              Column() {
                Text('😂').fontSize(42)
              }
              .width(70).height(70).backgroundColor('#7C1B24').borderRadius(20)
              .border({ width: 2, color: '#E9B84C' })
              .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
              Column() {
                Text('笑到扶墙的David').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#F5EFE0')
                Row() {
                  Text('笑点浓度 92%').fontSize(10).fontColor('#141021')
                    .backgroundColor('#E9B84C')
                    .padding({ left: 8, right: 8, top: 3, bottom: 3 }).borderRadius(10)
                  Text('Lv.8 剧场钉子户').fontSize(10).fontColor('#A89CB8').margin({ left: 8 })
                }
                .margin({ top: 6 })
                Text('ID: CB-2026-0124 · 陪笑会籍 GOLD').fontSize(9).fontColor('#6E6280')
                  .margin({ top: 6 })
              }
              .layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 14 })
            }
            .width('100%')

            // 观演徽章 chips
            Row() {
              ForEach(CB_BADGES, (b: CBBadge) => {
                Text(b.emoji + ' ' + b.name)
                  .fontSize(9).fontColor('#E9B84C')
                  .padding({ left: 7, right: 7, top: 4, bottom: 4 })
                  .backgroundColor('#141021').borderRadius(10)
                  .margin({ left: 4 })
              })
            }
            .width('100%').margin({ top: 12 }).padding({ left: 2 })
          }
          .width('100%').padding(14).backgroundColor('#1F1930').borderRadius(16)
          .margin({ top: 12 })

          // ===== 笑点偏好条形图 =====
          Column() {
            Text('🧪 笑点偏好占比').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#F5EFE0')
              .width('100%').padding({ top: 12, bottom: 8 })
            Column() {
              ForEach(CB_PREFS, (p: CBPref, idx: number) => {
                Row() {
                  Text(p.name).fontSize(11).fontColor('#A89CB8').width(64)
                  Column()
                    .width(this.prefWidths[idx] * 2.6 + 'vp')
                    .height(12).backgroundColor(p.color).borderRadius(6)
                  Text(p.pct.toString() + '%').fontSize(10).fontColor('#F5EFE0')
                    .margin({ left: 8 })
                  Column().layoutWeight(1)
                }
                .width('100%').margin({ top: 7 })
              })
            }
            .width('100%').padding({ bottom: 12 })
          }
          .width('100%').backgroundColor('#1F1930').borderRadius(16)
          .padding({ left: 14, right: 14 }).margin({ top: 12 })

          // ===== 收藏段子 =====
          Text('📖 收藏段子(' + CB_JOKES.length.toString() + ' 条)')
            .fontSize(15).fontWeight(FontWeight.Bold).fontColor('#F5EFE0')
            .width('100%').padding({ top: 14, bottom: 4 })
          Column() {
            ForEach(CB_JOKES, (j: CBJoke) => {
              this.jokeRow(j)
            })
          }
          .width('100%')

          // ===== 设置项 =====
          Text('⚙️ 设置').fontSize(15).fontWeight(FontWeight.Bold).fontColor('#F5EFE0')
            .width('100%').padding({ top: 14, bottom: 4 })
          Column() {
            ForEach(CB_SETTINGS, (s: CBSettingItem) => {
              this.settingRow(s)
            })
          }
          .width('100%').padding({ bottom: 16 })
        }
        .padding({ left: 12, right: 12 })
      }
      .scrollable(ScrollDirection.Vertical)
      .layoutWeight(1).scrollBar(BarState.Off)
    }
    .width('100%').height('100%')
  }
}

// ============ 主入口 ============
@Entry
@Component
struct CBComedyBaseApp {
  @State activeTab: CBTab = CBTab.Theater
  @State showSignOpenMic: boolean = false
  @State showBuyTicket: boolean = false
  @State showEditOpenMic: boolean = false
  @State showRefund: boolean = false
  @State showActor: boolean = false
  @State showReview: boolean = false
  @State buyShowName: string = '周一 emo 治愈专场'
  @State buyPrice: number = 120
  @State buyOldPrice: number = 150
  @State editTarget: CBSignup = CB_SIGNUPS[2]
  @State refundShowName: string = '周三谐音梗大赛'
  @State actorTarget: CBActor = CB_ACTORS[0]

  // ===== 全屏遮罩 Builder =====
  @Builder modalOverlay(onClose: () => void) {
    Column()
      .width('100%').height('100%')
      .backgroundColor('rgba(12,9,22,0.6)')
      .onClick(onClose)
  }

  // ===== 弹框1:报名开放麦 =====
  @Builder signOpenMicModal() {
    Column() {
      this.modalOverlay(() => { this.showSignOpenMic = false })
      Column() {
        CBSignOpenMicForm({
          onClose: () => { this.showSignOpenMic = false },
          onConfirm: () => { this.showSignOpenMic = false }
        })
      }
      .width('92%')
      .constraintSize({ maxHeight: '82%' })
      .backgroundColor('#1F1930')
      .borderRadius(16)
      .position({ x: '4%', y: '8%' })
    }
    .width('100%').height('100%')
    .position({ x: 0, y: 0 }).zIndex(999)
  }

  // ===== 弹框2:购票选座 =====
  @Builder buyTicketModal() {
    Column() {
      this.modalOverlay(() => { this.showBuyTicket = false })
      Column() {
        CBBuyTicketForm({
          showName: this.buyShowName,
          price: this.buyPrice,
          oldPrice: this.buyOldPrice,
          onClose: () => { this.showBuyTicket = false },
          onConfirm: () => { this.showBuyTicket = false }
        })
      }
      .width('94%')
      .constraintSize({ maxHeight: '84%' })
      .backgroundColor('#1F1930')
      .borderRadius(16)
      .position({ x: '3%', y: '7%' })
    }
    .width('100%').height('100%')
    .position({ x: 0, y: 0 }).zIndex(999)
  }

  // ===== 弹框3:编辑报名(底部抽屉式) =====
  @Builder editOpenMicModal() {
    Column() {
      this.modalOverlay(() => { this.showEditOpenMic = false })
      Column() {
        CBEditOpenMicForm({
          signupName: this.editTarget.name,
          signupTopic: this.editTarget.topic,
          signupForm: this.editTarget.form,
          onClose: () => { this.showEditOpenMic = false },
          onSave: () => { this.showEditOpenMic = false },
          onWithdraw: () => { this.showEditOpenMic = false }
        })
      }
      .width('100%')
      .constraintSize({ maxHeight: '70%' })
      .backgroundColor('#1F1930')
      .borderRadius({ topLeft: 24, topRight: 24 })
      .position({ x: 0, y: '30%' })
    }
    .width('100%').height('100%')
    .position({ x: 0, y: 0 }).zIndex(999)
  }

  // ===== 弹框4:退票(幕布红警示式) =====
  @Builder refundModal() {
    Column() {
      this.modalOverlay(() => { this.showRefund = false })
      Column() {
        Text('⚠️').fontSize(48).margin({ top: 22 })
        Text('确认退票?').fontSize(19).fontWeight(FontWeight.Bold).fontColor('#E9B84C')
          .margin({ top: 8 })
        Column() {
          Text('退票规则').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#F5EFE0')
          Text('· 开演前 48 小时外:全额退款').fontSize(10).fontColor('#A89CB8').margin({ top: 6 })
          Text('· 开演前 48 小时内:扣除票款 20% 手续费').fontSize(10).fontColor('#D94848').margin({ top: 4 })
          Text('· 开演后不支持退票,可转赠好友').fontSize(10).fontColor('#A89CB8').margin({ top: 4 })
        }
        .width('100%').padding(12).backgroundColor('#141021').borderRadius(12)
        .alignItems(HorizontalAlign.Start)
        .margin({ top: 14 })

        Row() {
          Text('🎟️').fontSize(20)
          Column() {
            Text(this.refundShowName).fontSize(13).fontWeight(FontWeight.Bold).fontColor('#F5EFE0').maxLines(1)
            Text('09-02 19:30 · A区3排8号 · ¥88').fontSize(10).fontColor('#A89CB8').margin({ top: 3 })
          }
          .layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 10 })
          Text('退款 ¥70.4').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#E9B84C')
        }
        .width('100%').padding(12).backgroundColor('#1F1930').borderRadius(12)
        .border({ width: 1, color: '#E9B84C' })
        .margin({ top: 10 })

        Row() {
          Text('保留门票').fontSize(14).fontColor('#A89CB8')
            .backgroundColor('#141021').borderRadius(20)
            .padding({ left: 26, right: 26, top: 10, bottom: 10 })
            .onClick(() => { this.showRefund = false })
          Text('确认退票').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#F5EFE0')
            .backgroundColor('#A32330').borderRadius(20)
            .padding({ left: 28, right: 28, top: 10, bottom: 10 })
            .margin({ left: 12 })
            .onClick(() => { this.showRefund = false })
        }
        .width('100%').justifyContent(FlexAlign.Center)
        .padding({ top: 18, bottom: 22 })
      }
      .width('86%')
      .backgroundColor('#1F1930')
      .borderRadius(16)
      .border({ width: 2, color: '#A32330' })
      .alignItems(HorizontalAlign.Center)
      .position({ x: '7%', y: '12%' })
    }
    .width('100%').height('100%')
    .position({ x: 0, y: 0 }).zIndex(999)
  }

  // ===== 弹框5:演员详情(档案海报式) =====
  @Builder actorModal() {
    Column() {
      this.modalOverlay(() => { this.showActor = false })
      Column() {
        CBActorForm({
          actor: this.actorTarget,
          onClose: () => { this.showActor = false },
          onConfirm: () => { this.showActor = false }
        })
      }
      .width('90%')
      .constraintSize({ maxHeight: '80%' })
      .backgroundColor('#1F1930')
      .borderRadius(16)
      .position({ x: '5%', y: '9%' })
    }
    .width('100%').height('100%')
    .position({ x: 0, y: 0 }).zIndex(999)
  }

  // ===== 弹框6:发布观后感(弹幕卡片式) =====
  @Builder reviewModal() {
    Column() {
      this.modalOverlay(() => { this.showReview = false })
      Column() {
        CBReviewForm({
          onClose: () => { this.showReview = false },
          onConfirm: () => { this.showReview = false }
        })
      }
      .width('92%')
      .constraintSize({ maxHeight: '82%' })
      .backgroundColor('#1F1930')
      .borderRadius(16)
      .position({ x: '4%', y: '8%' })
    }
    .width('100%').height('100%')
    .position({ x: 0, y: 0 }).zIndex(999)
  }

  // ===== 内容区分发 =====
  @Builder contentArea() {
    Stack() {
      if (this.activeTab === CBTab.Theater) {
        CBTheaterContent({
          onBuyTicket: (showName: string) => {
            this.buyShowName = showName
            this.buyPrice = 120
            this.buyOldPrice = 150
            this.showBuyTicket = true
          }
        })
      } else if (this.activeTab === CBTab.OpenMic) {
        CBOpenMicContent({
          onSignOpenMic: () => { this.showSignOpenMic = true },
          onEditSignup: (name: string, topic: string, form: string) => {
            this.editTarget = {
              id: 0, name: name, color: '#E9B84C', form: form,
              minutes: 10, topic: topic, status: '待审'
            }
            this.showEditOpenMic = true
          }
        })
      } else if (this.activeTab === CBTab.Sessions) {
        CBSessionsContent({
          onBuyTicket: (showName: string) => {
            this.buyShowName = showName
            this.buyPrice = 88
            this.buyOldPrice = 110
            this.showBuyTicket = true
          }
        })
      } else if (this.activeTab === CBTab.Actors) {
        CBActorsContent({
          onOpenActor: (actor: CBActor) => {
            this.actorTarget = actor
            this.showActor = true
          }
        })
      } else if (this.activeTab === CBTab.Tickets) {
        CBTicketsContent({
          onRefund: (show: string) => {
            this.refundShowName = show
            this.showRefund = true
          },
          onReview: () => { this.showReview = true }
        })
      } else {
        CBAudienceContent()
      }

      // ===== 弹框层 =====
      if (this.showSignOpenMic) { this.signOpenMicModal() }
      if (this.showBuyTicket) { this.buyTicketModal() }
      if (this.showEditOpenMic) { this.editOpenMicModal() }
      if (this.showRefund) { this.refundModal() }
      if (this.showActor) { this.actorModal() }
      if (this.showReview) { this.reviewModal() }
    }
    .width('100%').height('100%')
  }

  build() {
    Column() {
      // ===== 头部:徽章 + 金字 + 搜索 + 消息角标(无动画,票务购物风) =====
      Row() {
        Stack() {
          Column()
            .width(34).height(34)
            .backgroundColor('#A32330')
            .borderRadius(10)
            .border({ width: 1, color: '#E9B84C' })
          Text('🎭').fontSize(18)
        }
        .width(34).height(34)

        Column() {
          Row() {
            Text('COMEDY').fontSize(15).fontWeight(FontWeight.Bold).fontColor('#F5EFE0')
            Text('BASE').fontSize(15).fontWeight(FontWeight.Bold).fontColor('#E9B84C')
          }
          Text('笑场 · 潮流脱口秀俱乐部').fontSize(8).fontColor('#A89CB8')
            .margin({ top: 1 })
        }
        .alignItems(HorizontalAlign.Start)
        .margin({ left: 8 })

        Row() {
          Text('🔍').fontSize(12).margin({ left: 8 })
          TextInput({ placeholder: '搜演出 / 演员 / 场次' })
            .placeholderColor('#6E6280').fontSize(11)
            .fontColor('#F5EFE0')
            .backgroundColor('#1F1930').borderRadius(16)
            .layoutWeight(1).height(32)
            .margin({ left: 6, right: 6 })
            .padding({ left: 10, right: 10 })
        }
        .layoutWeight(1)
        .margin({ left: 10, right: 6 })

        Stack() {
          Text('🔔').fontSize(18)
          Text('5').fontSize(9).fontWeight(FontWeight.Bold).fontColor('#141021')
            .backgroundColor('#E9B84C')
            .width(14).height(14).borderRadius(7)
            .textAlign(TextAlign.Center)
            .position({ x: 20, y: -2 })
        }
        .width(30).height(26)
        .margin({ right: 12 })
        .onClick(() => { this.activeTab = CBTab.Tickets })
      }
      .width('100%').height(54)
      .backgroundColor('#0C0916')
      .alignItems(VerticalAlign.Center)

      // ===== 内容区 Stack =====
      Column() {
        this.contentArea()
      }
      .layoutWeight(1)
      .backgroundColor('#141021')

      // ===== 底部 Tab(6 个,可点击切换) =====
      Row() {
        ForEach(CB_TABS, (item: CBTabItem) => {
          Column() {
            Text(item.icon).fontSize(18)
              .opacity(this.activeTab === item.tab ? 1.0 : 0.45)
            Text(item.label).fontSize(9)
              .fontColor(this.activeTab === item.tab ? '#E9B84C' : '#6E6280')
              .fontWeight(this.activeTab === item.tab ? FontWeight.Bold : FontWeight.Normal)
              .margin({ top: 1 })
            if (this.activeTab === item.tab) {
              Column().width(18).height(3)
                .backgroundColor('#E9B84C').borderRadius(2)
                .margin({ top: 2 })
            } else {
              Column().width(18).height(3)
                .backgroundColor('#0C0916').borderRadius(2)
                .margin({ top: 2 })
            }
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Center)
          .padding({ top: 5, bottom: 5 })
          .onClick(() => { this.activeTab = item.tab })
        })
      }
      .width('100%')
      .backgroundColor('#0C0916')
      .border({ width: { top: 1 }, color: '#352B4A' })
      .padding({ top: 3, bottom: 4 })
    }
    .width('100%').height('100%')
    .backgroundColor('#141021')
  }
}

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐