本文基于 HarmonyOS NEXT(API 24)ArkTS 声明式 UI 框架,完整拆解一个短剧剧场电商社区应用「多多短剧剧场」的源码实现。应用以短剧紫 #6200EA 为主色、潮流橙 #FF6D00 为强调色、底色 #F5F5FF 潮流短剧风,采用六组件多 Tab 短剧电商架构(HotPlayTab 热播 / RankTab 排行 / FollowTab 追剧 / RechargeTab 充值 / CommunityTab65 社区 / ProfileTab65 我的 六个独立 @Component 子组件 + DuoDuoDuanJuApp 主入口),每个 Tab 拥有独立的渐变头与差异化布局(热播=类型 chips 横滑+竖屏大卡+双列短剧卡;排行=4 榜单分段器+领奖台 TOP3+排名列表含趋势箭头;追剧=更新提醒+追剧列表含进度条+取消追剧;充值=金币余额头+套餐卡+VIP 特权卡+充值记录;社区=热门话题+剧评动态含评分星级;我的=渐变头+三列统计悬浮卡+编辑昵称+本周观看时长柱状图+观看记录),配套 4 种模态弹窗(充值金币底部抽屉含 6 套餐+3 支付方式 / 取消追剧居中 / 编辑昵称底部抽屉 / 删除观看记录居中可移除),覆盖从短剧热播浏览、排行榜发现、追剧管理、金币充值 VIP 开通、剧评社区互动到个人中心的完整短剧电商社区闭环。

多多短剧剧场整体运行效果

一、引言:追剧上瘾 · 一集上头

短剧是当下最火的内容消费形态——一集 1-2 分钟,节奏紧凑不拖沓,每集结尾都有反转,看得欲罢不能。「重生之复仇千金」女主重生后每一步都算无遗策,复仇过程太爽了;「无敌战神归来」战神回归搅动风云震惊天下,打戏场面太燃了;「万界神帝归来」玄幻短剧的天花板,特效不错剧情紧凑;「闪婚老伴是豪门」甜宠剧的标杆,男女主 CP 感太强了每集都在撒糖;「鬼医毒妃倾天下」古装宫斗做得太精良,女主又美又飒医术毒术设定新颖。「多多短剧剧场」正是为这群短剧爱好者打造的一站式短剧电商社区应用:用户可以在热播 Tab 按类型(全部/甜宠/复仇/玄幻/古装/热血)浏览 8 部短剧、在排行 Tab 查看 4 个榜单(热播榜/飙升榜/新剧榜/口碑榜)的领奖台 TOP3 与完整排名、在追剧 Tab 管理 5 部追剧(查看更新提醒与观看进度)、在充值 Tab 选择 6 款金币充值套餐(60 金币到 6480 金币)与 3 款 VIP 特权卡(月度/季度/年度)、在社区 Tab 浏览 4 个热门话题与 5 条剧评动态、在我的 Tab 查看追剧统计与本周观看时长柱状图。

应用的视觉语言高度统一:短剧紫 #6200EA 代表短剧的神秘与沉浸,潮流橙 #FF6D00 代表追剧的热情与上头,底色 #F5F5FF 营造潮流短剧的科技感氛围。六个 Tab 各自拥有独立的渐变头——热播 Tab 紫到橙渐变 #6200EA→#FF6D00(angle:135)、排行 Tab 橙到紫渐变 #FF6D00→#6200EA(angle:135)、追剧 Tab 紫到浅紫渐变 #6200EA→#9575CD(angle:180)、充值 Tab 紫到橙渐变 #6200EA→#FF6D00(angle:180)、社区 Tab 浅紫到紫渐变 #9575CD→#6200EA(angle:135)、我的 Tab 紫到橙渐变 #6200EA→#FF6D00(angle:180)——每个 Tab 通过渐变头的色彩组合快速区分模块,同时保持短剧紫潮流橙主色调的统一。

与多多亲子母婴、多多鲜花绿植相同,本应用采用六组件多 Tab 短剧电商架构——六个独立的 @Component 子组件各自管理自己的 @State 与弹窗,主入口 DuoDuoDuanJuApp 通过 @State activeTab 切换显示哪个子组件。我的 Tab 内嵌本周观看时长柱状图(7 天 WATCH_BARS_65,紫色系渐变色从浅 #D1C4E9 到深 #5500CC,周六 350m 最高形成「周末追剧时长飙升」可视化),是本应用最具特色的数据可视化。排行 Tab 的领奖台 TOP3(中间冠军高两侧亚军季军低,模拟真实领奖台视觉)和追剧 Tab 的进度条(currentEp/totalEp 百分比动态计算)是短剧应用区别于普通电商的特色交互。

本文将从架构概览、色彩系统、数据模型、六组件多 Tab 短剧电商架构、6 大 Tab 差异化布局、4 种模态弹窗、特性对比、完整代码、开发环境搭建等维度,对源码 65.ets 进行逐模块深度解析。

二、架构概览:六组件多 Tab 短剧电商架构 + 主入口分发

整个应用采用 ArkTS 声明式 UI 范式,多组件架构——六个独立的 @Component 子组件 + 一个 @Entry 主入口组件,通过主入口的 @State activeTab 状态切换显示哪个子组件。架构上可分为以下几层:

  • 类型定义层DramaItem(短剧 10 字段含 isNew 新剧标记)、RankItem(排行 7 字段含 trend 趋势箭头)、FollowDrama(追剧 5 字段含 updated 更新标记/currentEp/totalEp 进度)、RechargePlan(充值套餐 6 字段含 popular 热门/bonus 赠币)、VipPlan(VIP 卡 6 字段含 perks 权益数组/popular)、ReviewPost(剧评 8 字段含 rating 评分)、WatchRecord(观看记录 6 字段含 progress 进度百分比)、HotTopic(热门话题 5 字段含独立 color/bg)、BarData65(柱状图数据 3 字段),共 9 个 interface。
  • 静态配置层dramas(8 部短剧)、rankItems(8 部排行短剧含 trend)、followDramas(5 部追剧含 updated)、rechargePlans(6 款充值套餐含 bonus/popular)、vipPlans(3 款 VIP 卡含 perks 数组)、reviewPosts(5 条剧评含 rating)、watchRecords(6 条观看记录含 progress)、HOT_TOPICS(4 个热门话题含独立 color/bg)、WATCH_BARS_65(7 天观看时长柱状图数据紫色系渐变色)、RANK_LABELS(4 个榜单)、PAY_METHODS(3 种支付方式)。
  • 子组件层HotPlayTab(热播 Tab,含类型 chips 横滑+竖屏大卡+双列卡)、RankTab(排行 Tab,含 4 榜单分段器+领奖台 TOP3+排名列表)、FollowTab(追剧 Tab,含更新提醒+追剧列表进度条+取消追剧居中弹窗)、RechargeTab(充值 Tab,含金币余额头+套餐卡+VIP 卡+充值记录+充值金币底部抽屉弹窗)、CommunityTab65(社区 Tab,含热门话题+剧评动态)、ProfileTab65(我的 Tab,含渐变头+三列统计悬浮卡+编辑昵称底部抽屉+本周观看时长柱状图+观看记录+删除记录居中弹窗),共 6 个独立 @Component 子组件。
  • 主入口层DuoDuoDuanJuApp@Entry 主组件,@State activeTab 控制当前 Tab,@Builder bottomTabItem 构建底部 Tab 项,build() 中通过 if/else if 分发显示对应子组件,底部 6 Tab 导航栏带紫色系阴影)。

多多短剧剧场架构与热播Tab运行效果

主入口的 build() 方法以 Column 为根容器,上层是 Stack 包裹的子组件内容区(layoutWeight(1) 占满剩余空间),下层是 Row 布局的底部 6 Tab 导航栏。子组件内容区通过 if (this.activeTab === 0) { HotPlayTab() } 等条件渲染切换,每个子组件独立管理自己的 @State 与弹窗,互不干扰。底部 Tab 导航栏使用纯白底 + 紫色系阴影 shadow({ radius: 8, color: 'rgba(98,0,234,0.12)', offsetY: -2 }),激活态短剧紫 #6200EA 字粗体 + 图标 opacity 1.0,非激活态灰字 + 图标 opacity 0.45,通过 @Builder bottomTabItem(icon, label, tabIdx) 复用构建。

这种六组件多 Tab 短剧电商架构是中大型 ArkTS 应用的标准模式——每个子组件独立管理状态与弹窗,代码职责清晰、可复用性强、便于团队协作开发,主入口只负责 Tab 切换与分发,不涉及具体业务逻辑。

三、色彩系统:短剧紫主色 + 潮流橙强调 + 潮流短剧风

应用的配色通过 9 个 interface 中的 HotTopic(热门话题含 color/bg)、BarData65(柱状图含 color)和各数据模型中的 color 字段约束,由静态配置统一提供,核心色值如下:

色值名 HEX 用途
短剧紫 #6200EA 主色,按钮、标签、激活文字、播放量、柱状图深色、底部阴影
潮流橙 #FF6D00 强调色,评分、NEW 标签、更新标签、热门标签、充值按钮、价格
潮流底色 #F5F5FF 页面底色,潮流短剧风核心
纯白 #FFFFFF 卡片背景、底部导航栏
浅紫 #9575CD 追剧 Tab 渐变头终点、社区 Tab 渐变头起点、发表剧评统计
浅紫底 #EDE7F6 短剧图标区、头像背景、编辑昵称按钮底
浅橙底 #FFF3E0 冠军图标区、更新提醒背景、充值套餐选中态
价格红 #E53935 赠币提示
金色 #FF6D00 评分星号(与潮流橙同色)
#999999/#666666/#333333 次级文字/辅助文字/主文字

除了这 10 个基础色,应用还在多个场景中引入了热门话题独立色、紫色系渐变柱状图色以丰富视觉层次:

  • 热门话题独立色HOT_TOPICS 4 个热门话题各自拥有独立的 color/bg——本周热剧 #FF6D00/#FFF3E0(潮流橙)、复仇爽文 #6200EA/#EDE7F6(短剧紫)、甜宠专区 #EC407A/#FCE4EC(粉)、玄幻修仙 #26A69A/#E0F2F1(青)。热门话题横滑中每个话题卡片使用独立的 bg 底色 + color 文字色,用颜色直观区分话题类型。
  • 紫色系渐变柱状图WATCH_BARS_65 7 天观看时长数据各自拥有独立的紫色系渐变色——周一 #D1C4E9 最浅→周二 #B39DDB→周三 #9575CD→周四 #7E57C2→周五 #673AB7→周六 #6200EA→周日 #5500CC 最深。本周观看时长柱状图中每根柱子使用对应星期的紫色系渐变色,从浅到深形成「周末追剧时长飙升」的可视化效果,周六 350m 最高柱子最深(#6200EA),周三 90m 最低柱子最浅(#9575CD)。
  • 领奖台三色:排行 Tab 领奖台 TOP3 中,冠军图标区使用浅橙底 #FFF3E0(潮流橙系,冠军最高荣耀),亚军图标区使用浅蓝紫底 #E8EAF6,季军图标区使用浅青底 #E0F2F1,用颜色区分领奖台名次。
  • 排名数字色:排行 Tab 完整排名列表中,前 3 名排名数字使用潮流橙 #FF6D00(突出 TOP3),其他名次使用灰 #999999,用颜色区分排名档次。

多多短剧剧场色彩系统与排行Tab运行效果

渐变的运用是本应用色彩表达的重点——六个 Tab 各自拥有独立的渐变头

  • 热播 Tab:angle:135 紫到橙渐变 #6200EA→#FF6D00(从短剧紫到潮流橙,热播主色调)
  • 排行 Tab:angle:135 橙到紫渐变 #FF6D00→#6200EA(从潮流橙到短剧紫,排行荣耀感)
  • 追剧 Tab:angle:180 紫到浅紫渐变 #6200EA→#9575CD(从短剧紫到浅紫,垂直渐变追剧沉浸感)
  • 充值 Tab:angle:180 紫到橙渐变 #6200EA→#FF6D00(从短剧紫到潮流橙,垂直渐变充值消费感)
  • 社区 Tab:angle:135 浅紫到紫渐变 #9575CD→#6200EA(从浅紫到短剧紫,社区互动感)
  • 我的 Tab:angle:180 紫到橙渐变 #6200EA→#FF6D00(从短剧紫到潮流橙,垂直渐变个人中心感)

每个 Tab 通过渐变头的色彩组合快速区分模块,同时保持短剧紫潮流橙主色调的统一,是本应用最具特色的色彩设计。充值 Tab 还有金币余额渐变头(💰 图标 + 2680 金币大字 + 我的金币,紫到橙渐变 angle:180),是短剧充值消费的标准视觉元素。

四、数据模型设计:9 个 interface 强类型约束

应用定义了 9 个 interface,覆盖短剧、排行、追剧、充值套餐、VIP 卡、剧评、观看记录、热门话题、柱状图数据九大领域:

4.1 DramaItem 短剧模型(10 字段含 isNew 新剧标记)

interface DramaItem {
  id: number
  title: string
  icon: string
  genre: string
  episodes: number
  views: string
  rating: number
  tag: string
  desc: string
  isNew: boolean
}

短剧模型 10 字段覆盖了短剧商品的完整信息:id 唯一标识、title 剧名、icon 图标、genre 类型(都市甜宠/复仇爽文/豪门恩怨/热血都市/古装宫斗/都市霸总/玄幻修仙/种田经商)、episodes 集数、views 播放量(如 2.3亿)、rating 评分、tag 标签(热播/爆款/新剧/热销/古装/甜宠/玄幻/种田)、desc 简介、isNew 是否新剧(布尔值,控制 NEW 标签显示)。

isNew 是特色字段——布尔值标记是否为新剧,热播 Tab 中 if (d.isNew) 条件渲染「NEW」标签(潮流橙底白字),帮助用户快速发现新上线的短剧。dramas 常量预置了 8 部短剧,从 60 集替身后妈到 120 集鬼医毒妃,播放量从 1.2亿到 4.2亿,评分从 8.6 到 9.5,覆盖不同类型与热度,其中 3 部标记为新剧(重生之复仇千金/替身后妈闯豪门/我的冰山女总裁)。

4.2 RankItem 排行模型(7 字段含 trend 趋势箭头)

interface RankItem {
  id: number
  title: string
  icon: string
  views: string
  rating: number
  genre: string
  trend: string
}

排行模型 7 字段,trend 是特色字段——记录排名趋势箭头(↑上升/→持平/↓下降),排行 Tab 完整排名列表中显示 r.trend + (idx+1) 趋势+排名,帮助用户了解短剧热度变化趋势。rankItems 常量预置了 8 部排行短剧,无敌战神归来 4.2亿播放量排名第 1(趋势↑),重生之复仇千金 3.8亿排名第 2(趋势↑),万界神帝归来 3.1亿排名第 3(趋势→),覆盖上升/持平/下降三种趋势。

4.3 FollowDrama 追剧模型(5 字段含 updated 更新标记/currentEp/totalEp 进度)

interface FollowDrama {
  id: number
  title: string
  icon: string
  currentEp: number
  totalEp: number
  updated: boolean
}

追剧模型 5 字段,updated/currentEp/totalEp 是特色字段——updated 布尔值标记是否有更新(控制更新提醒列表与「更新」标签),currentEp 当前看到第几集、totalEp 总集数,追剧 Tab 中通过 (currentEp / totalEp * 100) 动态计算观看进度百分比,渲染进度条(紫色填充 + 浅灰底)与「N% 已观看」文字。followDramas 常量预置了 5 部追剧,重生之复仇千金看到第 45 集/共 100 集(45% 进度,有更新),无敌战神归来看到第 68 集/共 90 集(76% 进度,无更新),覆盖不同观看进度与更新状态。

4.4 RechargePlan 充值套餐模型(6 字段含 popular 热门/bonus 赠币)

interface RechargePlan {
  id: number
  coins: number
  bonus: number
  price: number
  tag: string
  popular: boolean
}

充值套餐模型 6 字段,bonus/popular 是特色字段——bonus 赠送金币数(如充 300 送 30),充值 Tab 中 if (p.bonus > 0) 条件渲染「+N 赠币」提示(价格红字);popular 布尔值标记是否为热门套餐,充值 Tab 中 if (p.popular) 条件渲染「热门」标签(潮流橙底白字)。rechargePlans 常量预置了 6 款充值套餐,从 60 金币 ¥6(新手)到 6480 金币 ¥648(土豪,送 1500 赠币),300 金币 ¥30 标记为热门(送 30 赠币),覆盖不同充值档位。

4.5 VipPlan VIP 卡模型(6 字段含 perks 权益数组/popular)

interface VipPlan {
  id: number
  name: string
  price: number
  originalPrice: number
  duration: string
  perks: string[]
  popular: boolean
}

VIP 卡模型 6 字段,perks 是特色字段——字符串数组存储 VIP 权益列表(如全站短剧免费看/免广告/1080P 画质/提前看 N 集/专属弹幕颜色/专属头像框/专属客服),充值 Tab 中通过 ForEach(v.perks) 渲染权益列表(✓ 紫色勾 + 权益文字)。vipPlans 常量预置了 3 款 VIP 卡,月度 VIP ¥25(30天,4 项权益)、季度 VIP ¥68(90天,5 项权益,标记为推荐 popular)、年度 VIP ¥198(365天,6 项权益含 4K 画质/专属头像框/专属客服),覆盖不同订阅周期。

4.6 其他辅助模型

interface ReviewPost { id: number; avatar: string; name: string; dramaTitle: string; rating: number; content: string; likes: number; date: string }
interface WatchRecord { id: number; title: string; icon: string; episode: number; progress: number; watchTime: string }
interface HotTopic { icon: string; label: string; posts: string; color: string; bg: string }
interface BarData65 { label: string; value: number; color: string }

ReviewPost 剧评 8 字段(含 rating 评分 1-5 星),reviewPosts 预置 5 条剧评,社区 Tab 中通过三元表达式 r.rating >= 1 ? '★★★★★' : ... 渲染评分星级(实际所有 rating 都是 4-5 星,显示 5 星或 4 星)。WatchRecord 观看记录 6 字段(含 progress 进度百分比 0-100),watchRecords 预置 6 条观看记录,我的 Tab 中通过 width(r.progress + '%') 动态渲染进度条宽度。HotTopic 热门话题 5 字段(含独立 color/bg),HOT_TOPICS 预置 4 个热门话题(本周热剧/复仇爽文/甜宠专区/玄幻修仙,各自独立配色)。BarData65 柱状图数据 3 字段(含紫色系渐变色),WATCH_BARS_65 预置 7 天观看时长数据(周一到周日,紫色系渐变从浅到深,周六 350m 最高)。

五、六组件多 Tab 短剧电商架构:6 个独立子组件 + 主入口分发

本应用采用六组件多 Tab 短剧电商架构,与多多亲子母婴、多多鲜花绿植相同的多组件模式。六个独立的 @Component 子组件各自管理自己的 @State 与弹窗,主入口 DuoDuoDuanJuApp 通过 @State activeTab 切换显示哪个子组件。

5.1 主入口 DuoDuoDuanJuApp:Tab 分发 + 底部导航

@Entry
@Component
struct DuoDuoDuanJuApp {
  @State activeTab: number = 0

  @Builder
  bottomTabItem(icon: string, label: string, tabIdx: number) {
    Column({ space: 3 }) {
      Text(icon).fontSize(20).opacity(this.activeTab === tabIdx ? 1.0 : 0.45)
      Text(label).fontSize(9)
        .fontColor(this.activeTab === tabIdx ? '#6200EA' : '#999999')
        .fontWeight(this.activeTab === tabIdx ? FontWeight.Bold : FontWeight.Normal)
    }
    .layoutWeight(1).alignItems(HorizontalAlign.Center)
    .padding({ top: 5, bottom: 5 })
    .onClick(() => { this.activeTab = tabIdx })
  }

  build() {
    Column() {
      Stack() {
        if (this.activeTab === 0) { HotPlayTab() }
        else if (this.activeTab === 1) { RankTab() }
        else if (this.activeTab === 2) { FollowTab() }
        else if (this.activeTab === 3) { RechargeTab() }
        else if (this.activeTab === 4) { CommunityTab65() }
        else { ProfileTab65() }
      }.layoutWeight(1)

      Row() {
        this.bottomTabItem('🎬', '热播', 0)
        this.bottomTabItem('🏆', '排行', 1)
        this.bottomTabItem('📺', '追剧', 2)
        this.bottomTabItem('💰', '充值', 3)
        this.bottomTabItem('💬', '社区', 4)
        this.bottomTabItem('👤', '我的', 5)
      }
      .width('100%').backgroundColor('#FFFFFF')
      .padding({ top: 4, bottom: 6 })
      .shadow({ radius: 8, color: 'rgba(98,0,234,0.12)', offsetY: -2 })
    }
    .width('100%').height('100%').backgroundColor('#F5F5FF')
  }
}

主入口是整个应用的调度中心:

  • @State activeTab: number = 0:当前激活的 Tab 索引,默认 0(热播)。
  • @Builder bottomTabItem(icon, label, tabIdx):可复用的底部 Tab 项构建器,接收图标、文字、Tab 索引三个参数,激活态短剧紫 #6200EA 字粗体 + 图标 opacity 1.0,非激活态灰字 + 图标 opacity 0.45,点击切换 activeTab
  • build()Column 根容器,上层 Stack 包裹子组件内容区(通过 if/else if 条件渲染分发显示对应子组件,layoutWeight(1) 占满剩余空间),下层 Row 布局的底部 6 Tab 导航栏(纯白底 + 紫色系阴影 shadow({ radius: 8, color: 'rgba(98,0,234,0.12)', offsetY: -2 }),通过 this.bottomTabItem() 复用构建 6 个 Tab 项)。
  • 根容器底色 #F5F5FF 潮流短剧风。

主入口只负责 Tab 切换与分发,不涉及具体业务逻辑,所有业务逻辑(数据渲染、弹窗、状态管理)都封装在各自的子组件中,这是多组件架构的核心优势——职责分离、代码独立、可复用性强、便于团队协作

5.2 六个子组件的状态与弹窗分布

子组件 @State 变量 弹窗 特色功能
HotPlayTab selectedGenre 类型 chips 横滑(6类型)+竖屏大卡(首部剧)+双列短剧卡(7部含NEW标签)
RankTab rankIdx 4榜单分段器+领奖台TOP3(中间冠军高两侧低)+排名列表(含trend趋势箭头+TOP3橙色数字)
FollowTab showCancel/cancelId/followList 取消追剧居中 更新提醒(updated=true的剧)+追剧列表(进度条currentEp/totalEp+百分比+✕取消)
RechargeTab showRecharge/selectedPlan/selectedPay 充值金币底部抽屉 金币余额渐变头+3套餐卡(热门标签)+VIP特权卡(3款含perks权益数组)+充值记录+充值弹窗(6套餐分两行+3支付方式)
CommunityTab65 热门话题4个(独立color/bg)+剧评动态(5条含评分星级三元表达式+点赞评论分享)
ProfileTab65 showEditName/nameVal/showDelete/delId/recordList 编辑昵称底部抽屉 + 删除观看记录居中 渐变头+三列统计悬浮卡(追剧部数/观看时长/发表剧评)+编辑昵称(TextInput)+本周观看时长柱状图(7天紫色系渐变)+观看记录(进度条+✕删除)

六个子组件中,FollowTab、RechargeTab、ProfileTab65 拥有弹窗(共 4 个),HotPlayTab、RankTab 拥有筛选状态,CommunityTab65 为纯展示组件。每个子组件独立管理自己的状态,互不干扰,切换 Tab 时子组件的状态会保留。

六、热播 Tab:类型 chips 横滑 + 竖屏大卡 + 双列短剧卡

热播 Tab HotPlayTab 是应用的默认首页,由紫到橙渐变头 + 类型 chips 横滑 + 今日热播竖屏大卡 + 全部短剧双列卡四部分组成。

6.1 渐变头 + 类型 chips 横滑

  • 紫到橙渐变头angle:135 紫到橙渐变 #6200EA→#FF6D00(从短剧紫到潮流橙,热播主色调),「🎬 多多短剧」18sp 粗体白字 + 「追剧上瘾 · 一集上头」11sp 半透明白字,居中对齐 padding top/bottom 20。
  • 类型 chips 横滑Scroll horizontal 横向滚动,genreList 6 个类型(全部/甜宠/复仇/玄幻/古装/热血),每个类型 chips 12sp,激活态短剧紫 #6200EA 底白字粗体圆角 16,非激活态浅灰底灰字圆角 16,padding 左右 12 上下 6,点击切换 selectedGenre。类型 chips 横滑是短剧应用的标准导航模式,6 个类型覆盖短剧的主要题材,帮助用户快速筛选感兴趣的类型。

6.2 今日热播竖屏大卡(首部剧 dramas[0])+ 全部短剧双列卡(7 部)

「🔥 今日热播」标题 + 「更新至第100集」潮流橙标签(SpaceBetween 两端对齐),下方是竖屏大卡(首部剧 dramas[0] 闪婚老伴是豪门):

  • 图标区:icon 56sp,width 100 height 140(竖屏比例,模拟短剧竖屏播放),#EDE7F6 浅紫底圆角 12 居中。
  • 右侧 Column:剧名 15sp 粗体深字 + if (dramas[0].isNew) 条件渲染「NEW」标签(潮流橙底白字)+ 类型·集数 11sp 灰字 + 简介 10sp 深灰字(maxLines 2 省略号)+ 播放量 ▶2.3亿(短剧紫粗体)+ 评分 ★9.2(潮流橙粗体)+ 「立即观看」按钮(12sp 白字粗体,短剧紫底 Capsule height 32 padding 左右 20)。

竖屏大卡纯白底圆角 12 padding 12 margin 左右 12 下 8。竖屏大卡是短剧应用的特色布局——短剧通常是竖屏播放,大卡使用 width 100 height 140 的竖屏比例模拟短剧竖屏播放界面,突出首部热播剧。

「📺 全部短剧」标题,下方是 Row 布局的双列网格(ForEach(dramas.slice(1, 8)) 渲染剩余 7 部,每款 layoutWeight(1) 等宽,自动换行形成双列),每款短剧卡片:

  • 图标区:icon 32sp,width 100% height 80,#EDE7F6 浅紫底圆角 8 居中。
  • 剧名 10sp 深字(maxLines 1 省略号)。
  • 类型 8sp 灰字。
  • 播放量 ▶N(8sp 短剧紫)+ 评分 ★N(8sp 潮流橙粗体)。
  • if (d.isNew) 条件渲染「NEW」标签(7sp 白字,潮流橙底圆角 3 padding 左右 3)。

双列网格纯白底圆角 8 padding 6 margin 左右 3 下 6,居中对齐。7 部短剧从重生之复仇千金(NEW 爆款 3.8亿 9.5分)到农门医妃种田忙(种田 1.2亿 8.6分),覆盖不同类型与热度,其中 2 部标记为新剧。

热播 Tab 通过类型 chips 横滑 + 竖屏大卡 + 双列短剧卡三段式布局,完整展示了短剧热播频道的类型筛选与商品浏览功能,类型 chips 帮助用户按题材筛选、竖屏大卡突出首部热播剧、双列卡紧凑展示全部短剧,是短剧应用热播频道的标准布局。

七、排行 Tab:4 榜单分段器 + 领奖台 TOP3 + 排名列表(含趋势箭头)

排行 Tab RankTab 由橙到紫渐变头 + 4 榜单分段器 + 领奖台 TOP3 + 完整排名列表四部分组成,领奖台 TOP3 是本应用最具特色的布局之一。

7.1 渐变头 + 4 榜单分段器

  • 橙到紫渐变头angle:135 橙到紫渐变 #FF6D00→#6200EA(从潮流橙到短剧紫,排行荣耀感),「🏆 排行榜」16sp 粗体白字 + 「数据每日更新」11sp 半透明白字,居中对齐 padding top/bottom 14。
  • 4 榜单分段器Row 布局,RANK_LABELS 4 个榜单(热播榜/飙升榜/新剧榜/口碑榜),每个榜单文字 13sp,激活态短剧紫 #6200EA 字粗体,非激活态灰字,layoutWeight(1) 等宽居中,padding 上下 10,点击切换 rankIdx。分段器纯白底,是排行应用的标准筛选模式,4 个榜单覆盖短剧排行的不同维度(热度/飙升/新剧/口碑)。

7.2 领奖台 TOP3(中间冠军高两侧亚军季军低)

「🥇 TOP3 领奖台」标题,下方是 Row 布局的领奖台,三列高度不同模拟真实领奖台视觉

  • 亚军(第 2 名,左侧)rankItems[1] 重生之复仇千金,icon 32sp width/height 50 浅蓝紫底 #E8EAF6 圆角 8 + 🥈 银牌 20sp + 剧名 9sp 深字(maxLines 1)+ 播放量 9sp 短剧紫粗体,padding top 30 bottom 10(比冠军低,模拟亚军领奖台高度)。
  • 冠军(第 1 名,中间)rankItems[0] 无敌战神归来,icon 40sp width/height 60 浅橙底 #FFF3E0 圆角 8(冠军最高荣耀用潮流橙系)+ 🥇 金牌 24sp + 剧名 10sp 深字粗体(maxLines 1)+ 播放量 10sp 潮流橙粗体,padding top 10 bottom 10(比亚军高,模拟冠军领奖台最高)。
  • 季军(第 3 名,右侧)rankItems[2] 万界神帝归来,icon 32sp width/height 50 浅青底 #E0F2F1 圆角 8 + 🥉 铜牌 20sp + 剧名 9sp 深字(maxLines 1)+ 播放量 9sp 短剧紫粗体,padding top 30 bottom 10(与亚军同高,模拟季军领奖台高度)。

领奖台三列纯白底圆角 10 margin 左右 3,等宽 layoutWeight 1 居中对齐。领奖台 TOP3 是本应用最具特色的布局之一——通过中间冠军 padding top 10(高)、两侧亚军季军 padding top 30(低)的高度差,模拟真实领奖台的视觉效果(冠军站在最高处,亚军季军站在两侧较低处),配合🥇🥈🥉奖牌图标和冠军浅橙底(潮流橙系最高荣耀),让排行 TOP3 更加直观生动。

7.3 完整排名列表(8 部,含 trend 趋势箭头 + TOP3 橙色数字)

「📋 完整排名」标题,下方 ForEach(rankItems) 渲染 8 部排名短剧,每行:

  • 排名数字:(idx+1) 16sp,前 3 名使用潮流橙 #FF6D00 粗体,其他使用灰 #999999,width 28 居中(用颜色区分排名档次,突出 TOP3)。
  • icon 28sp width/height 44 浅紫底 #EDE7F6 圆角 8 居中。
  • 剧名 13sp 粗体深字(maxLines 1)+ 类型·播放量 10sp 灰字。
  • 右侧 Column:评分 ★N 11sp 潮流橙粗体 + 趋势 r.trend + (idx+1) 9sp 灰字(如 ↑1 上升/→3 持平/↓4 下降)。

排名列表纯白底圆角 8 padding 8 margin 左右 12 下 4,垂直居中对齐。8 部短剧从无敌战神归来(第 1 名,趋势↑,4.2亿,9.1分)到农门医妃种田忙(第 8 名,趋势↓,1.2亿,8.6分),覆盖上升/持平/下降三种趋势。trend 趋势箭头是排行应用的特色字段——帮助用户了解短剧热度变化趋势(↑上升说明越来越火,↓下降说明热度下降),配合排名数字让排行信息更加丰富。

排行 Tab 通过 4 榜单分段器 + 领奖台 TOP3(中间冠军高两侧低)+ 完整排名列表(含趋势箭头+TOP3 橙色数字)三段式布局,完整展示了短剧排行频道的榜单筛选与排名展示功能,领奖台 TOP3 是本应用最具特色的布局之一,趋势箭头让排行信息更加丰富,是短剧应用排行频道的标准布局。

八、追剧 Tab:更新提醒 + 追剧列表(进度条 + 取消追剧)

追剧 Tab FollowTab 由紫到浅紫渐变头 + 更新提醒 + 追剧列表三部分组成,配套取消追剧居中弹窗,进度条是短剧应用的特色交互。

8.1 渐变头 + 更新提醒(updated=true 的剧)

  • 紫到浅紫渐变头angle:180 紫到浅紫渐变 #6200EA→#9575CD(从短剧紫到浅紫,垂直渐变追剧沉浸感),「📺 我的追剧」16sp 粗体白字 + 「追剧中 N 部 · 更新 N 部」11sp 半透明白字(动态显示 this.followList.length 追剧中数量 + this.followList.filter(f => f.updated).length 更新数量),居中对齐 padding top/bottom 14。
  • 更新提醒:「🔔 更新提醒」标题,下方 ForEach(this.followList.filter(f => f.updated)) 渲染有更新的追剧(3 部:重生之复仇千金/万界神帝归来/鬼医毒妃倾天下),每行:
    • 🔴 红点 8sp(更新提示)。
    • 「《剧名》更新至第 N 集」12sp 深字(layoutWeight 1)。
    • 「去追」10sp 潮流橙粗体(点击跳转观看,此处仅展示)。

更新提醒纯白底?不,是浅橙底 #FFF3E0 圆角 8 padding 8 margin 左右 12 下 4,垂直居中对齐。更新提醒是短剧应用的特色功能——短剧通常每天更新几集,用户追的剧有更新时需要及时提醒,通过 filter(f => f.updated) 筛选有更新的追剧,单独列出在更新提醒区域,配合🔴红点和浅橙底,让用户一眼看到哪些剧有更新。

8.2 追剧列表(5 部,进度条 currentEp/totalEp + 百分比 + ✕ 取消追剧)

「📋 追剧列表」标题,下方 ForEach(this.followList) 渲染 5 部追剧,每部卡片:

  • 顶部行:icon 36sp width 56 height 72(竖屏比例,模拟短剧竖屏封面)浅紫底 #EDE7F6 圆角 8 居中。
  • 右侧 Column:
    • 剧名 13sp 粗体深字(layoutWeight 1)+ if (f.updated) 条件渲染「更新」标签(潮流橙底白字圆角 3 padding 左右 4)。
    • 「看到第 N 集 / 共 N 集」11sp 灰字。
    • 进度条:外层 Column width 120 height 4 浅灰底 #F0F0F0 圆角 2,内层 Row width (f.currentEp / f.totalEp * 100) + '%' height 4 短剧紫 #6200EA 圆角 2(动态计算进度百分比,紫色填充从左到右)。
    • 「N% 已观看」9sp 短剧紫字(动态计算 (currentEp / totalEp * 100).toFixed(0))。
  • ✕ 关闭按钮 16sp 灰字(点击设置 cancelId + 打开取消追剧弹窗)。

追剧列表纯白底圆角 10 padding 10 margin 左右 12 下 6。5 部追剧从重生之复仇千金(看到第 45 集/共 100 集,45% 进度,有更新)到鬼医毒妃倾天下(看到第 90 集/共 120 集,75% 进度,有更新),覆盖不同观看进度与更新状态。

进度条是短剧应用的特色交互——通过 (currentEp / totalEp * 100) 动态计算观看进度百分比,渲染紫色填充进度条(从左到右填充)与「N% 已观看」文字,让用户直观看到每部追剧的观看进度。进度条使用纯声明式实现——外层浅灰底 Column + 内层短剧紫填充 Row(width 动态百分比),无需 Canvas,是 ArkTS 声明式 UI 进度条的标准模式。

追剧 Tab 通过更新提醒(updated=true 的剧)+ 追剧列表(进度条+百分比+✕取消追剧)两段式布局,完整展示了短剧追剧管理频道的更新提醒与追剧进度功能,更新提醒让用户及时看到有更新的剧、进度条让用户直观看到观看进度、✕取消追剧方便用户管理追剧列表,是短剧应用追剧频道的标准布局。

多多短剧剧场追剧Tab运行效果

九、充值 Tab:金币余额头 + 套餐卡 + VIP 特权卡 + 充值记录

充值 Tab RechargeTab 由紫到橙金币余额渐变头 + 金币充值套餐卡 + VIP 特权卡 + 充值记录四部分组成,配套充值金币底部抽屉弹窗(含 6 套餐+3 支付方式),是短剧应用的付费消费核心频道。

9.1 金币余额渐变头 + 金币充值套餐卡(3 款)

  • 金币余额渐变头angle:180 紫到橙渐变 #6200EA→#FF6D00(从短剧紫到潮流橙,垂直渐变充值消费感),💰 图标 36sp + 「2680」28sp 粗体白字(金币余额大字)+ 「我的金币」11sp 半透明白字,居中对齐 padding top/bottom 24。金币余额渐变头是短剧充值消费的标准视觉元素——大字显示金币余额,让用户一眼看到自己有多少金币。
  • 金币充值套餐卡:「💎 金币充值」标题 + 「充值」按钮(潮流橙底白字 Capsule height 30,点击打开充值弹窗),下方 Row 布局渲染 rechargePlans.slice(0, 3) 前 3 款套餐(60 金币/300 金币/680 金币),每款套餐卡片:
    • 金币数 16sp 短剧紫粗体 + 「金币」9sp 灰字。
    • if (p.bonus > 0) 条件渲染「+N 赠币」9sp 价格红(如充 300 送 30)。
    • 价格 ¥N 13sp 潮流橙粗体。
    • if (p.popular) 条件渲染「热门」标签(8sp 白字,潮流橙底圆角 3 padding 左右 4,300 金币套餐标记为热门)。

套餐卡纯白底圆角 8 padding 上下 10 margin 左右 3,等宽 layoutWeight 1 居中对齐,点击打开充值弹窗。3 款套餐从 60 金币 ¥6(新手,无赠币)到 680 金币 ¥68(超值,送 80 赠币),300 金币 ¥30 标记为热门(送 30 赠币),覆盖低中充值档位。

9.2 VIP 特权卡(3 款,含 perks 权益数组)+ 充值记录

  • VIP 特权卡:「👑 VIP特权卡」标题,下方 ForEach(vipPlans) 渲染 3 款 VIP 卡(月度/季度/年度),每款卡片:
    • 顶部行:左侧 Column——VIP 名 15sp 粗体深字 + if (v.popular) 条件渲染「推荐」标签(潮流橙底白字,季度 VIP 标记为推荐)+ 时长 11sp 灰字(30天/90天/365天);右侧 Column——价格 ¥N 20sp 潮流橙粗体 + 原价 ¥N 10sp 灰字删除线。
    • 权益列表ForEach(v.perks) 渲染权益数组,每项 ✓ 10sp 短剧紫粗体 + 权益文字 10sp 深灰字(如全站短剧免费看/免广告/1080P 画质/提前看 N 集/专属弹幕颜色/专属头像框/专属客服)。
    • 「开通VIP」按钮:13sp 白字粗体,Capsule height 32 width 100%,推荐款(季度 VIP)使用潮流橙底,其他使用短剧紫底(backgroundColor(v.popular ? '#FF6D00' : '#6200EA'))。

VIP 卡纯白底圆角 12 padding 12 margin 左右 12 下 8。3 款 VIP 卡从月度 VIP ¥25(30天,4 项权益)到年度 VIP ¥198(365天,6 项权益含 4K 画质/专属头像框/专属客服),季度 VIP ¥68 标记为推荐(90天,5 项权益),覆盖不同订阅周期。perks 权益数组是 VIP 卡的特色字段——通过字符串数组存储 VIP 权益列表,ForEach 渲染每项权益(✓ 紫色勾 + 权益文字),让用户直观看到每款 VIP 卡的权益差异,年度 VIP 权益最多(6 项),月度 VIP 权益最少(4 项)。

  • 充值记录:「📜 充值记录」标题,下方纯白底圆角 10 margin 左右 12 下 8,3 条记录(充值300金币+30赠币 ¥30 / 开通季度VIP ¥68 / 充值60金币 ¥6),每条记录 💰/👑 图标 16sp + 描述 12sp 深字(layoutWeight 1)+ 金额 ¥N 12sp 潮流橙粗体,记录之间用 1px 浅灰分隔线分隔。充值记录让用户查看历史充值消费,增加付费消费的透明度。

充值 Tab 通过金币余额渐变头 + 金币充值套餐卡(3 款含热门标签+赠币)+ VIP 特权卡(3 款含 perks 权益数组)+ 充值记录四段式布局,完整展示了短剧充值消费频道的金币充值与 VIP 开通功能,金币套餐卡满足用户解锁短剧的需求、VIP 特权卡满足用户免广告免费看的需求、充值记录让用户查看历史消费,是短剧应用充值频道的标准布局。

多多短剧剧场充值Tab运行效果

十、社区 Tab:热门话题 + 剧评动态(含评分星级)

社区 Tab CommunityTab65 由浅紫到紫渐变头 + 热门话题 + 热门剧评三部分组成,是短剧应用的社区互动核心频道。

10.1 渐变头 + 热门话题(4 个,独立 color/bg)

  • 浅紫到紫渐变头angle:135 浅紫到紫渐变 #9575CD→#6200EA(从浅紫到短剧紫,社区互动感),「💬 剧评社区」16sp 粗体白字 + 「分享追剧心得 · 讨论剧情」11sp 半透明白字,居中对齐 padding top/bottom 14。
  • 热门话题:「🔥 热门话题」标题,下方 Row 布局渲染 HOT_TOPICS 4 个热门话题,每个话题卡片:
    • icon 22sp。
    • 话题名 10sp 独立 color 字粗体(如本周热剧潮流橙/复仇爽文短剧紫/甜宠专区粉/玄幻修仙青)。
    • 帖子数 8sp 灰字(如 2.3万帖)。

话题卡使用独立的 bg 底色(如本周热剧浅橙底 #FFF3E0/复仇爽文浅紫底 #EDE7F6/甜宠专区浅粉底 #FCE4EC/玄幻修仙浅青底 #E0F2F1),圆角 8 padding 上下 10 margin 左右 3,等宽 layoutWeight 1 居中对齐。热门话题独立 color/bg 是社区 Tab 的特色设计——4 个热门话题各自拥有独立的配色(本周热剧潮流橙系/复仇爽文短剧紫系/甜宠专区粉系/玄幻修仙青系),用颜色直观区分话题类型,帮助用户快速找到感兴趣的话题社区。

10.2 热门剧评(5 条,含评分星级三元表达式 + 点赞评论分享)

「📝 热门剧评」标题,下方 ForEach(reviewPosts) 渲染 5 条剧评,每条剧评卡片:

  • 顶部行:avatar 28sp width/height 40 浅紫底 #EDE7F6 圆角 20(圆形头像)居中;右侧 Column——用户名 13sp 粗体深字 + 「评《剧名》」11sp 短剧紫字 + 评分星级(三元表达式 r.rating >= 1 ? '★★★★★' : r.rating >= 2 ? '★★★★' : ...,实际所有 rating 都是 4-5 星,显示 5 星或 4 星,10sp 潮流橙字)+ 日期 9sp 浅灰字。
  • 剧评内容 12sp 深灰字 lineHeight 18 width 100%(如「一口气追了45集根本停不下来!女主重生后每一步都算无遗策,复仇过程太爽了。强烈推荐!」)。
  • 底部行:👍 点赞 14sp + 点赞数 11sp 灰字 / 💬 评论 11sp 灰字 / 📤 分享 11sp 灰字,SpaceBetween 两端对齐。

剧评卡片纯白底圆角 10 padding 12 margin 左右 12 下 6。5 条剧评从追剧达人评重生之复仇千金(5 星,356 赞,08-24)到古装迷评鬼医毒妃倾天下(5 星,345 赞,08-20),覆盖不同短剧类型与评分。评分星级三元表达式是社区 Tab 的特色实现——通过嵌套三元表达式 r.rating >= 1 ? '★★★★★' : r.rating >= 2 ? '★★★★' : ... 根据评分动态渲染星级文字(5 星/4 星/3 星/2 星/1 星),是 ArkTS 中根据数值动态渲染文本的标准模式。

社区 Tab 通过热门话题(4 个独立 color/bg)+ 热门剧评(5 条含评分星级三元表达式+点赞评论分享)两段式布局,完整展示了短剧社区互动频道的话题分类与剧评浏览功能,热门话题帮助用户找到感兴趣的话题社区、剧评动态让用户分享追剧心得与讨论剧情,是短剧应用社区频道的标准布局。

多多短剧剧场社区Tab运行效果

十一、我的 Tab:渐变头 + 三列统计悬浮卡 + 编辑昵称 + 本周观看时长柱状图 + 观看记录

我的 Tab ProfileTab65 是应用信息最丰富的 Tab,由紫到橙渐变头 + 三列统计悬浮卡 + 编辑昵称 + 本周观看时长柱状图 + 观看记录五部分组成,配套编辑昵称底部抽屉弹窗和删除观看记录居中弹窗,本周观看时长柱状图是本应用最具特色的数据可视化。

11.1 紫到橙渐变头 + 三列统计悬浮卡

  • 紫到橙渐变头angle:180 紫到橙渐变 #6200EA→#FF6D00(从短剧紫到潮流橙,垂直渐变个人中心感),包含:🎭 头像 50sp,width/height 70,半透明白底圆角 35(圆形头像)居中;用户名 this.nameVal(默认「追剧狂人」,可通过编辑昵称修改)16sp 粗体白字;👑 年度 VIP 标签(半透明白底白字圆角 10)+ 💰 2680金币(半透明白字)。渐变头 padding top 30 bottom 20 居中对齐,是个人中心的标准头部设计。
  • 三列统计悬浮卡:纯白底圆角 10 padding 上下 12,margin 左右 12 top -12(向上偏移与渐变头重叠形成悬浮效果)+ shadow 阴影,等宽三列:
    • 「86 追剧部数」短剧紫粗体 16sp + 灰字 10sp
    • 「168h 观看时长」潮流橙粗体 16sp + 灰字 10sp
    • 「32 发表剧评」浅紫粗体 16sp + 灰字 10sp

三列统计卡通过 margin top: -12 向上偏移与渐变头重叠,形成悬浮卡片效果,是电商 App 个人中心的标准统计展示。三列使用短剧紫/潮流橙/浅紫三色对应追剧部数/观看时长/发表剧评,直观展示用户在短剧应用的活跃数据。

11.2 编辑昵称 + 本周观看时长柱状图(7 天紫色系渐变)

  • 编辑昵称:✏️ 图标 16sp + 「编辑昵称」13sp 短剧紫粗体(layoutWeight 1)+ > 箭头 16sp 浅灰字,浅紫底 #EDE7F6 圆角 10 padding 10 margin 左右 12 top 8,垂直居中对齐,点击打开编辑昵称底部抽屉弹窗。编辑昵称是个人中心的标准功能,用户可以修改自己的昵称。
  • 本周观看时长柱状图:「📊 本周观看时长」标题,下方是纯白底圆角 10 padding 12 margin 左右 12 下 8 的柱状图卡片:
    • Row space 4 布局,ForEach(WATCH_BARS_65) 渲染 7 根柱子(周一到周日),每根柱子 Column:
      • 顶部时长:「Nm」7sp 灰字(如 120m/180m/90m/210m/280m/350m/320m)。
      • 柱子主体:Column layoutWeight(1) width 16,使用独立的紫色系渐变色(周一 #D1C4E9 最浅→周二 #B39DDB→周三 #9575CD→周四 #7E57C2→周五 #673AB7→周六 #6200EA→周日 #5500CC 最深),圆角 3,justifyContent: FlexAlign.End(从底部向上生长)。
      • 底部星期标签:「周一/周二/…/周日」8sp 灰字。
    • 整行 height 110,每根柱子 height 100,等宽 layoutWeight 1 居中对齐。

本周观看时长柱状图是本应用最具特色的数据可视化——7 天观看时长数据使用紫色系渐变色从浅到深,周六 350m 最高柱子最深(#6200EA),周三 90m 最低柱子最浅(#9575CD),形成「周末追剧时长飙升」的可视化效果(周六周日追剧时长明显高于工作日)。柱子通过 layoutWeight(1) + justifyContent: FlexAlign.End 实现从底部向上生长的柱状图效果,顶部显示时长、底部显示星期,是 ArkTS 中纯声明式实现柱状图的标准模式(无需 Canvas,纯 Column + 背景色 + layoutWeight 实现)。紫色系渐变色从浅到深的配色方案,既统一了短剧紫主色调,又通过深浅变化直观表达观看时长的增长趋势。

11.3 观看记录(6 条,进度条 + ✕ 删除)

「⏱️ 观看记录」标题,下方 ForEach(this.recordList) 渲染 6 条观看记录,每行:

  • icon 28sp width/height 44 浅紫底 #EDE7F6 圆角 8 居中。
  • 右侧 Column:剧名 12sp 粗体深字(maxLines 1)+ 第 N 集 10sp 灰字 + 观看时间 10sp 灰字(如今天 14:30/昨天 22:00/2天前)+ 进度条(外层 width 100 height 3 浅灰底,内层 width r.progress + '%' height 3 短剧紫填充,动态进度百分比)。
  • ✕ 删除按钮 14sp 灰字(点击设置 delId + 打开删除观看记录弹窗)。

观看记录纯白底圆角 8 padding 8 margin 左右 12 下 4,垂直居中对齐。6 条记录从重生之复仇千金(第 45 集,78% 进度,今天 14:30)到我的冰山女总裁(第 20 集,30% 进度,3 天前),覆盖不同观看进度与时间。观看记录的进度条与追剧 Tab 的进度条实现方式相同——外层浅灰底 + 内层短剧紫填充(width 动态百分比),纯声明式实现。

我的 Tab 通过紫到橙渐变头 + 三列统计悬浮卡(追剧部数/观看时长/发表剧评)+ 编辑昵称 + 本周观看时长柱状图(7 天紫色系渐变,周六 350m 最高)+ 观看记录(进度条+✕删除)的丰富布局,完整展示了短剧应用个人中心的核心功能(活跃统计/昵称编辑/观看时长可视化/观看记录),本周观看时长柱状图是本应用最具特色的数据可视化,是短剧应用个人中心的标准布局。

多多短剧剧场我的Tab运行效果

十二、4 种模态弹窗:底部抽屉 + 居中两种样式

应用实现了 4 种模态弹窗,分布在 FollowTab(取消追剧居中)、RechargeTab(充值金币底部抽屉含 6 套餐+3 支付方式)、ProfileTab65(编辑昵称底部抽屉 + 删除观看记录居中),覆盖底部抽屉和居中两种弹窗样式。

12.1 充值金币底部抽屉(RechargeTab,6 套餐分两行 + 3 支付方式)

rechargeBuilderStack alignContent: Bottom 底部抽屉,纯白底圆角 topLeft/topRight 16 maxHeight 80%,包含:

  • 顶部行:💰 图标 20sp + 「充值金币」15sp 粗体深字(layoutWeight 1)+ ✕ 关闭按钮(灰字 18sp,点击关闭)。
  • 当前余额:「当前余额:2680 金币」12sp 灰字 padding 左 12。
  • 选择充值套餐:「选择充值套餐」12sp 灰字 + 6 套餐分两行渲染(第一行 rechargePlans.slice(0,3) 前 3 款,第二行 rechargePlans.slice(3,6) 后 3 款),每款套餐卡片:
    • 金币数 16sp(选中态潮流橙字,非选中深字)+ 「金币」9sp 灰字。
    • if (p.bonus > 0) 条件渲染「+N 赠币」8sp 价格红。
    • 价格 ¥N 12sp 短剧紫粗体。
    • 选中态:浅橙底 #FFF3E0 + 2px 潮流橙边框;非选中态:浅灰底 + 无边框。点击切换 selectedPlan
  • 选择支付方式:「选择支付方式」12sp 灰字 + ForEach(PAY_METHODS) 渲染 3 种支付方式(微信支付/支付宝/多多钱包),每行:
    • 选中态 🟢 绿点 / 非选中态 ⚪ 灰点 14sp。
    • 支付方式名 13sp 深字(layoutWeight 1)。
    • 箭头 14sp 浅灰字。

    • 点击切换 selectedPay
  • 确认按钮:「确认充值 ¥N」15sp 粗体白字(价格动态取 rechargePlans[this.selectedPlan].price),width 100% height 44,潮流橙底居中,点击关闭弹窗。

充值金币底部抽屉是短剧应用的标准付费交互——从底部滑出展示 6 款充值套餐(分两行 3+3)+ 3 种支付方式选择 + 确认充值按钮,不遮挡全部页面。6 套餐分两行渲染是充值弹窗的特色布局——6 款套餐较多,分两行(每行 3 款)展示更加紧凑美观。选中态使用浅橙底 + 潮流橙边框(与充值 Tab 主色调一致),3 种支付方式使用🟢/⚪ 圆点单选,是短剧应用充值弹窗的标准实现。

12.2 取消追剧居中弹窗(FollowTab,可移除)

cancelFollowBuilderStack alignContent: Center 居中,纯白底圆角 16 width 80% padding 20,包含:

  • 💔 图标 40sp
  • 「取消追剧?」15sp 粗体深字
  • 「取消后将从追剧列表中移除」12sp 灰字
  • 操作按钮行:「取消」浅灰底灰字 Capsule height 38(layoutWeight 1,点击关闭)+ 「确认」#F44336 红底白字粗体 Capsule height 38(layoutWeight 1,点击通过 filter 从 followList 移除对应 id 的追剧 + 关闭弹窗)。

取消追剧居中弹窗是轻量确认弹窗——💔 图标 + 简洁文案 + 双按钮,确认后通过 this.followList = this.followList.filter((f) => f.id !== this.cancelId) 实现可移除的追剧管理,是短剧应用的标准追剧取消交互。确认按钮使用红色 #F44336(强调取消追剧的不可逆性)。

12.3 编辑昵称底部抽屉(ProfileTab65,TextInput)

editNameBuilderStack alignContent: Bottom 底部抽屉,纯白底圆角 topLeft/topRight 16 maxHeight 50%,包含:

  • 顶部行:✏️ 图标 20sp + 「编辑昵称」15sp 粗体深字(layoutWeight 1)+ ✕ 关闭按钮(灰字 18sp,点击关闭)。
  • TextInput 文本输入框:placeholder「请输入昵称」,text 绑定 this.nameVal,width 100% height 40,fontSize 13,圆角 8,边框 1px 灰,margin 左右 12,onChange 更新 nameVal
  • 操作按钮行:「取消」浅灰底灰字 Capsule height 38(layoutWeight 1,点击关闭)+ 「保存」短剧紫底白字粗体 Capsule height 38(layoutWeight 1,点击关闭弹窗,nameVal 已通过 onChange 实时更新)。

编辑昵称底部抽屉是个人中心的标准交互——从底部滑出展示 TextInput 昵称输入框 + 保存按钮,不遮挡全部页面。TextInput 绑定 this.nameVal,onChange 实时更新,保存后渐变头的用户名会显示新昵称(因为 Text(this.nameVal) 响应式更新)。✏️ 铅笔图标直接对应编辑昵称主题,是个人中心的标准功能。

12.4 删除观看记录居中弹窗(ProfileTab65,可移除)

deleteRecordBuilderStack alignContent: Center 居中,纯白底圆角 16 width 80% padding 20,包含:

  • 🗑️ 垃圾桶图标 40sp
  • 「删除观看记录?」15sp 粗体深字
  • 「删除后无法恢复」12sp 灰字(强调删除的不可逆性)
  • 操作按钮行:「取消」浅灰底灰字 Capsule height 38(layoutWeight 1,点击关闭)+ 「删除」#F44336 红底白字粗体 Capsule height 38(layoutWeight 1,点击通过 filter 从 recordList 移除对应 id 的观看记录 + 关闭弹窗)。

删除观看记录居中弹窗与取消追剧弹窗结构类似,但文案更详细(删除后无法恢复),🗑️ 垃圾桶图标对应删除主题,确认后通过 this.recordList = this.recordList.filter((r) => r.id !== this.delId) 实现可移除的观看记录管理,是个人中心的标准观看记录删除交互。

4 种模态弹窗覆盖了底部抽屉(充值金币含 6 套餐+3 支付方式/编辑昵称 TextInput)和居中(取消追剧/删除观看记录)两种样式,底部抽屉用于需要输入或选择的复杂交互(6 套餐选择/3 支付方式选择/昵称 TextInput 输入),居中用于轻量确认交互(取消追剧/删除观看记录),是短剧应用弹窗设计的标准模式。

多多短剧剧场弹窗运行效果

十三、特性对比表

特性维度 多多短剧剧场实现
主题风格 短剧紫 #6200EA + 潮流橙 #FF6D00 + 底色 #F5F5FF 潮流短剧风
架构模式 六组件多 Tab 短剧电商架构(6 个独立 @Component 子组件 + 1 个 @Entry 主入口),主入口通过 @State activeTab + if/else if 分发显示子组件,每个子组件独立管理状态与弹窗
Tab 数量 底部 6 Tab(热播🎬/排行🏆/追剧📺/充值💰/社区💬/我的👤),每个 Tab 独立渐变头
渐变头 6 个 Tab 各自独立渐变头:热播紫→橙 angle:135 / 排行橙→紫 angle:135 / 追剧紫→浅紫 angle:180 / 充值紫→橙 angle:180 / 社区浅紫→紫 angle:135 / 我的紫→橙 angle:180
数据模型 9 个 interface(DramaItem 10字段含isNew / RankItem 7字段含trend趋势 / FollowDrama 5字段含updated/currentEp/totalEp进度 / RechargePlan 6字段含bonus赠币/popular / VipPlan 6字段含perks权益数组 / ReviewPost 8字段含rating评分 / WatchRecord 6字段含progress进度 / HotTopic 5字段含独立color/bg / BarData65 3字段)
业务数据量 8短剧+8排行(含trend)+5追剧(含updated)+6充值套餐(含bonus)+3VIP(含perks数组)+5剧评(含rating)+6观看记录(含progress)+4热门话题(独立color/bg)+7天观看时长(紫色系渐变)+4榜单+3支付方式
热门话题独立色 HOT_TOPICS 4 个话题各自独立 color/bg(本周热剧潮流橙/复仇爽文短剧紫/甜宠专区粉/玄幻修仙青),社区 Tab 中使用
紫色系渐变柱状图 WATCH_BARS_65 7 天观看时长各自独立紫色系渐变色(周一#D1C4E9最浅→周日#5500CC最深),本周观看时长柱状图从浅到深形成「周末追剧时长飙升」可视化,周六350m最高
领奖台TOP3 排行 Tab 领奖台三列高度不同(中间冠军padding top10高,两侧亚军季军padding top30低),模拟真实领奖台视觉,冠军浅橙底+🥇金牌,亚军浅蓝紫底+🥈银牌,季军浅青底+🥉铜牌
排名数字色 排行 Tab 完整排名前 3 名数字使用潮流橙,其他使用灰,用颜色区分排名档次突出 TOP3
趋势箭头 RankItem.trend 记录排名趋势(↑上升/→持平/↓下降),排行列表中显示趋势+排名,帮助用户了解短剧热度变化
进度条 追剧 Tab 追剧列表 + 我的 Tab 观看记录均使用进度条(currentEp/totalEp 或 progress 动态百分比,外层浅灰底+内层短剧紫填充),纯声明式实现无需 Canvas
更新提醒 追剧 Tab 通过 filter(f => f.updated) 筛选有更新的追剧,单独列出在更新提醒区域(🔴红点+浅橙底),让用户一眼看到哪些剧有更新
NEW标签 DramaItem.isNew 布尔值控制热播 Tab 竖屏大卡和双列卡中的「NEW」标签(潮流橙底白字),帮助用户快速发现新剧
VIP权益数组 VipPlan.perks 字符串数组存储 VIP 权益列表,充值 Tab 中通过 ForEach 渲染每项权益(✓紫色勾+权益文字),年度VIP权益最多6项,月度VIP最少4项
赠币提示 RechargePlan.bonus 赠送金币数,充值 Tab 中 if(p.bonus>0) 条件渲染「+N赠币」提示(价格红字),充值弹窗中同样显示
评分星级三元表达式 社区 Tab 剧评中通过嵌套三元表达式 r.rating>=1?‘★★★★★’:… 根据评分动态渲染星级文字,是 ArkTS 根据数值动态渲染文本的标准模式
6套餐分两行 充值弹窗中 6 款充值套餐分两行渲染(第一行前3款,第二行后3款),每行3款等宽,更加紧凑美观
3支付方式 充值弹窗中 PAY_METHODS 3 种支付方式(微信支付/支付宝/多多钱包),使用🟢/⚪圆点单选,点击切换 selectedPay
条件渲染 热播NEW标签 if(d.isNew) / 追剧更新标签 if(f.updated) / 充值赠币 if(p.bonus>0) / 充值热门 if(p.popular) / VIP推荐 if(v.popular),通过条件渲染实现差异化交互
模态弹窗 4 种:充值金币底部抽屉(6套餐分两行+3支付方式+动态价格确认) / 取消追剧居中(可移除filter) / 编辑昵称底部抽屉(TextInput绑定nameVal) / 删除观看记录居中(可移除filter),覆盖底部抽屉+居中两种样式
悬浮卡片 我的 Tab 三列统计卡通过 margin top:-12 向上偏移与渐变头重叠形成悬浮效果 + shadow 阴影
金币余额头 充值 Tab 金币余额渐变头(💰图标+2680金币大字+我的金币,紫到橙渐变angle:180),短剧充值消费的标准视觉元素
竖屏大卡 热播 Tab 首部剧使用 width100 height140 竖屏比例大卡,模拟短剧竖屏播放界面,突出首部热播剧
底部导航 6 Tab 纯白底 + 紫色系阴影 shadow(radius:8,color:rgba(98,0,234,0.12),offsetY:-2),激活态短剧紫字粗体+图标opacity1.0,非激活灰字+图标opacity0.45,@Builder bottomTabItem 复用构建
状态管理 6 个子组件各自独立 @State,主入口只管理 activeTab,多组件架构职责分离
特色字段 DramaItem.isNew新剧标记 / RankItem.trend趋势箭头 / FollowDrama.updated更新标记+currentEp/totalEp进度 / RechargePlan.bonus赠币+popular / VipPlan.perks权益数组 / ReviewPost.rating评分 / WatchRecord.progress进度 / HotTopic.color/bg独立色
布局差异化 热播=类型chips横滑(6类型)+竖屏大卡(首部剧)+双列短剧卡(7部含NEW) / 排行=4榜单分段器+领奖台TOP3(中间冠军高两侧低)+排名列表(含trend+TOP3橙色数字) / 追剧=更新提醒(updated)+追剧列表(进度条+百分比+✕取消) / 充值=金币余额头+3套餐卡(热门+赠币)+VIP特权卡(3款含perks权益)+充值记录 / 社区=热门话题4个(独立color/bg)+剧评动态(5条含评分星级三元表达式+点赞评论分享) / 我的=渐变头+三列统计悬浮卡+编辑昵称+本周观看时长柱状图(7天紫色系渐变)+观看记录(进度条+✕删除)
无粒子动画 本应用没有粒子动画系统,是纯电商静态布局应用
短剧电商社区闭环 热播浏览(类型筛选+竖屏大卡)+排行发现(领奖台TOP3+趋势)+追剧管理(更新提醒+进度条)+充值消费(金币套餐+VIP特权)+社区互动(热门话题+剧评评分)+个人中心(活跃统计+观看时长可视化),完整短剧电商社区闭环

十四、完整代码

应用完整源码位于 65.ets,总计约 1400 行,包含 9 个 interface、11 个常量数组/对象、6 个 @Component 子组件(HotPlayTab/RankTab/FollowTab/RechargeTab/CommunityTab65/ProfileTab65)+ 1 个 @Entry 主入口组件(DuoDuoDuanJuApp)。

核心代码结构如下:

// 65.ets 多多短剧剧场 —— 热播 / 排行 / 追剧 / 充值 / 社区 / 我的
// 主题:短剧紫 #6200EA × 潮流橙 #FF6D00,底色 #F5F5FF(潮流短剧风)
// 布局差异化:热播=竖屏大卡+双列短剧卡;排行=领奖台TOP3+排名列表;追剧=追剧列表+进度条+更新提醒;充值=套餐大卡+VIP卡+充值记录;社区=剧评动态+热门话题;我的=渐变头+观看时长+追剧统计+观看记录
// 弹框样式:充值金币(底部抽屉套餐+支付方式)、取消追剧(居中)、编辑昵称(底部抽屉)、删除观看记录(居中可移除)
// 底栏样式:紫橙渐变选中 + 未选灰色

// ============ 类型定义 ============
interface DramaItem {
  id: number
  title: string
  icon: string
  genre: string
  episodes: number
  views: string
  rating: number
  tag: string
  desc: string
  isNew: boolean
}

interface RankItem {
  id: number
  title: string
  icon: string
  views: string
  rating: number
  genre: string
  trend: string
}

interface FollowDrama {
  id: number
  title: string
  icon: string
  currentEp: number
  totalEp: number
  updated: boolean
}

interface RechargePlan {
  id: number
  coins: number
  bonus: number
  price: number
  tag: string
  popular: boolean
}

interface VipPlan {
  id: number
  name: string
  price: number
  originalPrice: number
  duration: string
  perks: string[]
  popular: boolean
}

interface ReviewPost {
  id: number
  avatar: string
  name: string
  dramaTitle: string
  rating: number
  content: string
  likes: number
  date: string
}

interface WatchRecord {
  id: number
  title: string
  icon: string
  episode: number
  progress: number
  watchTime: string
}

interface HotTopic {
  icon: string
  label: string
  posts: string
  color: string
  bg: string
}

interface BarData65 {
  label: string
  value: number
  color: string
}

// ============ 静态配置 ============
const dramas: DramaItem[] = [
  { id: 1, title: '闪婚老伴是豪门', icon: '💍', genre: '都市甜宠', episodes: 80, views: '2.3亿', rating: 9.2, tag: '热播', desc: '平凡女孩闪婚发现老公是豪门总裁', isNew: false },
  { id: 2, title: '重生之复仇千金', icon: '⚔️', genre: '复仇爽文', episodes: 100, views: '3.8亿', rating: 9.5, tag: '爆款', desc: '重生归来誓要让害她之人付出代价', isNew: true },
  { id: 3, title: '替身后妈闯豪门', icon: '👑', genre: '豪门恩怨', episodes: 60, views: '1.8亿', rating: 8.8, tag: '新剧', desc: '替身嫁入豪门意外获得真爱', isNew: true },
  { id: 4, title: '无敌战神归来', icon: '🥊', genre: '热血都市', episodes: 90, views: '4.2亿', rating: 9.1, tag: '热销', desc: '战神归来搅动风云震惊天下', isNew: false },
  { id: 5, title: '鬼医毒妃倾天下', icon: '🌹', genre: '古装宫斗', episodes: 120, views: '2.8亿', rating: 9.0, tag: '古装', desc: '穿越成废柴嫡女靠医术毒术逆袭', isNew: false },
  { id: 6, title: '我的冰山女总裁', icon: '🧊', genre: '都市霸总', episodes: 70, views: '1.5亿', rating: 8.7, tag: '甜宠', desc: '小职员与冰山女总裁的甜蜜日常', isNew: true },
  { id: 7, title: '万界神帝归来', icon: '🌟', genre: '玄幻修仙', episodes: 110, views: '3.1亿', rating: 9.3, tag: '玄幻', desc: '神帝重生纵横万界无人能挡', isNew: false },
  { id: 8, title: '农门医妃种田忙', icon: '🌿', genre: '种田经商', episodes: 85, views: '1.2亿', rating: 8.6, tag: '种田', desc: '穿越农女靠医术种田发家致富', isNew: false }
]

const rankItems: RankItem[] = [
  { id: 1, title: '无敌战神归来', icon: '🥊', views: '4.2亿', rating: 9.1, genre: '热血', trend: '↑' },
  { id: 2, title: '重生之复仇千金', icon: '⚔️', views: '3.8亿', rating: 9.5, genre: '复仇', trend: '↑' },
  { id: 3, title: '万界神帝归来', icon: '🌟', views: '3.1亿', rating: 9.3, genre: '玄幻', trend: '→' },
  { id: 4, title: '鬼医毒妃倾天下', icon: '🌹', views: '2.8亿', rating: 9.0, genre: '宫斗', trend: '↓' },
  { id: 5, title: '闪婚老伴是豪门', icon: '💍', views: '2.3亿', rating: 9.2, genre: '甜宠', trend: '↑' },
  { id: 6, title: '我的冰山女总裁', icon: '🧊', views: '1.5亿', rating: 8.7, genre: '霸总', trend: '↑' },
  { id: 7, title: '替身后妈闯豪门', icon: '👑', views: '1.8亿', rating: 8.8, genre: '豪门', trend: '→' },
  { id: 8, title: '农门医妃种田忙', icon: '🌿', views: '1.2亿', rating: 8.6, genre: '种田', trend: '↓' }
]

const followDramas: FollowDrama[] = [
  { id: 1, title: '重生之复仇千金', icon: '⚔️', currentEp: 45, totalEp: 100, updated: true },
  { id: 2, title: '无敌战神归来', icon: '🥊', currentEp: 68, totalEp: 90, updated: false },
  { id: 3, title: '万界神帝归来', icon: '🌟', currentEp: 82, totalEp: 110, updated: true },
  { id: 4, title: '闪婚老伴是豪门', icon: '💍', currentEp: 50, totalEp: 80, updated: false },
  { id: 5, title: '鬼医毒妃倾天下', icon: '🌹', currentEp: 90, totalEp: 120, updated: true }
]

const rechargePlans: RechargePlan[] = [
  { id: 1, coins: 60, bonus: 0, price: 6, tag: '新手', popular: false },
  { id: 2, coins: 300, bonus: 30, price: 30, tag: '日常', popular: true },
  { id: 3, coins: 680, bonus: 80, price: 68, tag: '超值', popular: false },
  { id: 4, coins: 1280, bonus: 200, price: 128, tag: '热门', popular: false },
  { id: 5, coins: 3280, bonus: 600, price: 328, tag: '豪华', popular: false },
  { id: 6, coins: 6480, bonus: 1500, price: 648, tag: '土豪', popular: false }
]

const vipPlans: VipPlan[] = [
  { id: 1, name: '月度VIP', price: 25, originalPrice: 30, duration: '30天', perks: ['全站短剧免费看', '免广告', '1080P画质', '提前看3集'], popular: false },
  { id: 2, name: '季度VIP', price: 68, originalPrice: 90, duration: '90天', perks: ['全站短剧免费看', '免广告', '1080P画质', '提前看5集', '专属弹幕颜色'], popular: true },
  { id: 3, name: '年度VIP', price: 198, originalPrice: 365, duration: '365天', perks: ['全站短剧免费看', '免广告', '4K画质', '提前看10集', '专属头像框', '专属客服'], popular: false }
]

const reviewPosts: ReviewPost[] = [
  { id: 1, avatar: '🎭', name: '追剧达人', dramaTitle: '重生之复仇千金', rating: 5, content: '一口气追了45集根本停不下来!女主重生后每一步都算无遗策,复仇过程太爽了。强烈推荐!', likes: 356, date: '08-24' },
  { id: 2, avatar: '👑', name: '短剧爱好者', dramaTitle: '无敌战神归来', rating: 4, content: '战神回归的设定太燃了,打戏场面做得不错。就是感情线有点突兀,总体值得追。', likes: 289, date: '08-23' },
  { id: 3, avatar: '⚔️', name: '爽文收割机', dramaTitle: '万界神帝归来', rating: 5, content: '玄幻短剧的天花板!特效不错,剧情紧凑不拖沓。每集结尾都有反转,看得欲罢不能。', likes: 412, date: '08-22' },
  { id: 4, avatar: '💍', name: '甜宠控', dramaTitle: '闪婚老伴是豪门', rating: 4, content: '甜宠剧的标杆!男女主CP感太强了,每集都在撒糖。就是套路有点老,但看着就是开心。', likes: 268, date: '08-21' },
  { id: 5, avatar: '🌹', name: '古装迷', dramaTitle: '鬼医毒妃倾天下', rating: 5, content: '古装宫斗做得太精良了,女主又美又飒。医术和毒术的设定很新颖,追了90集还看不够。', likes: 345, date: '08-20' }
]

const watchRecords: WatchRecord[] = [
  { id: 1, title: '重生之复仇千金', icon: '⚔️', episode: 45, progress: 78, watchTime: '今天 14:30' },
  { id: 2, title: '无敌战神归来', icon: '🥊', episode: 68, progress: 62, watchTime: '今天 10:15' },
  { id: 3, title: '万界神帝归来', icon: '🌟', episode: 82, progress: 90, watchTime: '昨天 22:00' },
  { id: 4, title: '闪婚老伴是豪门', icon: '💍', episode: 50, progress: 45, watchTime: '昨天 19:30' },
  { id: 5, title: '鬼医毒妃倾天下', icon: '🌹', episode: 90, progress: 88, watchTime: '2天前' },
  { id: 6, title: '我的冰山女总裁', icon: '🧊', episode: 20, progress: 30, watchTime: '3天前' }
]

const HOT_TOPICS: HotTopic[] = [
  { icon: '🔥', label: '本周热剧', posts: '2.3万帖', color: '#FF6D00', bg: '#FFF3E0' },
  { icon: '⚔️', label: '复仇爽文', posts: '1.8万帖', color: '#6200EA', bg: '#EDE7F6' },
  { icon: '💖', label: '甜宠专区', posts: '1.5万帖', color: '#EC407A', bg: '#FCE4EC' },
  { icon: '🌟', label: '玄幻修仙', posts: '1.2万帖', color: '#26A69A', bg: '#E0F2F1' }
]

const WATCH_BARS_65: BarData65[] = [
  { label: '周一', value: 120, color: '#D1C4E9' },
  { label: '周二', value: 180, color: '#B39DDB' },
  { label: '周三', value: 90, color: '#9575CD' },
  { label: '周四', value: 210, color: '#7E57C2' },
  { label: '周五', value: 280, color: '#673AB7' },
  { label: '周六', value: 350, color: '#6200EA' },
  { label: '周日', value: 320, color: '#5500CC' }
]

const RANK_LABELS: string[] = ['热播榜', '飙升榜', '新剧榜', '口碑榜']
const PAY_METHODS: string[] = ['微信支付', '支付宝', '多多钱包']

// ============ 全局弹窗遮罩 ============
function modalOverlay(onClose: () => void) {
}

// ============ Tab1 热播 ============
@Component
struct HotPlayTab {
  @State selectedGenre: number = 0
  private genreList: string[] = ['全部', '甜宠', '复仇', '玄幻', '古装', '热血']

  build() {
    Scroll() {
      Column() {
        Column({ space: 4 }) {
          Text('🎬 多多短剧')
            .fontSize(18)
            .fontColor('#FFFFFF')
            .fontWeight(FontWeight.Bold)
          Text('追剧上瘾 · 一集上头')
            .fontSize(11)
            .fontColor('rgba(255,255,255,0.8)')
        }
        .width('100%')
        .padding({ top: 20, bottom: 20 })
        .linearGradient({
          colors: [['#6200EA', 0.0], ['#FF6D00', 1.0]],
          angle: 135
        })
        .alignItems(HorizontalAlign.Center)

        Scroll() {
          Row({ space: 6 }) {
            ForEach(this.genreList, (g: string, idx: number) => {
              Text(g)
                .fontSize(12)
                .fontColor(this.selectedGenre === idx ? '#FFFFFF' : '#666666')
                .fontWeight(this.selectedGenre === idx ? FontWeight.Bold : FontWeight.Normal)
                .padding({ left: 12, right: 12, top: 6, bottom: 6 })
                .backgroundColor(this.selectedGenre === idx ? '#6200EA' : '#F5F5F5')
                .borderRadius(16)
                .onClick(() => {
                  this.selectedGenre = idx
                })
            }, (g: string) => g)
          }
          .padding({ left: 12, right: 12 })
        }
        .scrollable(ScrollDirection.Horizontal)
        .scrollBar(BarState.Off)
        .width('100%')
        .padding({ top: 10, bottom: 10 })

        Row({ space: 6 }) {
          Text('🔥 今日热播')
            .fontSize(15)
            .fontColor('#333333')
            .fontWeight(FontWeight.Bold)
          Text('更新至第100集')
            .fontSize(11)
            .fontColor('#FF6D00')
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)
        .padding({ left: 12, right: 12, top: 4, bottom: 6 })

        Column({ space: 10 }) {
          Row({ space: 12 }) {
            Text(dramas[0].icon)
              .fontSize(56)
              .width(100)
              .height(140)
              .backgroundColor('#EDE7F6')
              .borderRadius(12)
              .textAlign(TextAlign.Center)
            Column({ space: 6 }) {
              Row({ space: 6 }) {
                Text(dramas[0].title)
                  .fontSize(15)
                  .fontColor('#333333')
                  .fontWeight(FontWeight.Bold)
                  .layoutWeight(1)
                if (dramas[0].isNew) {
                  Text('NEW')
                    .fontSize(8)
                    .fontColor('#FFFFFF')
                    .backgroundColor('#FF6D00')
                    .borderRadius(3)
                    .padding({ left: 4, right: 4 })
                }
              }
              .width('100%')
              Text(dramas[0].genre + ' · ' + dramas[0].episodes + '集')
                .fontSize(11)
                .fontColor('#999999')
              Text(dramas[0].desc)
                .fontSize(10)
                .fontColor('#666666')
                .maxLines(2)
                .textOverflow({ overflow: TextOverflow.Ellipsis })
              Row({ space: 8 }) {
                Text('▶ ' + dramas[0].views)
                  .fontSize(11)
                  .fontColor('#6200EA')
                  .fontWeight(FontWeight.Bold)
                Text('★' + dramas[0].rating)
                  .fontSize(11)
                  .fontColor('#FF6D00')
                  .fontWeight(FontWeight.Bold)
              }
              Button() {
                Text('立即观看')
                  .fontSize(12)
                  .fontColor('#FFFFFF')
                  .fontWeight(FontWeight.Bold)
              }
              .type(ButtonType.Capsule)
              .height(32)
              .backgroundColor('#6200EA')
              .padding({ left: 20, right: 20 })
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Start)
          }
          .width('100%')
          .alignItems(VerticalAlign.Top)
        }
        .width('100%')
        .padding(12)
        .backgroundColor('#FFFFFF')
        .borderRadius(12)
        .margin({ left: 12, right: 12, bottom: 8 })

        Row({ space: 6 }) {
          Text('📺 全部短剧')
            .fontSize(15)
            .fontColor('#333333')
            .fontWeight(FontWeight.Bold)
        }
        .width('100%')
        .padding({ left: 12, right: 12, top: 8, bottom: 6 })

        Row() {
          ForEach(dramas.slice(1, 8), (d: DramaItem) => {
            Column({ space: 5 }) {
              Text(d.icon)
                .fontSize(32)
                .width('100%')
                .height(80)
                .backgroundColor('#EDE7F6')
                .borderRadius(8)
                .textAlign(TextAlign.Center)
              Text(d.title)
                .fontSize(10)
                .fontColor('#333333')
                .maxLines(1)
                .textOverflow({ overflow: TextOverflow.Ellipsis })
              Text(d.genre)
                .fontSize(8)
                .fontColor('#999999')
              Row({ space: 4 }) {
                Text('▶' + d.views)
                  .fontSize(8)
                  .fontColor('#6200EA')
                Text('★' + d.rating)
                  .fontSize(8)
                  .fontColor('#FF6D00')
                  .fontWeight(FontWeight.Bold)
              }
              if (d.isNew) {
                Text('NEW')
                  .fontSize(7)
                  .fontColor('#FFFFFF')
                  .backgroundColor('#FF6D00')
                  .borderRadius(3)
                  .padding({ left: 3, right: 3 })
              }
            }
            .layoutWeight(1)
            .padding(6)
            .backgroundColor('#FFFFFF')
            .borderRadius(8)
            .margin({ left: 3, right: 3, bottom: 6 })
            .alignItems(HorizontalAlign.Center)
          }, (d: DramaItem) => d.id.toString())
        }
        .width('100%')
        .padding({ left: 4, right: 4 })

        Column()
          .height(80)
      }
    }
    .scrollBar(BarState.Off)
    .constraintSize({ maxHeight: '100%' })
  }
}

// ============ Tab2 排行 ============
@Component
struct RankTab {
  @State rankIdx: number = 0

  build() {
    Scroll() {
      Column() {
        Column() {
          Text('🏆 排行榜')
            .fontSize(16)
            .fontColor('#FFFFFF')
            .fontWeight(FontWeight.Bold)
          Text('数据每日更新')
            .fontSize(11)
            .fontColor('rgba(255,255,255,0.8)')
        }
        .width('100%')
        .padding({ top: 14, bottom: 14 })
        .linearGradient({
          colors: [['#FF6D00', 0.0], ['#6200EA', 1.0]],
          angle: 135
        })
        .alignItems(HorizontalAlign.Center)

        Row({ space: 0 }) {
          ForEach(RANK_LABELS, (r: string, idx: number) => {
            Text(r)
              .fontSize(13)
              .fontColor(this.rankIdx === idx ? '#6200EA' : '#999999')
              .fontWeight(this.rankIdx === idx ? FontWeight.Bold : FontWeight.Normal)
              .layoutWeight(1)
              .textAlign(TextAlign.Center)
              .padding({ top: 10, bottom: 10 })
              .borderColor('#6200EA')
              .onClick(() => {
                this.rankIdx = idx
              })
          }, (r: string) => r)
        }
        .width('100%')
        .backgroundColor('#FFFFFF')

        Text('🥇 TOP3 领奖台')
          .fontSize(14)
          .fontColor('#333333')
          .fontWeight(FontWeight.Bold)
          .margin({ left: 12, top: 10, bottom: 6 })

        Row() {
          Column({ space: 6 }) {
            Text(rankItems[1].icon)
              .fontSize(32)
              .width(50)
              .height(50)
              .backgroundColor('#E8EAF6')
              .borderRadius(8)
              .textAlign(TextAlign.Center)
            Text('🥈')
              .fontSize(20)
            Text(rankItems[1].title)
              .fontSize(9)
              .fontColor('#333333')
              .maxLines(1)
              .textOverflow({ overflow: TextOverflow.Ellipsis })
            Text(rankItems[1].views)
              .fontSize(9)
              .fontColor('#6200EA')
              .fontWeight(FontWeight.Bold)
          }
          .layoutWeight(1)
          .padding({ top: 30, bottom: 10 })
          .backgroundColor('#FFFFFF')
          .borderRadius(10)
          .margin({ left: 3, right: 3 })
          .alignItems(HorizontalAlign.Center)

          Column({ space: 6 }) {
            Text(rankItems[0].icon)
              .fontSize(40)
              .width(60)
              .height(60)
              .backgroundColor('#FFF3E0')
              .borderRadius(8)
              .textAlign(TextAlign.Center)
            Text('🥇')
              .fontSize(24)
            Text(rankItems[0].title)
              .fontSize(10)
              .fontColor('#333333')
              .fontWeight(FontWeight.Bold)
              .maxLines(1)
              .textOverflow({ overflow: TextOverflow.Ellipsis })
            Text(rankItems[0].views)
              .fontSize(10)
              .fontColor('#FF6D00')
              .fontWeight(FontWeight.Bold)
          }
          .layoutWeight(1)
          .padding({ top: 10, bottom: 10 })
          .backgroundColor('#FFFFFF')
          .borderRadius(10)
          .margin({ left: 3, right: 3 })
          .alignItems(HorizontalAlign.Center)

          Column({ space: 6 }) {
            Text(rankItems[2].icon)
              .fontSize(32)
              .width(50)
              .height(50)
              .backgroundColor('#E0F2F1')
              .borderRadius(8)
              .textAlign(TextAlign.Center)
            Text('🥉')
              .fontSize(20)
            Text(rankItems[2].title)
              .fontSize(9)
              .fontColor('#333333')
              .maxLines(1)
              .textOverflow({ overflow: TextOverflow.Ellipsis })
            Text(rankItems[2].views)
              .fontSize(9)
              .fontColor('#6200EA')
              .fontWeight(FontWeight.Bold)
          }
          .layoutWeight(1)
          .padding({ top: 30, bottom: 10 })
          .backgroundColor('#FFFFFF')
          .borderRadius(10)
          .margin({ left: 3, right: 3 })
          .alignItems(HorizontalAlign.Center)
        }
        .width('100%')
        .padding({ left: 4, right: 4 })

        Text('📋 完整排名')
          .fontSize(14)
          .fontColor('#333333')
          .fontWeight(FontWeight.Bold)
          .margin({ left: 12, top: 12, bottom: 6 })

        ForEach(rankItems, (r: RankItem, idx: number) => {
          Row({ space: 10 }) {
            Text((idx + 1).toString())
              .fontSize(16)
              .fontColor(idx < 3 ? '#FF6D00' : '#999999')
              .fontWeight(FontWeight.Bold)
              .width(28)
              .textAlign(TextAlign.Center)
            Text(r.icon)
              .fontSize(28)
              .width(44)
              .height(44)
              .backgroundColor('#EDE7F6')
              .borderRadius(8)
              .textAlign(TextAlign.Center)
            Column({ space: 3 }) {
              Text(r.title)
                .fontSize(13)
                .fontColor('#333333')
                .fontWeight(FontWeight.Bold)
                .maxLines(1)
                .textOverflow({ overflow: TextOverflow.Ellipsis })
              Text(r.genre + ' · ' + r.views)
                .fontSize(10)
                .fontColor('#999999')
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Start)
            Column({ space: 2 }) {
              Text('★' + r.rating)
                .fontSize(11)
                .fontColor('#FF6D00')
                .fontWeight(FontWeight.Bold)
              Text(r.trend + (idx + 1))
                .fontSize(9)
                .fontColor('#999999')
            }
            .alignItems(HorizontalAlign.End)
          }
          .width('100%')
          .padding(8)
          .backgroundColor('#FFFFFF')
          .borderRadius(8)
          .margin({ left: 12, right: 12, bottom: 4 })
          .alignItems(VerticalAlign.Center)
        }, (r: RankItem) => r.id.toString())

        Column()
          .height(80)
      }
    }
    .scrollBar(BarState.Off)
    .constraintSize({ maxHeight: '100%' })
  }
}

// ============ Tab3 追剧 ============
@Component
struct FollowTab {
  @State showCancel: boolean = false
  @State cancelId: number = 0
  @State followList: FollowDrama[] = followDramas

  @Builder
  cancelFollowBuilder() {
    Stack({ alignContent: Alignment.Center }) {
      Column({ space: 12 }) {
        Text('💔')
          .fontSize(40)
        Text('取消追剧?')
          .fontSize(15)
          .fontColor('#333333')
          .fontWeight(FontWeight.Bold)
        Text('取消后将从追剧列表中移除')
          .fontSize(12)
          .fontColor('#999999')
        Row({ space: 12 }) {
          Button() {
            Text('取消')
              .fontSize(13)
              .fontColor('#666666')
          }
          .type(ButtonType.Capsule)
          .height(38)
          .backgroundColor('#F5F5F5')
          .layoutWeight(1)
          .onClick(() => {
            this.showCancel = false
          })
          Button() {
            Text('确认')
              .fontSize(13)
              .fontColor('#FFFFFF')
              .fontWeight(FontWeight.Bold)
          }
          .type(ButtonType.Capsule)
          .height(38)
          .backgroundColor('#F44336')
          .layoutWeight(1)
          .onClick(() => {
            this.followList = this.followList.filter((f: FollowDrama) => f.id !== this.cancelId)
            this.showCancel = false
          })
        }
        .width('100%')
      }
      .width('80%')
      .padding(20)
      .backgroundColor('#FFFFFF')
      .borderRadius(16)
      .alignItems(HorizontalAlign.Center)
    }
    .width('100%')
    .height('100%')
  }

  build() {
    Stack({ alignContent: Alignment.Bottom }) {
      Scroll() {
        Column() {
          Column() {
            Text('📺 我的追剧')
              .fontSize(16)
              .fontColor('#FFFFFF')
              .fontWeight(FontWeight.Bold)
            Text('追剧中 ' + this.followList.length + ' 部 · 更新 ' + this.followList.filter((f: FollowDrama) => f.updated).length + ' 部')
              .fontSize(11)
              .fontColor('rgba(255,255,255,0.8)')
          }
          .width('100%')
          .padding({ top: 14, bottom: 14 })
          .linearGradient({
            colors: [['#6200EA', 0.0], ['#9575CD', 1.0]],
            angle: 180
          })
          .alignItems(HorizontalAlign.Center)

          Text('🔔 更新提醒')
            .fontSize(14)
            .fontColor('#333333')
            .fontWeight(FontWeight.Bold)
            .margin({ left: 12, top: 8, bottom: 6 })

          ForEach(this.followList.filter((f: FollowDrama) => f.updated), (f: FollowDrama) => {
            Row({ space: 10 }) {
              Text('🔴')
                .fontSize(8)
              Text('《' + f.title + '》更新至第' + f.currentEp + '集')
                .fontSize(12)
                .fontColor('#333333')
                .layoutWeight(1)
              Text('去追')
                .fontSize(10)
                .fontColor('#FF6D00')
                .fontWeight(FontWeight.Bold)
            }
            .width('100%')
            .padding(8)
            .backgroundColor('#FFF3E0')
            .borderRadius(8)
            .margin({ left: 12, right: 12, bottom: 4 })
            .alignItems(VerticalAlign.Center)
          }, (f: FollowDrama) => f.id.toString())

          Text('📋 追剧列表')
            .fontSize(14)
            .fontColor('#333333')
            .fontWeight(FontWeight.Bold)
            .margin({ left: 12, top: 10, bottom: 6 })

          ForEach(this.followList, (f: FollowDrama) => {
            Column({ space: 8 }) {
              Row({ space: 10 }) {
                Text(f.icon)
                  .fontSize(36)
                  .width(56)
                  .height(72)
                  .backgroundColor('#EDE7F6')
                  .borderRadius(8)
                  .textAlign(TextAlign.Center)
                Column({ space: 4 }) {
                  Row({ space: 6 }) {
                    Text(f.title)
                      .fontSize(13)
                      .fontColor('#333333')
                      .fontWeight(FontWeight.Bold)
                      .layoutWeight(1)
                    if (f.updated) {
                      Text('更新')
                        .fontSize(8)
                        .fontColor('#FFFFFF')
                        .backgroundColor('#FF6D00')
                        .borderRadius(3)
                        .padding({ left: 4, right: 4 })
                    }
                  }
                  .width('100%')
                  Text('看到第' + f.currentEp + '集 / 共' + f.totalEp + '集')
                    .fontSize(11)
                    .fontColor('#999999')
                  Row() {
                    Column() {
                      Row() {
                        Column()
                          .width((f.currentEp / f.totalEp * 100) + '%')
                          .height(4)
                          .backgroundColor('#6200EA')
                          .borderRadius(2)
                      }
                      .width('100%')
                      .height(4)
                      .backgroundColor('#F0F0F0')
                      .borderRadius(2)
                    }
                    .width(120)
                  }
                  Text((f.currentEp / f.totalEp * 100).toFixed(0) + '% 已观看')
                    .fontSize(9)
                    .fontColor('#6200EA')
                }
                .layoutWeight(1)
                .alignItems(HorizontalAlign.Start)
                Text('✕')
                  .fontSize(16)
                  .fontColor('#999999')
                  .onClick(() => {
                    this.cancelId = f.id
                    this.showCancel = true
                  })
              }
              .width('100%')
              .alignItems(VerticalAlign.Top)
            }
            .width('100%')
            .padding(10)
            .backgroundColor('#FFFFFF')
            .borderRadius(10)
            .margin({ left: 12, right: 12, bottom: 6 })
          }, (f: FollowDrama) => f.id.toString())

          Column()
            .height(80)
        }
      }
      .scrollBar(BarState.Off)
      .constraintSize({ maxHeight: '100%' })

      if (this.showCancel) {
        this.cancelFollowBuilder()
      }
    }
    .width('100%')
    .height('100%')
  }
}

// ============ Tab4 充值 ============
@Component
struct RechargeTab {
  @State showRecharge: boolean = false
  @State selectedPlan: number = 1
  @State selectedPay: number = 0

  @Builder
  rechargeBuilder() {
    Stack({ alignContent: Alignment.Bottom }) {
      Column() {
        Column({ space: 12 }) {
          Row({ space: 8 }) {
            Text('💰')
              .fontSize(20)
            Text('充值金币')
              .fontSize(15)
              .fontColor('#333333')
              .fontWeight(FontWeight.Bold)
              .layoutWeight(1)
            Text('✕')
              .fontSize(18)
              .fontColor('#999999')
              .onClick(() => {
                this.showRecharge = false
              })
          }
          .width('100%')
          .padding(12)

          Text('当前余额:2680 金币')
            .fontSize(12)
            .fontColor('#666666')
            .padding({ left: 12 })

          Text('选择充值套餐')
            .fontSize(12)
            .fontColor('#666666')
            .padding({ left: 12, top: 8 })

          Row() {
            ForEach(rechargePlans.slice(0, 3), (p: RechargePlan, idx: number) => {
              Column({ space: 4 }) {
                Text(p.coins.toString())
                  .fontSize(16)
                  .fontColor(this.selectedPlan === idx ? '#FF6D00' : '#333333')
                  .fontWeight(FontWeight.Bold)
                Text('金币')
                  .fontSize(9)
                  .fontColor('#999999')
                if (p.bonus > 0) {
                  Text('+' + p.bonus + '赠币')
                    .fontSize(8)
                    .fontColor('#E53935')
                }
                Text('¥' + p.price)
                  .fontSize(12)
                  .fontColor('#6200EA')
                  .fontWeight(FontWeight.Bold)
              }
              .layoutWeight(1)
              .padding({ top: 10, bottom: 10 })
              .backgroundColor(this.selectedPlan === idx ? '#FFF3E0' : '#F5F5F5')
              .border(this.selectedPlan === idx ? { width: 2, color: '#FF6D00' } : { width: 0, color: '#FFFFFF' })
              .borderRadius(8)
              .margin({ left: 3, right: 3 })
              .alignItems(HorizontalAlign.Center)
              .onClick(() => {
                this.selectedPlan = idx
              })
            }, (p: RechargePlan) => p.id.toString())
          }
          .width('100%')
          .padding({ left: 4, right: 4, top: 4 })

          Row() {
            ForEach(rechargePlans.slice(3, 6), (p: RechargePlan, idx: number) => {
              Column({ space: 4 }) {
                Text(p.coins.toString())
                  .fontSize(16)
                  .fontColor(this.selectedPlan === idx + 3 ? '#FF6D00' : '#333333')
                  .fontWeight(FontWeight.Bold)
                Text('金币')
                  .fontSize(9)
                  .fontColor('#999999')
                if (p.bonus > 0) {
                  Text('+' + p.bonus + '赠币')
                    .fontSize(8)
                    .fontColor('#E53935')
                }
                Text('¥' + p.price)
                  .fontSize(12)
                  .fontColor('#6200EA')
                  .fontWeight(FontWeight.Bold)
              }
              .layoutWeight(1)
              .padding({ top: 10, bottom: 10 })
              .backgroundColor(this.selectedPlan === idx + 3 ? '#FFF3E0' : '#F5F5F5')
              .border(this.selectedPlan === idx + 3 ? { width: 2, color: '#FF6D00' } : { width: 0, color: '#FFFFFF' })
              .borderRadius(8)
              .margin({ left: 3, right: 3 })
              .alignItems(HorizontalAlign.Center)
              .onClick(() => {
                this.selectedPlan = idx + 3
              })
            }, (p: RechargePlan) => p.id.toString())
          }
          .width('100%')
          .padding({ left: 4, right: 4, top: 4 })

          Text('选择支付方式')
            .fontSize(12)
            .fontColor('#666666')
            .padding({ left: 12, top: 8 })

          Column({ space: 0 }) {
            ForEach(PAY_METHODS, (m: string, idx: number) => {
              Row({ space: 8 }) {
                Text(this.selectedPay === idx ? '🟢' : '⚪')
                  .fontSize(14)
                Text(m)
                  .fontSize(13)
                  .fontColor('#333333')
                  .layoutWeight(1)
                Text('>')
                  .fontSize(14)
                  .fontColor('#CCCCCC')
              }
              .width('100%')
              .padding({ left: 12, right: 12, top: 10, bottom: 10 })
              .onClick(() => {
                this.selectedPay = idx
              })
            }, (m: string) => m)
          }
          .width('100%')
          .backgroundColor('#FFFFFF')
          .borderRadius(8)
          .margin({ left: 12, right: 12, top: 4 })

          Row() {
            Text('确认充值 ¥' + rechargePlans[this.selectedPlan].price)
              .fontSize(15)
              .fontColor('#FFFFFF')
              .fontWeight(FontWeight.Bold)
          }
          .width('100%')
          .height(44)
          .backgroundColor('#FF6D00')
          .justifyContent(FlexAlign.Center)
          .onClick(() => {
            this.showRecharge = false
          })
        }
        .width('100%')
        .backgroundColor('#FFFFFF')
        .borderRadius({ topLeft: 16, topRight: 16 })
      }
      .width('100%')
      .constraintSize({ maxHeight: '80%' })
    }
    .width('100%')
    .height('100%')
  }

  build() {
    Stack({ alignContent: Alignment.Bottom }) {
      Scroll() {
        Column() {
          Column({ space: 4 }) {
            Text('💰')
              .fontSize(36)
            Text('2680')
              .fontSize(28)
              .fontColor('#FFFFFF')
              .fontWeight(FontWeight.Bold)
            Text('我的金币')
              .fontSize(11)
              .fontColor('rgba(255,255,255,0.8)')
          }
          .width('100%')
          .padding({ top: 24, bottom: 24 })
          .linearGradient({
            colors: [['#6200EA', 0.0], ['#FF6D00', 1.0]],
            angle: 180
          })
          .alignItems(HorizontalAlign.Center)

          Row() {
            Text('💎 金币充值')
              .fontSize(15)
              .fontColor('#333333')
              .fontWeight(FontWeight.Bold)
              .layoutWeight(1)
            Button() {
              Text('充值')
                .fontSize(12)
                .fontColor('#FFFFFF')
                .fontWeight(FontWeight.Bold)
            }
            .type(ButtonType.Capsule)
            .height(30)
            .backgroundColor('#FF6D00')
            .padding({ left: 16, right: 16 })
            .onClick(() => {
              this.showRecharge = true
            })
          }
          .width('100%')
          .justifyContent(FlexAlign.SpaceBetween)
          .padding({ left: 12, right: 12, top: 10, bottom: 6 })

          Row() {
            ForEach(rechargePlans.slice(0, 3), (p: RechargePlan) => {
              Column({ space: 4 }) {
                Text(p.coins.toString())
                  .fontSize(16)
                  .fontColor('#6200EA')
                  .fontWeight(FontWeight.Bold)
                Text('金币')
                  .fontSize(9)
                  .fontColor('#999999')
                if (p.bonus > 0) {
                  Text('+' + p.bonus)
                    .fontSize(9)
                    .fontColor('#E53935')
                }
                Text('¥' + p.price)
                  .fontSize(13)
                  .fontColor('#FF6D00')
                  .fontWeight(FontWeight.Bold)
                if (p.popular) {
                  Text('热门')
                    .fontSize(8)
                    .fontColor('#FFFFFF')
                    .backgroundColor('#FF6D00')
                    .borderRadius(3)
                    .padding({ left: 4, right: 4 })
                }
              }
              .layoutWeight(1)
              .padding({ top: 10, bottom: 10 })
              .backgroundColor('#FFFFFF')
              .borderRadius(8)
              .margin({ left: 3, right: 3 })
              .alignItems(HorizontalAlign.Center)
              .onClick(() => {
                this.showRecharge = true
              })
            }, (p: RechargePlan) => p.id.toString())
          }
          .width('100%')
          .padding({ left: 4, right: 4 })

          Text('👑 VIP特权卡')
            .fontSize(15)
            .fontColor('#333333')
            .fontWeight(FontWeight.Bold)
            .margin({ left: 12, top: 12, bottom: 6 })

          ForEach(vipPlans, (v: VipPlan) => {
            Column({ space: 8 }) {
              Row({ space: 10 }) {
                Column({ space: 4 }) {
                  Row({ space: 6 }) {
                    Text(v.name)
                      .fontSize(15)
                      .fontColor('#333333')
                      .fontWeight(FontWeight.Bold)
                    if (v.popular) {
                      Text('推荐')
                        .fontSize(8)
                        .fontColor('#FFFFFF')
                        .backgroundColor('#FF6D00')
                        .borderRadius(3)
                        .padding({ left: 4, right: 4 })
                    }
                  }
                  Text(v.duration)
                    .fontSize(11)
                    .fontColor('#999999')
                }
                .layoutWeight(1)
                .alignItems(HorizontalAlign.Start)
                Column({ space: 2 }) {
                  Text('¥' + v.price)
                    .fontSize(20)
                    .fontColor('#FF6D00')
                    .fontWeight(FontWeight.Bold)
                  Text('¥' + v.originalPrice)
                    .fontSize(10)
                    .fontColor('#999999')
                    .decoration({ type: TextDecorationType.LineThrough })
                }
                .alignItems(HorizontalAlign.End)
              }
              .width('100%')
              .alignItems(VerticalAlign.Top)

              Column({ space: 4 }) {
                ForEach(v.perks, (perk: string) => {
                  Row({ space: 6 }) {
                    Text('✓')
                      .fontSize(10)
                      .fontColor('#6200EA')
                      .fontWeight(FontWeight.Bold)
                    Text(perk)
                      .fontSize(10)
                      .fontColor('#666666')
                  }
                }, (perk: string) => perk)
              }
              .width('100%')
              .padding({ left: 4 })

              Button() {
                Text('开通VIP')
                  .fontSize(13)
                  .fontColor('#FFFFFF')
                  .fontWeight(FontWeight.Bold)
              }
              .type(ButtonType.Capsule)
              .height(32)
              .backgroundColor(v.popular ? '#FF6D00' : '#6200EA')
              .width('100%')
            }
            .width('100%')
            .padding(12)
            .backgroundColor('#FFFFFF')
            .borderRadius(12)
            .margin({ left: 12, right: 12, bottom: 8 })
          }, (v: VipPlan) => v.id.toString())

          Text('📜 充值记录')
            .fontSize(14)
            .fontColor('#333333')
            .fontWeight(FontWeight.Bold)
            .margin({ left: 12, top: 8, bottom: 6 })

          Column({ space: 0 }) {
            Row({ space: 8 }) {
              Text('💰')
                .fontSize(16)
              Text('充值300金币+30赠币')
                .fontSize(12)
                .fontColor('#333333')
                .layoutWeight(1)
              Text('¥30')
                .fontSize(12)
                .fontColor('#FF6D00')
                .fontWeight(FontWeight.Bold)
            }
            .padding({ left: 12, right: 12, top: 10, bottom: 10 })

            Row() {
              Column()
                .height(1)
                .width('100%')
                .backgroundColor('#F0F0F0')
            }
            .width('100%')
            .padding({ left: 12, right: 12 })

            Row({ space: 8 }) {
              Text('👑')
                .fontSize(16)
              Text('开通季度VIP')
                .fontSize(12)
                .fontColor('#333333')
                .layoutWeight(1)
              Text('¥68')
                .fontSize(12)
                .fontColor('#FF6D00')
                .fontWeight(FontWeight.Bold)
            }
            .padding({ left: 12, right: 12, top: 10, bottom: 10 })

            Row() {
              Column()
                .height(1)
                .width('100%')
                .backgroundColor('#F0F0F0')
            }
            .width('100%')
            .padding({ left: 12, right: 12 })

            Row({ space: 8 }) {
              Text('💰')
                .fontSize(16)
              Text('充值60金币')
                .fontSize(12)
                .fontColor('#333333')
                .layoutWeight(1)
              Text('¥6')
                .fontSize(12)
                .fontColor('#FF6D00')
                .fontWeight(FontWeight.Bold)
            }
            .padding({ left: 12, right: 12, top: 10, bottom: 10 })
          }
          .width('100%')
          .backgroundColor('#FFFFFF')
          .borderRadius(10)
          .margin({ left: 12, right: 12, bottom: 8 })

          Column()
            .height(80)
        }
      }
      .scrollBar(BarState.Off)
      .constraintSize({ maxHeight: '100%' })

      if (this.showRecharge) {
        this.rechargeBuilder()
      }
    }
    .width('100%')
    .height('100%')
  }
}

// ============ Tab5 社区 ============
@Component
struct CommunityTab65 {
  build() {
    Scroll() {
      Column() {
        Column() {
          Text('💬 剧评社区')
            .fontSize(16)
            .fontColor('#FFFFFF')
            .fontWeight(FontWeight.Bold)
          Text('分享追剧心得 · 讨论剧情')
            .fontSize(11)
            .fontColor('rgba(255,255,255,0.8)')
        }
        .width('100%')
        .padding({ top: 14, bottom: 14 })
        .linearGradient({
          colors: [['#9575CD', 0.0], ['#6200EA', 1.0]],
          angle: 135
        })
        .alignItems(HorizontalAlign.Center)

        Text('🔥 热门话题')
          .fontSize(14)
          .fontColor('#333333')
          .fontWeight(FontWeight.Bold)
          .margin({ left: 12, top: 8, bottom: 6 })

        Row() {
          ForEach(HOT_TOPICS, (t: HotTopic) => {
            Column({ space: 4 }) {
              Text(t.icon)
                .fontSize(22)
              Text(t.label)
                .fontSize(10)
                .fontColor(t.color)
                .fontWeight(FontWeight.Bold)
              Text(t.posts)
                .fontSize(8)
                .fontColor('#999999')
            }
            .layoutWeight(1)
            .padding({ top: 10, bottom: 10 })
            .backgroundColor(t.bg)
            .borderRadius(8)
            .margin({ left: 3, right: 3 })
            .alignItems(HorizontalAlign.Center)
          }, (t: HotTopic) => t.label)
        }
        .width('100%')
        .padding({ left: 4, right: 4 })

        Text('📝 热门剧评')
          .fontSize(14)
          .fontColor('#333333')
          .fontWeight(FontWeight.Bold)
          .margin({ left: 12, top: 10, bottom: 6 })

        ForEach(reviewPosts, (r: ReviewPost) => {
          Column({ space: 8 }) {
            Row({ space: 8 }) {
              Text(r.avatar)
                .fontSize(28)
                .width(40)
                .height(40)
                .backgroundColor('#EDE7F6')
                .borderRadius(20)
                .textAlign(TextAlign.Center)
              Column({ space: 3 }) {
                Text(r.name)
                  .fontSize(13)
                  .fontColor('#333333')
                  .fontWeight(FontWeight.Bold)
                Row({ space: 6 }) {
                  Text('评《' + r.dramaTitle + '》')
                    .fontSize(11)
                    .fontColor('#6200EA')
                  Text(r.rating >= 1 ? '★★★★★' : r.rating >= 2 ? '★★★★' : r.rating >= 3 ? '★★★' : r.rating >= 4 ? '★★' : '★')
                    .fontSize(10)
                    .fontColor('#FF6D00')
                }
                Text(r.date)
                  .fontSize(9)
                  .fontColor('#CCCCCC')
              }
              .layoutWeight(1)
              .alignItems(HorizontalAlign.Start)
            }
            .width('100%')
            .alignItems(VerticalAlign.Top)

            Text(r.content)
              .fontSize(12)
              .fontColor('#444444')
              .lineHeight(18)
              .width('100%')

            Row({ space: 16 }) {
              Row({ space: 4 }) {
                Text('👍')
                  .fontSize(14)
                Text(r.likes.toString())
                  .fontSize(11)
                  .fontColor('#999999')
              }
              Text('💬 评论')
                .fontSize(11)
                .fontColor('#999999')
              Text('📤 分享')
                .fontSize(11)
                .fontColor('#999999')
            }
            .width('100%')
            .justifyContent(FlexAlign.SpaceBetween)
          }
          .width('100%')
          .padding(12)
          .backgroundColor('#FFFFFF')
          .borderRadius(10)
          .margin({ left: 12, right: 12, bottom: 6 })
        }, (r: ReviewPost) => r.id.toString())

        Column()
          .height(80)
      }
    }
    .scrollBar(BarState.Off)
    .constraintSize({ maxHeight: '100%' })
  }
}

// ============ Tab6 我的 ============
@Component
struct ProfileTab65 {
  @State showEditName: boolean = false
  @State nameVal: string = '追剧狂人'
  @State showDelete: boolean = false
  @State delId: number = 0
  @State recordList: WatchRecord[] = watchRecords

  @Builder
  editNameBuilder() {
    Stack({ alignContent: Alignment.Bottom }) {
      Column() {
        Column({ space: 10 }) {
          Row({ space: 8 }) {
            Text('✏️')
              .fontSize(20)
            Text('编辑昵称')
              .fontSize(15)
              .fontColor('#333333')
              .fontWeight(FontWeight.Bold)
              .layoutWeight(1)
            Text('✕')
              .fontSize(18)
              .fontColor('#999999')
              .onClick(() => {
                this.showEditName = false
              })
          }
          .width('100%')
          .padding(12)

          TextInput({ placeholder: '请输入昵称', text: this.nameVal })
            .width('100%')
            .height(40)
            .fontSize(13)
            .borderRadius(8)
            .border({ width: 1, color: '#E0E0E0' })
            .margin({ left: 12, right: 12 })
            .onChange((val: string) => {
              this.nameVal = val
            })

          Row({ space: 12 }) {
            Button() {
              Text('取消')
                .fontSize(13)
                .fontColor('#666666')
            }
            .type(ButtonType.Capsule)
            .height(38)
            .backgroundColor('#F5F5F5')
            .layoutWeight(1)
            .onClick(() => {
              this.showEditName = false
            })
            Button() {
              Text('保存')
                .fontSize(13)
                .fontColor('#FFFFFF')
                .fontWeight(FontWeight.Bold)
            }
            .type(ButtonType.Capsule)
            .height(38)
            .backgroundColor('#6200EA')
            .layoutWeight(1)
            .onClick(() => {
              this.showEditName = false
            })
          }
          .width('100%')
          .padding(12)
        }
        .width('100%')
        .backgroundColor('#FFFFFF')
        .borderRadius({ topLeft: 16, topRight: 16 })
      }
      .width('100%')
      .constraintSize({ maxHeight: '50%' })
    }
    .width('100%')
    .height('100%')
  }

  @Builder
  deleteRecordBuilder() {
    Stack({ alignContent: Alignment.Center }) {
      Column({ space: 12 }) {
        Text('🗑️')
          .fontSize(40)
        Text('删除观看记录?')
          .fontSize(15)
          .fontColor('#333333')
          .fontWeight(FontWeight.Bold)
        Text('删除后无法恢复')
          .fontSize(12)
          .fontColor('#999999')
        Row({ space: 12 }) {
          Button() {
            Text('取消')
              .fontSize(13)
              .fontColor('#666666')
          }
          .type(ButtonType.Capsule)
          .height(38)
          .backgroundColor('#F5F5F5')
          .layoutWeight(1)
          .onClick(() => {
            this.showDelete = false
          })
          Button() {
            Text('删除')
              .fontSize(13)
              .fontColor('#FFFFFF')
              .fontWeight(FontWeight.Bold)
          }
          .type(ButtonType.Capsule)
          .height(38)
          .backgroundColor('#F44336')
          .layoutWeight(1)
          .onClick(() => {
            this.recordList = this.recordList.filter((r: WatchRecord) => r.id !== this.delId)
            this.showDelete = false
          })
        }
        .width('100%')
      }
      .width('80%')
      .padding(20)
      .backgroundColor('#FFFFFF')
      .borderRadius(16)
      .alignItems(HorizontalAlign.Center)
    }
    .width('100%')
    .height('100%')
  }

  build() {
    Stack({ alignContent: Alignment.Bottom }) {
      Scroll() {
        Column() {
          Column({ space: 6 }) {
            Text('🎭')
              .fontSize(50)
              .width(70)
              .height(70)
              .backgroundColor('rgba(255,255,255,0.3)')
              .borderRadius(35)
              .textAlign(TextAlign.Center)
            Text(this.nameVal)
              .fontSize(16)
              .fontColor('#FFFFFF')
              .fontWeight(FontWeight.Bold)
            Row({ space: 8 }) {
              Text('👑 年度VIP')
                .fontSize(10)
                .fontColor('#FFFFFF')
                .backgroundColor('rgba(255,255,255,0.25)')
                .borderRadius(10)
                .padding({ left: 8, right: 8, top: 2, bottom: 2 })
              Text('💰 2680金币')
                .fontSize(10)
                .fontColor('rgba(255,255,255,0.9)')
            }
          }
          .width('100%')
          .padding({ top: 30, bottom: 20 })
          .linearGradient({
            colors: [['#6200EA', 0.0], ['#FF6D00', 1.0]],
            angle: 180
          })
          .alignItems(HorizontalAlign.Center)

          Row() {
            Column({ space: 2 }) {
              Text('86')
                .fontSize(16)
                .fontColor('#6200EA')
                .fontWeight(FontWeight.Bold)
              Text('追剧部数')
                .fontSize(10)
                .fontColor('#999999')
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Center)
            Column({ space: 2 }) {
              Text('168h')
                .fontSize(16)
                .fontColor('#FF6D00')
                .fontWeight(FontWeight.Bold)
              Text('观看时长')
                .fontSize(10)
                .fontColor('#999999')
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Center)
            Column({ space: 2 }) {
              Text('32')
                .fontSize(16)
                .fontColor('#9575CD')
                .fontWeight(FontWeight.Bold)
              Text('发表剧评')
                .fontSize(10)
                .fontColor('#999999')
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Center)
          }
          .width('100%')
          .padding({ top: 12, bottom: 12 })
          .backgroundColor('#FFFFFF')
          .margin({ left: 12, right: 12, top: -12 })
          .borderRadius(10)
          .shadow({ radius: 4, color: 'rgba(0,0,0,0.08)', offsetY: 2 })

          Row({ space: 10 }) {
            Text('✏️')
              .fontSize(16)
            Text('编辑昵称')
              .fontSize(13)
              .fontColor('#6200EA')
              .fontWeight(FontWeight.Bold)
              .layoutWeight(1)
            Text('>')
              .fontSize(16)
              .fontColor('#CCCCCC')
          }
          .width('100%')
          .padding(10)
          .backgroundColor('#EDE7F6')
          .borderRadius(10)
          .margin({ left: 12, right: 12, top: 8 })
          .alignItems(VerticalAlign.Center)
          .onClick(() => {
            this.showEditName = true
          })

          Text('📊 本周观看时长')
            .fontSize(14)
            .fontColor('#333333')
            .fontWeight(FontWeight.Bold)
            .margin({ left: 12, top: 14, bottom: 6 })

          Column() {
            Row({ space: 4 }) {
              ForEach(WATCH_BARS_65, (b: BarData65) => {
                Column({ space: 4 }) {
                  Text(b.value + 'm')
                    .fontSize(7)
                    .fontColor('#999999')
                  Column()
                    .layoutWeight(1)
                    .width(16)
                    .backgroundColor(b.color)
                    .borderRadius(3)
                  Text(b.label)
                    .fontSize(8)
                    .fontColor('#999999')
                }
                .height(100)
                .layoutWeight(1)
                .justifyContent(FlexAlign.End)
                .alignItems(HorizontalAlign.Center)
              }, (b: BarData65) => b.label)
            }
            .width('100%')
            .height(110)
          }
          .width('100%')
          .padding(12)
          .backgroundColor('#FFFFFF')
          .borderRadius(10)
          .margin({ left: 12, right: 12, bottom: 8 })

          Text('⏱️ 观看记录')
            .fontSize(14)
            .fontColor('#333333')
            .fontWeight(FontWeight.Bold)
            .margin({ left: 12, top: 8, bottom: 6 })

          ForEach(this.recordList, (r: WatchRecord) => {
            Row({ space: 10 }) {
              Text(r.icon)
                .fontSize(28)
                .width(44)
                .height(44)
                .backgroundColor('#EDE7F6')
                .borderRadius(8)
                .textAlign(TextAlign.Center)
              Column({ space: 3 }) {
                Text(r.title)
                  .fontSize(12)
                  .fontColor('#333333')
                  .fontWeight(FontWeight.Bold)
                  .maxLines(1)
                  .textOverflow({ overflow: TextOverflow.Ellipsis })
                Row({ space: 6 }) {
                  Text('第' + r.episode + '集')
                    .fontSize(10)
                    .fontColor('#999999')
                  Text(r.watchTime)
                    .fontSize(10)
                    .fontColor('#999999')
                }
                Row() {
                  Column() {
                    Row() {
                      Column()
                        .width(r.progress + '%')
                        .height(3)
                        .backgroundColor('#6200EA')
                        .borderRadius(2)
                    }
                    .width('100%')
                    .height(3)
                    .backgroundColor('#F0F0F0')
                    .borderRadius(2)
                  }
                  .width(100)
                }
              }
              .layoutWeight(1)
              .alignItems(HorizontalAlign.Start)
              Text('✕')
                .fontSize(14)
                .fontColor('#999999')
                .onClick(() => {
                  this.delId = r.id
                  this.showDelete = true
                })
            }
            .width('100%')
            .padding(8)
            .backgroundColor('#FFFFFF')
            .borderRadius(8)
            .margin({ left: 12, right: 12, bottom: 4 })
            .alignItems(VerticalAlign.Center)
          }, (r: WatchRecord) => r.id.toString())

          Column()
            .height(80)
        }
      }
      .scrollBar(BarState.Off)
      .constraintSize({ maxHeight: '100%' })

      if (this.showEditName) {
        this.editNameBuilder()
      }
      if (this.showDelete) {
        this.deleteRecordBuilder()
      }
    }
    .width('100%')
    .height('100%')
  }
}

// ============ 主入口 ============
@Entry
@Component
struct DuoDuoDuanJuApp {
  @State activeTab: number = 0

  @Builder
  bottomTabItem(icon: string, label: string, tabIdx: number) {
    Column({ space: 3 }) {
      Text(icon)
        .fontSize(20)
        .opacity(this.activeTab === tabIdx ? 1.0 : 0.45)
      Text(label)
        .fontSize(9)
        .fontColor(this.activeTab === tabIdx ? '#6200EA' : '#999999')
        .fontWeight(this.activeTab === tabIdx ? FontWeight.Bold : FontWeight.Normal)
    }
    .layoutWeight(1)
    .alignItems(HorizontalAlign.Center)
    .padding({ top: 5, bottom: 5 })
    .onClick(() => {
      this.activeTab = tabIdx
    })
  }

  build() {
    Column() {
      Stack() {
        if (this.activeTab === 0) {
          HotPlayTab()
        } else if (this.activeTab === 1) {
          RankTab()
        } else if (this.activeTab === 2) {
          FollowTab()
        } else if (this.activeTab === 3) {
          RechargeTab()
        } else if (this.activeTab === 4) {
          CommunityTab65()
        } else {
          ProfileTab65()
        }
      }
      .layoutWeight(1)

      Row() {
        this.bottomTabItem('🎬', '热播', 0)
        this.bottomTabItem('🏆', '排行', 1)
        this.bottomTabItem('📺', '追剧', 2)
        this.bottomTabItem('💰', '充值', 3)
        this.bottomTabItem('💬', '社区', 4)
        this.bottomTabItem('👤', '我的', 5)
      }
      .width('100%')
      .backgroundColor('#FFFFFF')
      .padding({ top: 4, bottom: 6 })
      .shadow({ radius: 8, color: 'rgba(98,0,234,0.12)', offsetY: -2 })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5FF')
  }
}

65.ets 放入 HarmonyOS 项目的 entry/src/main/ets/pages/ 目录下,在 main_pages.json 中注册路由,即可在 DevEco Studio 中预览运行。

十五、开发环境搭建:DevEco Studio + API 24

本应用基于 HarmonyOS NEXT(API 24)开发,使用 ArkTS 声明式 UI 框架。以下是完整的开发环境搭建步骤。

15.1 安装 DevEco Studio

从华为开发者联盟官网下载 DevEco Studio 安装包,双击运行安装程序。安装欢迎界面如下,点击「Next」继续。

DevEco Studio安装欢迎界面

15.2 选择安装位置

在安装路径选择界面,建议选择空间充足的磁盘(建议剩余空间 20GB 以上),避免使用中文路径或含空格的路径。点击「Next」继续。

选择DevEco Studio安装路径

15.3 配置安装选项

在安装选项配置界面,勾选「Create Desktop Shortcut」(创建桌面快捷方式)、「Add to PATH」(添加到环境变量)等选项,方便后续命令行调用。点击「Next」继续。

DevEco Studio安装选项配置

15.4 新建项目:选择 Empty Ability 模板

安装完成后启动 DevEco Studio,点击「Create Project」新建项目。在模板选择界面,选择「Empty Ability」(空 Ability)模板,这是最基础的 ArkTS 应用模板,适合从零构建自定义 UI。点击「Next」继续。

选择Empty Ability模板

15.5 配置项目信息:API 24

在项目配置界面,填写项目名称(如 DuoDuoDuanJu)、包名(如 com.example.duoduoduanju),Compatible SDK 选择 6.1.1(24)(即 API 24),Save location 选择项目保存路径。确认信息无误后点击「Finish」创建项目。

配置项目信息与API 24

15.6 导入源码与运行

项目创建完成后,将 65.ets 文件复制到 entry/src/main/ets/pages/ 目录下,打开 entry/src/main/resources/base/profile/main_pages.json,在 src 数组中添加 "pages/65" 路由注册。

在 DevEco Studio 顶部工具栏选择 Previewer(预览器)或连接真机/模拟器,点击运行按钮即可看到「多多短剧剧场」应用的完整运行效果。可以切换底部 6 Tab(热播/排行/追剧/充值/社区/我的),在热播 Tab 按类型筛选短剧、在排行 Tab 查看领奖台 TOP3 与排名趋势、在追剧 Tab 查看更新提醒与观看进度、在充值 Tab 选择金币套餐与 VIP 卡、在社区 Tab 浏览热门话题与剧评、在我的 Tab 查看本周观看时长柱状图,体验完整的短剧电商社区闭环。

十六、总结

「多多短剧剧场」是一个完整的短剧电商社区应用,基于 HarmonyOS NEXT(API 24)ArkTS 声明式 UI 框架构建,单文件 65.ets 约 1400 行代码实现了全部功能。

应用的核心亮点包括:

  1. 六组件多 Tab 短剧电商架构:与多多亲子母婴、多多鲜花绿植相同的多组件模式,六个独立的 @Component 子组件(HotPlayTab 热播/RankTab 排行/FollowTab 追剧/RechargeTab 充值/CommunityTab65 社区/ProfileTab65 我的)+ 一个 @Entry 主入口 DuoDuoDuanJuApp。主入口通过 @State activeTab + if/else if 条件渲染分发显示对应子组件,每个子组件独立管理自己的 @State 与弹窗,互不干扰。@Builder bottomTabItem(icon, label, tabIdx) 可复用构建底部 6 Tab 项,激活态短剧紫 #6200EA 字粗体+图标 opacity 1.0,非激活灰字+图标 opacity 0.45,底部导航栏纯白底+紫色系阴影 shadow({ radius: 8, color: 'rgba(98,0,234,0.12)', offsetY: -2 })。这种多组件架构让每个 Tab 的代码独立、职责清晰、可复用性强,是中大型 ArkTS 应用的标准架构模式。
  2. 短剧紫 + 潮流橙 + 潮流短剧风视觉体系:短剧紫 #6200EA 主色 + 潮流橙 #FF6D00 强调色 + 底色 #F5F5FF 潮流短剧风,价格红 #E53935 赠币提示、浅紫 #9575CD 追剧/社区渐变、浅紫底 #EDE7F6 短剧图标区、浅橙底 #FFF3E0 冠军/更新提醒/充值选中态。六个 Tab 各自拥有独立的渐变头——热播紫→橙 angle:135、排行橙→紫 angle:135、追剧紫→浅紫 angle:180、充值紫→橙 angle:180、社区浅紫→紫 angle:135、我的紫→橙 angle:180——每个 Tab 通过渐变头的色彩组合快速区分模块,同时保持短剧紫潮流橙主色调的统一。充值 Tab 还有金币余额渐变头(💰图标+2680金币大字+我的金币,紫到橙渐变 angle:180),是短剧充值消费的标准视觉元素。
  3. 热门话题独立色 + 紫色系渐变柱状图 + 领奖台三色 + 排名数字色四色独立色体系HOT_TOPICS 4 个热门话题各自独立 color/bg(本周热剧潮流橙/复仇爽文短剧紫/甜宠专区粉/玄幻修仙青),社区 Tab 中使用;WATCH_BARS_65 7 天观看时长数据各自独立紫色系渐变色(周一 #D1C4E9 最浅→周日 #5500CC 最深),本周观看时长柱状图从浅到深形成「周末追剧时长飙升」可视化,周六 350m 最高;领奖台 TOP3 中冠军浅橙底、亚军浅蓝紫底、季军浅青底,用颜色区分领奖台名次;排行列表前 3 名数字使用潮流橙、其他使用灰,用颜色区分排名档次。四色独立色体系是本应用最具视觉特色的设计——用颜色直观区分话题类型、观看时长趋势、领奖台名次和排名档次,帮助用户快速理解信息。
  4. 6 大 Tab 完全差异化布局:热播=类型 chips 横滑(6类型全部/甜宠/复仇/玄幻/古装/热血)+竖屏大卡(首部剧 width100 height140 竖屏比例模拟短剧竖屏播放)+双列短剧卡(7部含 NEW 标签);排行=4 榜单分段器(热播榜/飙升榜/新剧榜/口碑榜)+领奖台 TOP3(中间冠军 padding top10 高两侧亚军季军 padding top30 低,模拟真实领奖台视觉,冠军浅橙底+🥇金牌)+排名列表(8部含 trend 趋势箭头↑→↓+TOP3 橙色数字);追剧=更新提醒(filter updated=true 的剧,🔴红点+浅橙底)+追剧列表(5部含进度条 currentEp/totalEp 动态百分比+N%已观看+✕取消追剧);充值=金币余额渐变头(2680金币大字)+3 套餐卡(含热门标签+赠币提示)+VIP 特权卡(3款含 perks 权益数组,✓紫色勾+权益文字,年度VIP 6项权益最多)+充值记录;社区=热门话题4个(独立 color/bg)+剧评动态(5条含评分星级三元表达式 r.rating>=1?‘★★★★★’:…+点赞评论分享);我的=渐变头+三列统计悬浮卡(追剧部数86/观看时长168h/发表剧评32,margin top:-12 悬浮重叠)+编辑昵称(TextInput 绑定 nameVal)+本周观看时长柱状图(7天紫色系渐变从浅到深,周六350m最高)+观看记录(6条含进度条+✕删除)。6 大 Tab 布局完全不同,是短剧电商社区差异化设计的典范。
  5. 本周观看时长柱状图(紫色系渐变色,纯声明式实现):我的 Tab 内嵌本周观看时长柱状图(7 天 WATCH_BARS_65,紫色系渐变色从浅 #D1C4E9 到深 #5500CC,周六 350m 最高柱子最深 #6200EA,周三 90m 最低柱子最浅 #9575CD),通过纯声明式实现——Row 布局 + ForEach 渲染 7 根柱子 + 每根柱子 Column layoutWeight(1) width 16 backgroundColor(b.color) borderRadius(3) justifyContent(FlexAlign.End) 实现从底部向上生长 + 顶部时长显示 + 底部星期标签,无需 Canvas,纯 Column + 背景色 + layoutWeight 实现柱状图,是 ArkTS 声明式 UI 数据可视化的标准模式。紫色系渐变色从浅到深的配色方案,既统一了短剧紫主色调,又通过深浅变化直观表达观看时长的增长趋势(周末追剧时长明显高于工作日)。
  6. 领奖台 TOP3(中间冠军高两侧亚军季军低,模拟真实领奖台视觉):排行 Tab 的领奖台 TOP3 是本应用最具特色的布局之一——通过中间冠军 padding top 10(高)、两侧亚军季军 padding top 30(低)的高度差,模拟真实领奖台的视觉效果(冠军站在最高处,亚军季军站在两侧较低处),配合🥇🥈🥉奖牌图标和冠军浅橙底(潮流橙系最高荣耀)、亚军浅蓝紫底、季军浅青底,让排行 TOP3 更加直观生动。领奖台三列等宽 layoutWeight 1 居中对齐,纯白底圆角 10,是排行应用 TOP3 展示的创新布局。
  7. 进度条(currentEp/totalEp 动态百分比,纯声明式实现)+ 更新提醒 + NEW 标签:追剧 Tab 追剧列表和我的 Tab 观看记录均使用进度条——通过 (currentEp / totalEp * 100)progress 动态计算观看进度百分比,渲染外层浅灰底 Column + 内层短剧紫填充 Row(width 动态百分比),纯声明式实现无需 Canvas,配合「N% 已观看」文字让用户直观看到观看进度。追剧 Tab 还有更新提醒(通过 filter(f => f.updated) 筛选有更新的追剧,🔴红点+浅橙底单独列出)和热播 Tab 的NEW 标签if (d.isNew) 条件渲染潮流橙底白字 NEW 标签),帮助用户及时发现新内容与更新。进度条+更新提醒+NEW 标签是短剧应用区别于普通电商的特色交互——短剧是持续更新的内容,用户需要及时了解更新与观看进度。
  8. 4 种模态弹窗覆盖底部抽屉+居中两种样式(含 6 套餐分两行+3 支付方式+TextInput):充值金币底部抽屉(6 套餐分两行 3+3 渲染,选中态浅橙底+潮流橙边框;3 种支付方式微信/支付宝/多多钱包,🟢/⚪ 圆点单选;确认充值动态价格 ¥N)、取消追剧居中弹窗(💔图标+可移除 filter+红色确认按钮)、编辑昵称底部抽屉(✏️图标+TextInput 绑定 nameVal+保存后渐变头用户名响应式更新)、删除观看记录居中弹窗(🗑️图标+删除后无法恢复提示+可移除 filter)。底部抽屉用于需要输入或选择的复杂交互(6 套餐选择/3 支付方式选择/昵称 TextInput 输入),居中用于轻量确认交互(取消追剧/删除观看记录),是短剧应用弹窗设计的标准模式。6 套餐分两行渲染是充值弹窗的特色布局——6 款套餐较多,分两行(每行 3 款)展示更加紧凑美观。
  9. 强类型数据模型 + 特色字段 + 条件渲染 + 三元表达式:9 个 interface 约束所有数据结构,DramaItem 含 isNew 新剧标记,RankItem 含 trend 趋势箭头,FollowDrama 含 updated 更新标记+currentEp/totalEp 进度,RechargePlan 含 bonus 赠币+popular,VipPlan 含 perks 权益数组,ReviewPost 含 rating 评分,WatchRecord 含 progress 进度百分比,HotTopic 含独立 color/bg。条件渲染 if (d.isNew)(NEW 标签)、if (f.updated)(更新标签)、if (p.bonus > 0)(赠币提示)、if (p.popular)(热门标签)、if (v.popular)(推荐标签+按钮颜色切换)实现差异化交互。三元表达式 r.rating >= 1 ? '★★★★★' : ... 根据评分动态渲染星级文字,是 ArkTS 根据数值动态渲染文本的标准模式。
  10. 完整短剧电商社区闭环 + 短剧应用特色功能:从热播浏览(类型筛选+竖屏大卡)、排行发现(领奖台 TOP3+趋势箭头)、追剧管理(更新提醒+进度条)、充值消费(金币套餐+VIP 特权)、社区互动(热门话题+剧评评分)到个人中心(活跃统计+观看时长可视化),覆盖短剧电商社区的完整业务闭环。8 部短剧+8 部排行+5 部追剧+6 款充值套餐+3 款 VIP 卡+5 条剧评+6 条观看记录的丰富预置数据,让应用开箱即可体验完整的短剧电商社区流程。竖屏大卡模拟短剧竖屏播放、领奖台 TOP3 模拟真实领奖台、进度条展示观看进度、更新提醒及时通知新内容、NEW 标签发现新剧、金币充值+VIP 特权满足付费消费、热门话题+剧评评分满足社区互动、本周观看时长柱状图可视化追剧习惯,这些短剧应用特色功能让应用不仅仅是一个电商 App,更是一个有温度的短剧社区。

多多短剧剧场整体运行效果

对于希望学习 HarmonyOS ArkTS 声明式 UI 开发的读者,本应用提供了一个完整的多组件短剧电商社区架构参考范本:如何通过多个独立 @Component 子组件拆分复杂应用、如何通过主入口 @State activeTab + if/else if 分发显示子组件、如何用 @Builder 复用底部 Tab 项构建、如何用条件渲染 if() 实现差异化交互(NEW 标签/更新标签/赠币提示/热门标签)、如何用三元表达式根据数值动态渲染文本(评分星级)、如何用纯声明式(外层浅灰底+内层填充 width 动态百分比)实现进度条、如何用纯声明式(Column+背景色+layoutWeight)实现柱状图数据可视化、如何用底部抽屉(Stack alignContent:Bottom+maxHeight)和居中弹窗(Stack alignContent:Center)两种弹窗样式、如何用 margin top:-12 实现悬浮卡片重叠效果、如何用热门话题独立色/紫色系渐变柱状图/领奖台三色/排名数字色四色独立色体系丰富视觉层次、如何用六个 Tab 各自独立渐变头快速区分模块同时保持主色调统一、如何用领奖台 TOP3(中间高两侧低)模拟真实领奖台视觉、如何用 6 套餐分两行+3 支付方式实现充值弹窗、如何用 TextInput 绑定 @State 实现编辑昵称响应式更新。将源码导入 DevEco Studio(API 24)即可直接运行预览,在此基础上可以扩展更多短剧数据、接入真实短剧播放 API 与付费系统、增加短剧竖屏播放器、增加弹幕互动、增加追剧更新推送通知、增加金币任务系统、增加 VIP 专属短剧、增加剧评点赞评论回复、增加观看时长排行榜、增加短剧推荐算法,打造一个真正可用的短剧电商社区平台。

追剧上瘾,一集上头。愿每一部短剧都能让你欲罢不能,愿每一次更新都能准时提醒,愿每一条进度条都能记录你的追剧时光,愿每一根柱状图都能见证你的周末狂欢,也愿你在代码与短剧紫潮流橙之间,找到属于自己的那份沉浸与上头。🎬🏆——多多短剧剧场,追剧上瘾,一集上头。

Logo

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

更多推荐