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

引言:当脱口秀遇上鸿蒙生态
在内容消费多元化的今天,线下脱口秀已成为都市年轻人重要的文化娱乐方式。从开放麦的新人试炼到商演的成熟专场,从演员粉丝运营到票务选座管理,一个完整的脱口秀俱乐部需要覆盖演出、演员、观众、票务四大维度的数字化能力。
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 渐变与光影效果
项目大量使用线性渐变营造剧场光影氛围:专场横幅渐变(主题色→深色)、聚光锥渐变(金色→透明)、演员卡片渐变(主题色→深黑)、头部横幅渐变(深红→深黑)。这些渐变配合属性动画,营造出动态的、有生命力的剧场光影体验。

三、开发环境搭建
3.1 环境要求
DevEco Studio 5.0+(支持HarmonyOS 6.1.1)、HarmonyOS SDK API 24、Node.js 18+、至少8GB内存。
3.2 安装DevEco Studio

从华为开发者官网下载最新版本,运行安装包,选择安装路径,勾选创建桌面快捷方式,点击安装。
3.3 配置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),共同营造"舞台灯光闪烁+笑声音符飘升"的剧场氛围。

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

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

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

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

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 性能优化策略
- 合理使用@State:只有需触发UI更新的状态用@State,静态数据用const,减少响应式开销
- 列表渲染优化:使用ForEach框架自动diff复用,列表项固定尺寸避免布局抖动
- 动画性能:使用属性动画而非逐帧动画,动画属性限制在opacity/translate/rotate/scale等合成层属性避免重排,无限循环用iterations:-1+Alternate零定时器
- 减少重渲染:弹框条件渲染未显示不渲染,避免build()中复杂计算
- 资源优化:使用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 技术亮点
- 无定时器循环动画:animateTo+iterations:-1+Alternate实现5组持续动画,零定时器高性能自动生命周期
- 可视化选座系统:5×8座位图三态管理,伪随机已售分布,分区定价动态总价
- 数据驱动周场次表:7天×3时段完全由数据驱动,3纯函数计算单元格样式
- 集中式弹框管理:6弹框统一在根组件Stack管理,布尔变量控制显隐,状态传递数据
- @Builder UI复用:18个@Builder方法封装可复用UI片段,大幅减少重复代码
14.3 对鸿蒙开发的启示
ArkTS声明式UI范式使开发者用简洁代码构建复杂界面。animateTo属性动画是一大亮点,声明式指定目标值框架自动处理中间帧,零代码实现持续循环动画。单文件架构在概念验证/教学演示/快速原型场景有独特优势。纯函数封装业务逻辑是被低估的最佳实践,无副作用易测试可复用。随着鸿蒙生态完善,未来脱口秀应用可通过原子化服务、万能卡片、多设备协同、AI能力、5G+AR实现更创新的体验。

附录:完整源码
以下是 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')
}
}
更多推荐



所有评论(0)