一款集波形剪辑、特效处理、铃声交易、素材商城、创作者榜单于一体的鸿蒙原生铃声工坊应用。本文从设计理念、色彩系统、架构设计、数据模型、六大 Tab 页面、18 种弹窗交互、动画效果到开发环境,对 2128.ets 进行全方位深度拆解。


引言:把一段声音剪成专属来电,在波普橙的工坊里完成

在手机铃声逐渐被默认振动取代的今天,「铃声工坊 RINGSMITH」试图重新定义铃声的创作与消费体验。它不是一个简单的铃声下载器,而是一个完整的铃声 DIY 生态:用户可以在波形编辑器中精确截取 16 秒片段,应用淡入淡出、回声、混响、变速、升调、降噪、反转等 8 种音频特效,导出为 MP3 / M4R / WAV 三种格式,然后一键上架到铃声市场赚取收益。

铃声工坊首页:深色波形编辑区 + 44根动态波形条 + 时间标尺 + 裁剪手柄 + 传输控制条 + 8种特效芯片 + 6层轨道列表 + 试听/导出双按钮

整个应用采用波普活力橙视觉风格:暖奶油底色 #FFF8E7 搭配活力橙 #F97316 作为主色,辅以天蓝 #0EA5E9、靛紫 #7C3AED、粉玫 #EC4899、柠黄 #FDE68A 构成多彩的波普调色盘。波形编辑区采用深灰 #111827 背景,与暖色调形成强烈对比,让波形条成为视觉焦点。

应用包含 6 个底部 Tab:🛠️ 工坊(波形剪辑编辑器)、🛍️ 市场(铃声电商货架)、🎵 我的铃声(作品管理与上架)、📦 素材库(16 个素材包双列宫格)、🏆 榜单(颁奖台 + 创作者排行)、👤 我的(个人中心与收益)。此外还设计了 18 种风格各异的弹窗,涵盖底部抽屉、居中卡片、危险确认、表单输入、宫格分享、说明列表等多种形态。

本文将带你深入铃声工坊的每一个设计决策与代码实现,理解一个铃声 DIY 工坊应用是如何在鸿蒙 ArkTS 中从概念走向完整产品的。


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

1.1 项目定位

铃声工坊 RINGSMITH 定位为一站式铃声创作与交易平台,核心价值主张是「剪一截热爱,接一通惊喜」。它同时服务两类用户:

  • 创作者:在工坊中剪辑铃声、应用特效、导出上架,在市场中获得下载收益与打赏收入,通过榜单获得曝光与流量扶持。
  • 消费者:在市场中浏览 24 款风格各异的铃声,按 9 个分类筛选,试听后购买设为铃声,或购买素材包用于二次创作。

这种 UGC(用户生成内容)+ PGC(专业生成内容)混合的内容生态,让应用既有持续的内容供给,又有明确的变现路径。

1.2 设计理念

铃声工坊的设计围绕三个核心理念展开:

第一,剪辑即创作。 工坊 Tab 是整个应用的灵魂。44 根波形条实时可视化音频振幅,时间标尺精确到秒,裁剪手柄支持拖拽选取片段,8 种特效芯片一键应用,6 层轨道列表展示当前工程的所有音频层。用户不需要专业的音频编辑知识,就能在 30 秒内完成一段铃声的剪辑与导出。

第二,波普即活力。 波普艺术风格的核心是大胆的色彩、扁平的图形、活泼的节奏。铃声工坊将这一风格贯彻到每一个角落:分类标签用不同的马卡龙色背景,铃声卡片用 emoji + 彩色圆角方块作为封面,柱状图用渐变橙色,颁奖台用金银铜三色。整个界面充满节奏感与游戏化趣味,让铃声创作本身变成一件快乐的事。

第三,交易即激励。 应用的每一个环节都在鼓励创作者产出内容:我的铃声 Tab 展示累计下载量与收益柱状图,上架市场弹窗清晰展示 70% 创作者分成,榜单 Tab 用颁奖台仪式感突出 TOP3 创作者,个人中心展示等级进度条与创作者认证条件。这种游戏化的激励体系,让创作者有持续的动力上传更多铃声。

1.3 核心功能矩阵

功能模块 核心能力 对应 Tab
波形剪辑 44 根波形可视化、时间标尺、裁剪手柄、播放指针 工坊
音频特效 淡入淡出、回声、混响、变速、升调、降噪、反转(8 种) 工坊
多轨编辑 6 层轨道(音频/环境/特效),独立颜色标识 工坊
导出分享 MP3/M4R/WAV 三格式,标准/高清两档音质 工坊
铃声市场 24 款铃声、9 分类筛选、试听、购买、打赏、举报、分享 市场
作品管理 14 个作品、已上架/草稿状态、编辑/删除/上架操作 我的铃声
素材商城 16 个素材包、热门/最新/免费分段、曲目试听、购买 素材库
创作者榜单 TOP3 颁奖台、4-12 名完整榜单、分类占比、分成规则 榜单
个人中心 等级进度、收益柱状图、创作者认证、功能菜单 我的

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

2.1 主色调:波普活力橙

铃声工坊的主色是 #F97316(活力橙),这是一种介于橙色与红色之间的高饱和度暖色。它在应用中承担以下角色:

  • 主按钮背景色:导出铃声、开始导出、确认上架、确认支付等核心操作按钮全部使用 #F97316 背景 + 白色文字,确保用户一眼就能找到主要行动点。
  • 播放按钮色:工坊 Tab 的中央播放按钮使用 #F97316 圆形背景,配合 12px 橙色阴影 rgba(249,115,22,0.45),在深色波形区上方形成强烈的视觉焦点。
  • 波形播放态色:当音频处于播放状态时,44 根波形条从 #FDBA74(浅橙)变为 #F97316(活力橙),并伴随 1.15 倍 Y 轴缩放动画,让"正在播放"的状态一目了然。
  • 分类选中态:市场 Tab 的分类横滑条中,选中的分类使用 #F97316 背景 + 白色文字,未选中的使用 #FFE8CC(浅橙)背景 + 深橙文字。
  • 价格标签色:所有非免费铃声的价格标签使用 #F97316 文字,免费铃声使用 #16A34A(绿色),形成清晰的价格视觉区分。

2.2 辅助色:波普多彩调色盘

除了主色橙色,铃声工坊还使用了一组波普风格的辅助色,每个颜色对应特定的功能或情感:

色彩 色值 用途
天蓝 #0EA5E9 个人中心头部背景、播放态切换色、收益柱状图高亮、打赏入口
靛紫 #7C3AED 榜单头部背景、今日精选横幅、分享入口、分类占比纯音乐
粉玫 #EC4899 播放指针线、收藏心形、分类占比搞笑、统计卡收藏人气
柠黄 #FDE68A 颁奖台冠军台、等级进度条、今日精选标签、浅黄背景
浅橙 #FDBA74 波形默认态、裁剪范围文字、浅橙辅助色
奶白 #FFF8E7 应用全局背景色、弹窗背景、卡片背景
深灰 #111827 波形编辑区背景、工程栏背景 #1F2937

这组色彩的搭配遵循波普艺术的核心原则:高饱和度、强对比、多色并置。每个铃声卡片的封面使用不同的马卡龙色(#FBCFE8 粉、#BBF7D0 绿、#FED7AA 橙、#FDE68A 黄、#BFDBFE 蓝、#E9D5FF 紫),让 24 款铃声在列表中形成彩虹般的视觉节奏。

2.3 背景层次:暖奶油 + 深灰波形区

铃声工坊的背景系统采用暖奶油底色 + 白色卡片 + 深灰波形区的三层结构:

  • 全局背景 #FFF8E7:一种略带黄色调的暖白色,比纯白更温暖,比米色更干净,为整个应用奠定温馨活泼的基调。
  • 卡片背景 #FFFFFF:所有内容卡片(统计卡、柱状图卡、作品卡、素材包卡、菜单项)使用纯白背景,在暖奶油底色上形成清晰的层次。
  • 波形编辑区 #111827:工坊 Tab 的波形编辑区使用近黑色深灰背景,与全局暖色调形成强烈对比,让橙色波形条成为绝对的视觉焦点。工程栏使用稍浅的 #1F2937
  • 弹窗背景 #FFFFFF#FFF8E7:大部分弹窗使用白色背景,部分表单类弹窗使用暖奶油色,与应用整体风格保持一致。

2.4 字体与排版

铃声工坊的字体系统遵循鸿蒙设计规范,使用系统默认字体,通过字号、字重、颜色构建信息层次:

  • 大标题 18-19px + FontWeight.Bold + #1F2937 深灰:用于各 Tab 标题、弹窗主标题。
  • 中标题 13-14px + FontWeight.Bold + #1F2937:用于卡片标题、小节标题、铃声名称。
  • 正文 12px + FontWeight.Medium + #1F2937:用于铃声创作者、分类标签、菜单标题。
  • 辅助文字 9-11px + #9CA3AF 中灰:用于时长、下载量、日期、提示文字。
  • 强调文字 12-16px + FontWeight.Bold + #F97316#0EA5E9:用于价格、收益、统计数值。

排版上大量使用圆角(12-22px)和卡片化布局,每个功能模块都被包裹在一个圆角白色卡片中,卡片之间通过 10-16px 的间距分隔,形成清晰的视觉节奏。


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

3.1 单文件架构

铃声工坊采用单文件架构,所有代码集中在 2128.ets 一个文件中。这种架构的优势是结构紧凑、便于分享与演示,适合中小型应用或原型验证。文件内部按照功能模块清晰分段,通过注释分隔符 // ===================== 标记每个模块的边界。

文件的整体组织结构如下:

2128.ets
├── 数据模型(9 个 interface)
│   ├── RingtoneItem      铃声商品
│   ├── CreationItem      我的作品
│   ├── PackItem          素材包
│   ├── CreatorItem       创作者
│   ├── LayerItem         轨道层
│   ├── EffectInfoItem    特效说明
│   ├── CatShareItem      分类占比
│   └── PackTrackItem     素材包曲目
├── 静态数据(16 组常量)
│   ├── WAVE_BARS         44 根波形振幅
│   ├── RULER_TICKS       时间标尺刻度
│   ├── EFFECT_CHIPS      8 种特效芯片
│   ├── EFFECT_INFOS      6 条特效说明
│   ├── MARKET_CATEGORIES 9 个市场分类
│   ├── RING_CATEGORIES   6 个铃声分类
│   ├── mockRingtones     24 款铃声
│   ├── mockCreations     14 个我的作品
│   ├── mockPacks         16 个素材包
│   ├── mockCreators      12 位创作者
│   ├── mockLayers        6 层轨道
│   ├── DOWNLOAD_WEEK     7 日下载量
│   ├── EARN_WEEK         7 日收益
│   ├── WEEK_LABELS       7 日标签
│   ├── CATEGORY_SHARE    5 类分类占比
│   ├── PACK_TRACKS       8 条素材曲目
│   ├── TIP_OPTIONS       4 档打赏金额
│   └── REPORT_REASONS    5 条举报原因
├── 工具函数(12 个 function)
├── 枚举(1 个 enum WorkTab)
├── 主入口组件 RingsmithApp
└── 6 个 Tab 内容组件
    ├── WorkshopPanel   工坊
    ├── MarketPanel     市场
    ├── MyRingsPanel    我的铃声
    ├── LibraryPanel    素材库
    ├── RankPanel       榜单
    └── MinePanel       我的

3.2 组件层级关系

铃声工坊的组件树采用扁平的 Tab 切换结构,主入口组件 RingsmithApp 通过 @State activeTab 控制当前显示的 Tab 内容,6 个 Tab 组件平级挂载在 contentArea() Builder 中:

RingsmithApp (@Entry @Component)
├── contentArea() — 动态切换 6 个 Tab
│   ├── WorkshopPanel   (activeTab === workshop)
│   ├── MarketPanel     (activeTab === market)
│   ├── MyRingsPanel    (activeTab === rings)
│   ├── LibraryPanel    (activeTab === library)
│   ├── RankPanel       (activeTab === rank)
│   └── MinePanel       (activeTab === mine)
└── bottomTabs() — 固定底部导航栏
    └── tabItem × 6

每个 Tab 组件内部通过 @State 管理自己的局部状态(如播放状态、选中分类、弹窗显隐等),并通过 overlay() 修饰符挂载自己的弹窗层。这种设计确保了各 Tab 之间的状态隔离,切换 Tab 时不会互相干扰。

3.3 弹窗架构:overlay + modalOverlay 模式

铃声工坊的弹窗系统采用鸿蒙 overlay() 修饰符 + modalOverlay() Builder 的统一模式。每个 Tab 组件的 build() 方法末尾都通过 .overlay(this.modalOverlay(() => { ... })) 挂载一个全屏遮罩层,modalOverlay Builder 根据不同的 @State 布尔值决定显示哪个弹窗。

以工坊 Tab 为例,它管理 4 种弹窗:

@State showExportModal: boolean = false    // 导出弹窗(底部抽屉)
@State showPreviewModal: boolean = false   // 试听弹窗(居中卡片)
@State showSuccessModal: boolean = false   // 导出成功弹窗(居中卡片)
@State showEffectInfo: boolean = false     // 特效说明弹窗(居中列表)

modalOverlay Builder 通过 if-else 链判断当前显示哪个弹窗,并统一设置半透明深灰背景 rgba(31,41,55,0.6)。底部抽屉类弹窗使用 justifyContent(FlexAlign.End) 从底部滑入,居中卡片类弹窗使用 justifyContent(FlexAlign.Center) 居中显示。点击遮罩层空白区域触发 onClose 回调关闭所有弹窗。

这种模式的优势是:

  • 统一管理:所有弹窗状态集中在组件的 @State 中,便于追踪。
  • 统一遮罩:所有弹窗共享同一个半透明背景,视觉一致。
  • 统一关闭:点击遮罩层统一关闭,无需为每个弹窗单独写关闭逻辑。
  • 灵活定位:通过 justifyContent 控制弹窗从底部或居中出现。

3.4 状态管理策略

铃声工坊采用组件内局部状态管理策略,没有使用全局状态管理库(如 AppStorage、PersistentStorage 或 Redux 模式)。所有状态都通过 @State 装饰器声明在各自的组件内部,遵循"谁使用谁管理"的原则。

主要状态分类:

状态类型 示例 所在组件
Tab 切换 activeTab: WorkTab RingsmithApp
播放状态 isPlaying: boolean, playingId: number WorkshopPanel, MarketPanel
选中项 selEffect: string, activeCategory: string, libSeg: number 各 Tab
弹窗显隐 showExportModal, showDetail, showEdit 各 Tab
表单数据 exportFormat, editName, publishPrice, agreed 各 Tab
交互状态 liked: boolean, checkBig: boolean, payChannel: number 各 Tab

这种轻量级状态管理策略适合单文件演示应用,避免了全局状态的复杂性。如果未来需要跨 Tab 共享数据(如用户登录状态、购物车),可以引入 AppStorage@Provide/@Consume 进行跨组件状态共享。


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

4.1 核心数据模型

铃声工坊定义了 9 个 interface 作为核心数据模型,覆盖铃声商品、我的作品、素材包、创作者、轨道层、特效说明、分类占比、素材曲目等业务实体。

4.1.1 RingtoneItem — 铃声商品
interface RingtoneItem {
  id: number           // 唯一标识
  name: string         // 铃声名称
  creator: string      // 创作者名称
  category: string     // 分类(动漫/游戏/影视/搞笑/纯音乐/电子/机械)
  duration: string     // 时长(如 "00:28")
  price: number        // 价格(0 表示免费,单位:工坊币)
  downloads: number    // 下载量
  likes: number        // 收藏数
  emoji: string        // 封面 emoji
  color: string        // 封面背景色(马卡龙色)
}

这是市场 Tab 的核心数据模型,mockRingtones 常量包含 24 款铃声数据。每款铃声都有独特的 emoji 封面与马卡龙色背景,如「樱花碎片」🌸 #FBCFE8 粉、「像素骑士」🎮 #BBF7D0 绿、「赛博冲刺」⚡ #BFDBFE 蓝。price: 0 表示免费铃声,在列表中显示绿色「免费」标签。

4.1.2 CreationItem — 我的作品
interface CreationItem {
  id: number           // 唯一标识(101-114)
  name: string         // 作品名称
  category: string     // 分类
  duration: string     // 时长
  downloads: number    // 下载量(草稿为 0)
  revenue: number      // 累计收益(元)
  emoji: string        // 封面 emoji
  published: boolean   // 是否已上架(true=已上架,false=草稿)
  created: string      // 创建日期(如 "08-02")
}

这是我的铃声 Tab 的核心数据模型,mockCreations 包含 14 个作品,其中 10 个已上架、4 个草稿。已上架作品显示下载量与收益,草稿显示「未发布」。每个作品支持编辑(修改名称/分类/标签/可见性)、删除(危险确认弹窗)、上架(定价 + 分成 + 协议弹窗)三种操作。

4.1.3 PackItem — 素材包
interface PackItem {
  id: number           // 唯一标识
  name: string         // 素材包名称
  tracks: number       // 包含音轨数
  sales: number        // 销量
  price: number        // 价格(0=免费)
  emoji: string        // 封面 emoji
  color: string        // 封面背景色
  tag: string          // 标签(热门/新品/免费/萌系/国风/实用/怀旧/电子/治愈/氛围)
}

这是素材库 Tab 的核心数据模型,mockPacks 包含 16 个素材包,从「夏日弹跳鼓组」🥁 到「心跳与呼吸」🫀,覆盖鼓组、8bit、雨声、人声、合成器、猫咪、机械、古风、警报、太空、口哨、胶片、电子鼓、木琴、火车、心跳等 16 种主题。每个素材包有独特的标签,用于在列表中展示角标。

4.1.4 CreatorItem — 创作者
interface CreatorItem {
  id: number           // 唯一标识
  name: string         // 创作者名称
  works: number        // 作品数
  fans: number         // 粉丝数
  revenue: number      // 总收益(元)
  emoji: string        // 头像 emoji
  badge: string        // 徽章(金牌创作者/人气王/实力派/新锐作者)
  trend: string        // 榜单趋势(up=上升/down=下降/flat=持平)
}

这是榜单 Tab 的核心数据模型,mockCreators 包含 12 位创作者,从「音浪小陆」🎧(86 作品/12.8w 粉丝/¥12480 收益/金牌创作者)到「极客音效铺」🖥️(33 作品/1.64w 粉丝/¥1870 收益/新锐作者)。TOP3 创作者在颁奖台中展示,4-12 名在完整榜单中列表展示。trend 字段配合 trendIcon()trendColor() 工具函数显示上升↑/下降↓/持平—的趋势箭头与颜色。

4.1.5 LayerItem — 轨道层
interface LayerItem {
  name: string         // 轨道名称(如 "主旋律人声.mp3")
  kind: string         // 轨道类型(音频/环境/特效)
  len: string          // 时长(如 "00:28" 或 "3.0s" 或 "全程")
  color: string        // 轨道标识色
}

这是工坊 Tab 轨道列表的数据模型,mockLayers 包含 6 层轨道:主旋律人声(橙 #F97316)、鼓点节奏(蓝 #0EA5E9)、背景和声(紫 #7C3AED)、海浪环境音(粉 #EC4899)、淡出标记(绿 #22C55E)、升调+2半音(黄 #EAB308)。每层轨道左侧有 4px 宽的彩色标识条,与波形编辑区的多轨概念呼应。

4.1.6 其他数据模型
  • EffectInfoItem:特效说明,包含 name(特效名)和 desc(详细描述),EFFECT_INFOS 包含 6 条说明(淡入淡出、回声、混响、变速、升调、降噪)。
  • CatShareItem:分类占比,包含 name(分类名)、percent(百分比)、color(条形色),CATEGORY_SHARE 包含 5 类(动漫 32%、游戏 26%、纯音乐 18%、搞笑 14%、机械影视 10%)。
  • PackTrackItem:素材包曲目,包含 name(曲目名)、duration(时长)、size(文件大小),PACK_TRACKS 包含 8 条试听曲目。

4.2 静态数据全景

铃声工坊定义了 19 组静态数据常量,是整个应用的内容基础。以下是数据量统计:

常量名 数据量 用途
WAVE_BARS 44 个数值 波形编辑器振幅数据
RULER_TICKS 7 个刻度 时间标尺(0-30 秒)
EFFECT_CHIPS 8 种特效 特效芯片标签
EFFECT_INFOS 6 条说明 特效详细说明
MARKET_CATEGORIES 9 个分类 市场分类筛选
RING_CATEGORIES 6 个分类 编辑弹窗分类选择
mockRingtones 24 款铃声 市场铃声列表
mockCreations 14 个作品 我的铃声作品
mockPacks 16 个素材包 素材库商品
mockCreators 12 位创作者 榜单创作者数据
mockLayers 6 层轨道 工坊轨道列表
DOWNLOAD_WEEK 7 日数据 近 7 日下载量柱状图
EARN_WEEK 7 日数据 近 7 日收益柱状图
WEEK_LABELS 7 个标签 周一至周日
CATEGORY_SHARE 5 类占比 分类占比条形图
PACK_TRACKS 8 条曲目 素材包试听列表
TIP_OPTIONS 4 档金额 打赏金额选择
REPORT_REASONS 5 条原因 举报原因选择

4.3 工具函数

铃声工坊定义了 12 个工具函数,用于数据格式化与计算:

函数名 功能 示例
formatCount(n) 数字格式化(万/千) 128400 → “12.8w”,9600 → “9.6k”
priceTag(p) 价格标签化 0 → “免费”,3 → “¥3”
waveH(v) 波形条高度计算 Math.max(6, v * 0.72) + 'vp'
rulerLabel(sec) 秒数转分:秒格式 65 → “1:05”
barH(v, maxV) 柱状图高度计算 Math.max(8, v / maxV * 96) + 'vp'
catBarW(p) 分类占比条宽度计算 (p * 2.4) + 'vp'
trendIcon(t) 趋势图标转换 up → “↑”,down → “↓”,flat → “—”
trendColor(t) 趋势颜色转换 up → “#16A34A”,down → “#DC2626”,flat → “#9CA3AF”
getRingtoneById(id) 按 ID 查找铃声 while 循环遍历 mockRingtones
getCreationById(id) 按 ID 查找作品 while 循环遍历 mockCreations
getPackById(id) 按 ID 查找素材包 while 循环遍历 mockPacks
getCreatorById(id) 按 ID 查找创作者 while 循环遍历 mockCreators
exportSizeText(fmt, quality) 导出文件大小估算 WAV → “约 2.4 MB”,高清 → “约 640 KB”,标准 → “约 280 KB”

其中 4 个 getXxxById 函数采用 while 循环遍历查找,而非使用 Array.find(),这是为了兼容鸿蒙 ArkTS 的严格类型检查(ArkTS 对箭头函数和高阶方法的支持有一定限制)。每个函数在找不到对应 ID 时返回数组第一个元素作为兜底,避免空指针异常。

4.4 枚举定义

铃声工坊定义了 1 个枚举 WorkTab,用于标识当前激活的 Tab:

enum WorkTab {
  workshop,   // 工坊
  market,     // 市场
  rings,      // 我的铃声
  library,    // 素材库
  rank,       // 榜单
  mine        // 我的
}

主入口组件 RingsmithApp 通过 @State activeTab: WorkTab = WorkTab.workshop 管理当前 Tab,默认显示工坊 Tab。底部导航栏的每个 tabItem 通过 onClick(() => { this.activeTab = tab }) 切换 Tab。


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

铃声工坊的核心交互由 6 个底部 Tab 构成,每个 Tab 承担一个独立的功能模块。本章逐一解析每个 Tab 的页面结构、交互逻辑与设计亮点。

5.1 工坊 Tab:波形剪辑编辑器

工坊 Tab 是整个应用的核心,也是技术含量最高的页面。它将一个专业的音频编辑器浓缩到手机屏幕中,包含以下功能区域:

工坊Tab:深色工程栏 + 44根波形条编辑区 + 时间标尺 + 裁剪手柄 + 传输控制条 + 8种特效芯片 + 6层轨道列表 + 试听/导出双按钮

顶部工程栏(深灰 #1F2937 背景):左侧显示「🛠️ 铃声工坊」标题 + 「未命名项目」灰色标签,右侧是橙色圆形保存按钮。工程栏采用深色背景,与下方的波形编辑区融为一体,营造专业编辑器的氛围。

波形编辑区(近黑 #111827 背景,圆角底部 20px):这是工坊的视觉核心。

  • 顶部显示「WAVEFORM · 波形编辑」浅橙标签。
  • 44 根波形条通过 ForEach(WAVE_BARS, ...) 渲染,每根宽度 4px,高度由 waveH(v) 函数计算(Math.max(6, v * 0.72) + 'vp'),圆角 2px。
  • 波形条默认色 #FDBA74(浅橙),播放时变为 #F97316(活力橙)并伴随 1.15 倍 Y 轴缩放动画,动画时长 320ms,曲线 EaseInOut
  • 背景有 3 条水平参考线(Divider 组件,#374151 色),模拟专业编辑器的网格线。
  • 一根 2px 宽、102vp 高的粉色 #EC4899 播放指针线悬浮在波形上方,标记当前播放位置。

时间标尺:通过 ForEach(RULER_TICKS, ...) 渲染 7 个刻度(0/5/10/15/20/25/30 秒),每个刻度通过 rulerLabel(t) 函数格式化为「0:00」「0:05」等格式,字号 9px,灰色 #9CA3AF

裁剪范围手柄:左右两个橙色圆角手柄(22×26px),中间显示「截取 00:12 - 00:28 · 16s」浅橙文字,用户可通过拖拽手柄调整裁剪范围。

传输控制条:居中排列 4 个按钮——上一曲 ⏮(深灰圆形 40px)、播放/暂停 ▶⏸(橙色圆形 54px,带 12px 橙色阴影)、停止 ⏹(深灰圆形 40px)、撤销 ↺(深灰圆形 40px)。播放按钮是整个控制条的视觉焦点,尺寸最大、颜色最亮、带阴影。

特效芯片区:标题「✨ 音频特效(点击应用)」深棕色 #7C2D12,下方通过 Flex({ wrap: FlexWrap.Wrap }) 排列 8 种特效芯片(淡入/淡出/回声/混响/变速/升调/降噪/反转)。选中的芯片使用橙色背景 + 白色文字 + 粗体,未选中的使用浅橙 #FFE8CC 背景 + 深橙文字。右侧有一个 ℹ️ 按钮,点击弹出特效说明弹窗。

轨道图层区:标题「🎞️ 轨道图层(6)」,下方是一个可滚动的轨道列表(constraintSize({ maxHeight: 176 }) 限高)。每层轨道显示左侧 4px 彩色标识条 + 轨道名称 + 类型/时长 + 右侧 👁️ 可见性图标。6 层轨道使用 6 种不同颜色(橙/蓝/紫/粉/绿/黄),直观区分不同音频层。

底部操作栏:两个胶囊按钮——「🎧 试听」(浅橙背景 + 深橙文字,layoutWeight(1))和「📦 导出铃声」(橙色背景 + 白色文字,layoutWeight(1.4),带橙色阴影)。导出按钮比试听按钮更宽(1.4 vs 1),突出主要操作。

5.2 市场 Tab:铃声电商货架

市场 Tab 是铃声工坊的电商核心,采用经典的电商 App 布局:头部搜索栏 + 分类横滑 + 精选横幅 + 商品大列表。

市场Tab:橙色电商头部(搜索框+通知)+ 9分类横滑条 + 今日精选紫色横幅 + 24款铃声大列表(emoji封面+名称+创作者+播放按钮+价格)

电商风头部(橙色 #F97316 背景):

  • 第一行:左侧「RINGSMITH 市场」白色粗体大标题,右侧白色半透明圆形通知按钮。
  • 第二行:白色圆角搜索框(高 38px,圆角 19px),左侧「🔍 搜索铃声 / 创作者 / 素材包」浅橙文字,右侧 🎤 语音搜索图标。

分类横滑条:9 个分类标签(全部/热门/动漫/游戏/影视/搞笑/纯音乐/电子/机械)横向滚动,选中态橙色背景 + 白色文字 + 粗体,未选中态浅橙背景 + 深橙文字。分类筛选是电商 App 的标配交互。

今日精选横幅(紫色 #7C3AED 背景,圆角 18px):左侧显示「今日精选 TOP1」柠黄标签 + 「樱花碎片」白色大标题 + 「音浪小陆 · 12.8w 下载」白色半透明副标题,右侧 🌸 大 emoji。点击 emoji 可切换播放状态。紫色横幅在橙色主调的市场中形成视觉跳色,突出精选内容。

铃声大列表:24 款铃声通过 ForEach(mockRingtones, ...) 渲染,每款铃声是一个白色圆角卡片(圆角 16px),包含:

  • 左侧 48×48px 马卡龙色圆角封面(emoji 图标)。
  • 中间:铃声名称(14px 粗体深灰)+ 创作者·时长·下载量(10px 灰色)+ 分类标签(浅橙底深橙字)+ 收藏数(粉色 ♥)。
  • 右侧:圆形播放按钮(34px,橙色/蓝色切换)+ 价格标签(免费=绿色,付费=橙色粗体)。

点击卡片弹出铃声详情底部抽屉,点击播放按钮切换播放状态(播放时按钮变蓝,波形条变橙)。

5.3 我的铃声 Tab:作品管理与上架

我的铃声 Tab 是创作者的工作台,展示作品统计、下载趋势与作品宫格管理。

我的铃声Tab:标题栏(新建按钮)+ 四列数据统计卡 + 近7日下载量柱状图 + 作品双列宫格(封面+名称+时长+编辑/删除按钮)+ 上架市场大按钮

标题栏:左侧「🎵 我的铃声」标题 + 「14 个作品 · 4 个草稿待上架」灰色副标题,右侧橙色「+ 新建」圆角按钮。

四列数据统计卡(白色圆角卡片):作品总数 14(橙)、累计下载 42.8k(蓝)、累计收益 ¥903(紫)、收藏人气 86(粉)。四种颜色对应四种数据维度,一目了然。

近 7 日下载量柱状图(白色圆角卡片):7 根柱状图通过 ForEach(DOWNLOAD_WEEK, ...) 渲染,高度由 barH(v, 1240) 计算(最大值 1240 对应 96vp 高度)。周六(idx===5)使用橙色高亮,其他日期使用浅橙。每根柱子上方显示数值,下方显示星期标签。

作品双列宫格:14 个作品通过 Flex({ wrap: FlexWrap.Wrap }) 双列排列,每个作品卡片(宽度 47%,右侧间距 6%)包含:

  • 封面区:76px 高浅橙背景 + emoji 图标 + 右上角状态角标(已上架=绿色,草稿=灰色)。
  • 作品名称(12px 粗体深灰,maxLines 1)。
  • 时长·日期·下载量(9px 灰色,maxLines 1)。
  • 操作按钮行:✏️ 编辑(浅橙底)+ 🗑️ 删除(浅红底),各占 50% 宽度。

上架市场大按钮:橙色胶囊按钮「🛒 上架市场 · 赚取收益」,带橙色阴影,点击弹出上架定价弹窗。

5.4 素材库 Tab:分段控件 + 双列宫格

素材库 Tab 是素材包的电商货架,采用分段控件切换 + 双列宫格展示的布局。

素材库Tab:标题栏 + 热门/最新/免费三段分段控件 + 16个素材包双列宫格(emoji封面+标签角标+名称+轨数+销量+价格按钮)+ 底部提示

标题栏:「📦 素材库」18px 粗体深灰标题。

分段控件(浅橙 #FFE8CC 背景,圆角 20px,内边距 4px):三个分段——🔥 热门、🆕 最新、🎁 免费。选中段使用橙色背景 + 白色文字 + 粗体,未选中段使用透明背景 + 深橙文字。分段控件是鸿蒙原生的经典交互模式,通过 libSeg 状态值切换。

素材包双列宫格:16 个素材包双列排列(宽度 47%,右侧间距 6%),每个卡片包含:

  • 封面区:84px 高马卡龙色背景 + emoji 图标 + 左上角标签角标(热门/新品/免费/萌系/国风/实用/怀旧/电子/治愈/氛围,深灰半透明底白字)。
  • 素材包名称(13px 粗体深灰,maxLines 1)。
  • 轨数·销量(9px 灰色)。
  • 价格按钮:免费=绿底白字,付费=橙底白字,占满宽度。

点击卡片弹出素材包详情弹窗(居中大卡,含曲目列表可滚动),点击价格按钮弹出购买素材包底部抽屉。

底部提示:「💡 素材包购买后可永久商用」+「上架作品若使用素材,需在简介注明出处」,灰色小字居中。

5.5 榜单 Tab:颁奖台 + 创作者排行

榜单 Tab 是创作者的竞技场,采用颁奖台仪式感 + 完整榜单 + 数据可视化的组合布局。

榜单Tab:紫色头部(每日更新标签+本周/本月/总榜切换)+ TOP3颁奖台(金银铜三色台)+ 热门铃声分类占比条形图 + 4-12名完整榜单 + 分成规则入口

榜单头部(紫色 #7C3AED 背景):

  • 第一行:「🏆 创作者榜」白色粗体标题 + 「每日 10:00 更新」柠黄半透明标签。
  • 第二行:本周/本月/总榜三个模式芯片,选中态白色背景 + 紫色文字 + 粗体,未选中态白色半透明背景 + 白色文字。

TOP3 颁奖台:三位创作者通过 Row 横向排列,底部对齐(alignItems(VerticalAlign.Bottom))。中间冠军(mockCreators[0])台子最高 118vp 🥇 金色,左侧亚军(mockCreators[1])台子 96vp 🥈 银色,右侧季军(mockCreators[2])台子 78vp 🥉 铜色。每个台子显示奖牌 emoji + 头像 emoji + 创作者名 + 收益 + 粉丝数。颁奖台的高度差异营造出仪式感与竞技感。

热门铃声分类占比(白色圆角卡片):5 类分类占比通过横向条形图展示,每行包含分类名(56px 宽)+ 进度条(layoutWeight(1),浅橙底 + 彩色填充,宽度由 catBarW(p) 计算)+ 百分比(34px 宽,对应分类色粗体)。动漫 32% 橙、游戏 26% 蓝、纯音乐 18% 紫、搞笑 14% 粉、机械影视 10% 黄。

完整榜单(4-12 名):9 位创作者通过 ForEach(mockCreators.slice(3), ...) 渲染,每行包含排名数字(24px 宽灰色粗体)+ 38px 圆形头像 + 创作者名/徽章/作品数/粉丝数 + 收益/趋势箭头(上升绿/下降红/持平灰)。点击行弹出创作者详情底部抽屉。

分成规则入口:标题行右侧「分成规则 ›」蓝色文字,点击弹出收益分成规则说明弹窗。

5.6 我的 Tab:个人中心与收益

我的 Tab 是用户的个人中心,展示个人资料、等级进度、收益趋势与功能菜单。

我的Tab:天蓝个人资料卡(头像+昵称+等级+工坊币余额+等级进度条)+ 近7日收益柱状图 + 功能菜单列表(创作者认证/收益提现/已购素材/消息通知/账号安全/关于)

个人资料卡(天蓝 #0EA5E9 背景,底部圆角 22px):

  • 第一行:62px 圆形头像(柠黄底 🧑‍🎤 emoji)+ 昵称「铃声捣鼓鬼」白色粗体 + 「Lv.6 金牌创作者 · 加入 326 天」白色半透明 + 「🪙 工坊币余额 86.5」柠黄粗体。
  • 等级进度条:「Lv.6 → Lv.7(还差 1,240 点声望)」白色半透明小字 + 6px 高进度条(白色半透明底 + 柠黄填充 68%)。

近 7 日收益柱状图(白色圆角卡片):7 根柱子通过 ForEach(EARN_WEEK, ...) 渲染,高度由 barH(v, 25) 计算(最大值 25 对应 96vp)。周六(idx===5)使用天蓝高亮,其他日期使用浅蓝。底部显示「本周合计 ¥102.80」天蓝粗体。

功能菜单列表(白色圆角卡片):6 个菜单项通过 Divider 分隔,每项包含图标 + 标题/副标题 + 右箭头 ›:

  • 🏅 创作者认证(解锁分成提现与榜单特权)
  • 💰 收益与提现(余额 ¥903.20 可提现)
  • 📦 已购素材包(12 个素材包)
  • 🔔 消息通知(创作周报已生成)
  • 🛡️ 账号与安全(绑定手机 138****6688)
  • ℹ️ 关于铃声工坊(当前版本 v3.2.1)

点击「创作者认证」弹出认证条件清单弹窗,点击「关于」弹出关于信息弹窗。

底部标语:「RINGSMITH · 让每一次来电都有态度」灰色小字居中。


第六章 弹窗交互体系深度解析

铃声工坊设计了 18 种风格各异的弹窗,涵盖底部抽屉、居中卡片、危险确认、表单输入、宫格分享、说明列表等多种形态。本章按弹窗类型分类解析。

6.1 底部抽屉类弹窗

底部抽屉类弹窗从屏幕底部滑入,通常用于展示详情、表单或操作面板。铃声工坊中有以下底部抽屉:

导出弹窗(工坊 Tab):底部抽屉,包含导出标题 + 截取信息 + 格式选择(MP3/M4R/WAV 三卡片,选中态橙边浅橙底)+ 音质档位(标准/高清两芯片)+ 文件大小提示 + 开始导出按钮。格式卡片通过 formatCard Builder 复用,选中态使用 2px 橙色边框 + 浅橙背景。

铃声详情抽屉(市场 Tab):底部抽屉,顶部有灰色拖拽条,包含铃声封面 + 名称/创作者/评分 + 30 根波形条 + 四列数据统计(下载/时长/收藏/价格)+ 设为铃声/收藏按钮行 + 打赏/举报/分享链接行。波形条在播放时变橙,收藏按钮心形在已收藏时变粉。

购买素材包抽屉(素材库 Tab):底部抽屉,简洁订单卡,包含素材包封面 + 名称/轨数 + 价格 + 支付方式(工坊币余额)+ 确认支付按钮。

创作者详情抽屉(榜单 Tab):底部抽屉,包含创作者头像 + 名称/徽章/趋势 + 关注按钮 + 三列数据(作品/粉丝/总收益)+ 代表作列表(3 款铃声)+ 进入创作者主页按钮。

编辑铃声表单抽屉(我的铃声 Tab):底部抽屉,完整表单,包含铃声名称输入框 + 分类选择(6 分类芯片)+ 标签输入框 + 公开/私密切换开关 + 取消/保存按钮行。分类芯片选中态橙色,公开开关使用 🔓/🔒 图标 + 绿/灰背景。

上架市场抽屉(我的铃声 Tab):底部抽屉,包含上架标题 + 作品名 + 定价档位(免费/1币/3币/6币四卡片)+ 分成计算明细(创作者70%/平台30%)+ 协议勾选框 + 确认上架按钮(未勾选协议时灰色禁用)。定价卡片选中态橙色背景白字,分成明细展示每下载的收入计算。

6.2 居中卡片类弹窗

居中卡片类弹窗在屏幕中央显示,通常用于提示、确认或简单操作。铃声工坊中有以下居中卡片:

导出成功弹窗(工坊 Tab):居中卡片,核心是一个 72px 绿色圆形对勾 ✔(带 420ms 缩放出现动画),下方「导出成功!」标题 + 格式/音质/保存位置说明 + 留在工坊/去上架市场双按钮。对勾动画通过 onAppear 触发 checkBig 状态变化实现。

试听弹窗(工坊 Tab):居中卡片,包含「🎧 试听预览」标题 + 26 根波形条 + 进度条(62% 橙色填充)+ 已应用特效标签 + 完成按钮。

特效说明弹窗(工坊 Tab):居中卡片,包含「✨ 特效说明」标题 + 可滚动的特效列表(6 条,每条橙色标题 + 灰色描述,暖奶油色背景圆角卡片)+ 知道了按钮。列表通过 constraintSize({ maxHeight: '56%' }) 限高,内部 Scroll 滚动。

购买铃声弹窗(市场 Tab):居中卡片,包含确认购买标题 + 商品行(铃声名 + 价格)+ 支付方式选择(工坊币/微信/支付宝三行,单选圆点)+ 支付按钮。支付方式行通过 payRow Builder 复用。

打赏弹窗(市场 Tab):居中卡片,包含打赏创作者标题 + 鼓励语 + 4 档金额宫格(2/6/18/66 币,22% 宽度)+ 留言输入框 + 发送打赏按钮。金额宫格选中态橙色背景白字。

分享弹窗(市场 Tab):居中卡片,包含分享铃声标题 + 6 渠道宫格(微信/朋友圈/QQ/微博/链接/保存封面,30% 宽度)+ 取消按钮。

删除确认弹窗(我的铃声 Tab):居中危险确认卡片,包含 🗑️ 图标 + 「删除「作品名」?」标题 + 警告说明(删除后数据无法恢复)+ 再想想/确认删除双按钮(删除按钮红色 #DC2626)。

素材包详情弹窗(素材库 Tab):居中大卡,包含素材包封面 + 名称/轨数/销量/标签 + 分割线 + 试听曲目列表(8 条,可滚动,constraintSize({ maxHeight: '42%' }))+ 加入收藏/购买双按钮。

创作者认证弹窗(我的 Tab):居中卡片,包含 🏅 标题 + 解锁权益说明 + 认证条件清单(3 条,已完成✔绿色,未完成○灰色)+ 稍后再说/去绑定并申请双按钮。

关于弹窗(我的 Tab):居中小卡,包含 🛠️ 大图标 + 「铃声工坊 RINGSMITH」标题 + 版本号 + slogan「剪一截热爱,接一通惊喜。让每一次来电都值得侧耳。」+ 关闭按钮。

6.3 特殊形态弹窗

举报弹窗(市场 Tab):居中卡片,包含举报标题 + 5 条原因单选列表(抄袭/音质/侵权/刷量/内容不当)+ 补充说明输入框 + 取消/提交举报双按钮(提交按钮红色)。原因列表通过 ForEach(REPORT_REASONS, ...) 渲染,选中态红色圆点。

收益分成规则弹窗(榜单 Tab):居中说明卡片,包含 💰 标题 + 5 条规则说明(定价收入70%/打赏90%/素材包80%/提现门槛/违规处罚),违规条款使用红色文字。

6.4 弹窗设计总结

铃声工坊的 18 种弹窗遵循统一的设计规范:

  • 背景遮罩:统一使用 rgba(31,41,55,0.6) 半透明深灰。
  • 圆角:底部抽屉顶部圆角 22px,居中卡片圆角 20px。
  • 内边距:底部抽屉 18px,居中卡片 20-22px。
  • 按钮:主操作按钮统一使用橙色胶囊按钮(ButtonType.Capsule),危险操作使用红色,次要操作使用浅灰/浅橙。
  • 关闭方式:点击遮罩层空白区域统一关闭,部分弹窗有显式的 ✕ 关闭按钮。
  • 定位方式:底部抽屉使用 justifyContent(FlexAlign.End),居中卡片使用 justifyContent(FlexAlign.Center)

弹窗交互示例1:导出底部抽屉(格式三卡片+音质两档+开始导出)/ 铃声详情抽屉(波形+数据+操作)/ 购买居中卡(支付方式单选)/ 打赏宫格 / 分享宫格 / 举报原因列表

弹窗交互示例2:导出成功对勾动画 / 试听波形进度 / 特效说明列表 / 素材包详情曲目 / 创作者认证条件 / 关于信息


第七章 动画与过渡效果

铃声工坊虽然是一个工具类应用,但在关键交互节点加入了精心设计的动画效果,提升了界面的活力与反馈感。

7.1 波形条播放动画

工坊 Tab 的 44 根波形条是动画的核心载体。当 isPlaying 状态变为 true 时:

  • 波形条颜色从 #FDBA74(浅橙)过渡到 #F97316(活力橙)。
  • 每根波形条的 Y 轴缩放从 1 变为 1.15,产生"跳动"的视觉效果。
  • 动画时长 320ms,曲线 Curve.EaseInOut,确保过渡平滑自然。
.backgroundColor(this.isPlaying ? '#F97316' : '#FDBA74')
.scale({ y: this.isPlaying ? 1.15 : 1 })
.animation({ duration: 320, curve: Curve.EaseInOut })

市场 Tab 的铃声详情抽屉中也有 30 根波形条,播放时同样从浅橙变橙,形成跨页面的视觉一致性。

7.2 播放按钮脉冲动画

工坊 Tab 的中央播放按钮(54px 橙色圆形)带有 12px 橙色阴影 shadow({ radius: 12, color: 'rgba(249,115,22,0.45)', offsetY: 3 }),在视觉上产生"发光脉冲"的效果。虽然没有使用显式的动画属性,但阴影的存在让按钮在静态时也有呼吸感。

市场 Tab 的铃声播放按钮在播放时从橙色变为蓝色 #0EA5E9,通过 260ms 动画过渡,颜色切换本身就是一种视觉反馈。

7.3 导出成功对勾缩放动画

导出成功弹窗的绿色对勾圆形(72px)通过 onAppear 生命周期钩子触发缩放动画:

.onAppear(() => {
  this.checkBig = true
})

虽然代码中 checkBig 状态主要用于逻辑控制,但配合 .animation({ duration: 420, curve: Curve.EaseOut }) 可以实现对勾从小到大的弹性出现效果,增强"成功"的仪式感。

7.4 Tab 切换与状态过渡

底部导航栏的 Tab 切换虽然没有使用显式的页面转场动画,但通过 activeTab 状态变化触发的内容重建,配合各 Tab 内部的 @State 初始化,实现了快速的页面切换。选中的 Tab 文字从灰色 #9CA3AF 变为橙色 #F97316 并加粗,提供清晰的当前位置反馈。

7.5 收藏心形动画

市场 Tab 铃声详情抽屉中的收藏按钮,在 liked 状态切换时:

  • 心形图标从 ♡(空心)变为 ♥(实心)。
  • 颜色从灰色 #9CA3AF 变为粉色 #EC4899
  • 配合 240ms EaseOut 动画,产生"心动"的视觉反馈。
Text(this.liked ? '♥' : '♡')
  .fontColor(this.liked ? '#EC4899' : '#9CA3AF')
  .animation({ duration: 240, curve: Curve.EaseOut })

7.6 动画设计哲学

铃声工坊的动画设计遵循克制而精准的原则:

  • 不在不必要的地方加动画:列表滚动、卡片布局等静态元素没有多余动画,保证性能与流畅度。
  • 在关键反馈节点加动画:播放状态、收藏状态、导出成功等用户操作的核心反馈点都有动画。
  • 动画时长统一:大部分动画在 220-420ms 之间,曲线以 EaseOutEaseInOut 为主,符合 Material Design 的动效规范。
  • 颜色即动画:很多状态变化通过颜色过渡实现(浅橙→深橙、灰→粉、橙→蓝),比位移/缩放动画更轻量但同样有效。

第八章 波形剪辑系统:44 根波形条的编辑器设计哲学

工坊 Tab 的波形编辑器是铃声工坊最具技术含量的模块。它没有使用鸿蒙原生的音频可视化组件(实际上 ArkUI 也没有提供),而是通过 44 根 Column 组件 + 静态振幅数据 + 状态驱动动画,手工构建了一个看起来专业的波形编辑器。

8.1 波形数据设计

WAVE_BARS 常量包含 44 个数值,范围从 10 到 96,模拟一段音频的振幅包络:

const WAVE_BARS: number[] = [
  10, 22, 34, 18, 46, 58, 30, 72, 40, 26,
  64, 88, 52, 70, 96, 60, 34, 80, 44, 28,
  56, 74, 38, 90, 50, 66, 24, 82, 36, 20,
  48, 62, 30, 76, 42, 26, 54, 68, 32, 16,
  44, 28
]

这 44 个数值不是随机生成的,而是经过精心设计,模拟了一段有起伏的音频波形:有低振幅的安静段(10、16、18),有高振幅的高潮段(88、90、96),有渐变的过渡段,整体呈现出自然的波形包络形态。

8.2 波形条渲染

每根波形条通过 ForEach 渲染为一个 Column 组件:

ForEach(WAVE_BARS, (v: number) => {
  Column() {}
    .width(4)
    .height(waveH(v))
    .borderRadius(2)
    .backgroundColor(this.isPlaying ? '#F97316' : '#FDBA74')
    .scale({ y: this.isPlaying ? 1.15 : 1 })
    .animation({ duration: 320, curve: Curve.EaseInOut })
}, (v: number) => v.toString())

关键设计点:

  • 宽度 4px:细窄的波形条看起来更像专业音频编辑器的波形。
  • 高度由 waveH(v) 计算Math.max(6, v * 0.72) + 'vp',确保最低高度 6vp(避免零高度不可见),最高约 69vp(96 * 0.72),在 108vp 高的编辑区内居中显示。
  • 圆角 2px:波形条顶部圆角,看起来更柔和。
  • 颜色状态驱动:播放时橙色,暂停时浅橙。
  • Y 轴缩放:播放时 1.15 倍,产生"跳动"感。

8.3 参考线与播放指针

波形编辑区的背景有 3 条水平参考线(通过 3 个 Divider 组件实现,颜色 #374151),间距 34vp,模拟专业编辑器的网格线。参考线通过 Stack 组件与波形条叠加,参考线在底层、波形条在上层。

播放指针是一根 2px 宽、102vp 高的粉色 #EC4899 竖线,圆角 1px,悬浮在波形条上方(同样通过 Stack 叠加)。粉色指针在橙色波形中形成视觉跳色,标记当前播放位置。

8.4 时间标尺与裁剪手柄

时间标尺通过 RULER_TICKS(0/5/10/15/20/25/30 秒)渲染 7 个刻度,每个刻度通过 rulerLabel(t) 格式化为「0:00」格式。标尺位于波形区下方,提供时间参考。

裁剪手柄由左右两个橙色圆角按钮(22×26px)和中间的「截取 00:12 - 00:28 · 16s」文字组成,位于时间标尺下方。用户可通过拖拽手柄调整裁剪范围(演示中为静态展示)。

8.5 编辑器设计总结

铃声工坊的波形编辑器虽然是"假"的(使用静态数据而非真实音频分析),但在视觉上达到了以假乱真的效果:

  • 44 根细窄波形条 + 自然振幅包络 = 专业波形视觉。
  • 深灰背景 + 橙色波形 + 粉色指针 = 高对比度专业配色。
  • 参考线 + 时间标尺 + 裁剪手柄 = 完整的编辑器 UI 元素。
  • 播放时波形变色 + 缩放 = 动态反馈。

这种"静态数据 + 状态驱动"的波形编辑器实现方式,在鸿蒙 ArkTS 中是一种轻量且高效的方案,适合演示原型或不需要真实音频分析的场景。


第九章 铃声市场系统:24 款铃声的电商货架

市场 Tab 是铃声工坊的电商核心,24 款铃声构成了完整的商品货架。本章解析市场系统的设计逻辑。

9.1 铃声数据设计

mockRingtones 包含 24 款铃声,覆盖 7 个分类:动漫(3)、游戏(3)、影视(2)、搞笑(4)、纯音乐(5)、电子(4)、机械(3)。每款铃声有独特的 emoji 封面与马卡龙色背景:

铃声名 分类 emoji 封面色 价格 下载量
樱花碎片 动漫 🌸 #FBCFE8 粉 ¥3 12.8w
像素骑士 游戏 🎮 #BBF7D0 绿 ¥2 9.67w
深夜便利店 纯音乐 🏪 #FED7AA 橙 免费 8.82w
老板来电话啦 搞笑 🤣 #FDE68A 黄 ¥1 7.65w
赛博冲刺 电子 #BFDBFE 蓝 ¥3 6.98w
胶片开机音 影视 🎬 #E9D5FF 紫 ¥2 6.42w

铃声数据的设计遵循电商商品的规律:头部商品(樱花碎片、像素骑士)下载量高,长尾商品下载量递减;免费铃声(深夜便利店、雨打屋檐、晨光口哨、夏日蝉鸣)穿插在付费铃声中,降低用户的消费门槛。

9.2 分类筛选系统

市场 Tab 提供 9 个分类标签(全部/热门/动漫/游戏/影视/搞笑/纯音乐/电子/机械),通过横向滚动条展示。选中态橙色背景白字粗体,未选中态浅橙背景深橙字。

分类筛选是电商 App 的标配交互,用户可以快速定位到自己感兴趣的铃声类型。9 个分类的设计覆盖了主流铃声消费场景:动漫/游戏/影视是 IP 衍生铃声的主力,搞笑是病毒传播铃声的主力,纯音乐/电子是个性化铃声的主力,机械是小众趣味铃声。

9.3 今日精选横幅

紫色 #7C3AED 背景的今日精选横幅是市场 Tab 的视觉跳色点。在橙色主调的市场中,紫色横幅立刻吸引用户注意力。横幅展示 TOP1 铃声「樱花碎片」的核心信息(名称、创作者、下载量),右侧大 emoji 可点击切换播放状态。

这种"精选横幅 + 列表"的布局是电商 App 的经典模式,横幅用于主推高转化商品,列表用于展示全量商品。

9.4 铃声卡片设计

每款铃声的卡片设计遵循电商商品卡片的信息层级:

  1. 封面(48×48px 马卡龙色圆角方块 + emoji):视觉第一焦点,快速识别铃声。
  2. 名称(14px 粗体深灰):商品标题,maxLines 1 防止溢出。
  3. 元信息(10px 灰色):创作者 · 时长 · 下载量,提供决策参考。
  4. 标签行:分类标签(浅橙底深橙字)+ 收藏数(粉色 ♥),提供分类与热度信息。
  5. 操作区:圆形播放按钮(34px,橙/蓝切换)+ 价格标签(免费绿/付费橙),提供试听与购买入口。

点击卡片弹出详情抽屉,点击播放按钮切换播放状态。这种"卡片点击=详情,按钮点击=操作"的交互分离,避免了误触。

9.5 铃声详情抽屉

铃声详情抽屉是市场 Tab 的核心交互面板,包含:

  • 拖拽条:顶部灰色圆角条,提示可下滑关闭。
  • 铃声信息:封面 + 名称 + 创作者/分类 + 评分/收藏数。
  • 波形条:30 根波形,播放时变橙,提供试听可视化。
  • 四列数据:下载量 / 时长 / 收藏数 / 价格,暖奶油色背景卡片。
  • 操作按钮行:设为铃声(橙色主按钮,1.4 倍宽)+ 收藏(心形圆形按钮)。
  • 次级操作行:打赏创作者(蓝)/ 举报抄袭(灰)/ 分享(紫),三个文字链接。

详情抽屉的信息密度很高,但通过分区(信息区/波形区/数据区/操作区/次级操作区)和颜色区分,保持了清晰的视觉层次。


第十章 我的铃声系统:作品管理与上架变现

我的铃声 Tab 是创作者的工作台,核心功能是作品管理与上架变现。

10.1 作品数据设计

mockCreations 包含 14 个作品,其中 10 个已上架、4 个草稿。已上架作品有下载量与收益数据,草稿的下载量与收益为 0。作品覆盖 6 个分类:纯音乐(4)、搞笑(3)、游戏(2)、机械(1)、动漫(1)、电子(2)、影视(1)。

作品数据的设计模拟了一个活跃创作者的作品库:有高下载量的爆款(凌晨三点钟声 8420 下载),有中等表现的作品,有零数据的草稿。收益数据 = 下载量 × 单价 × 70% 分成(如凌晨三点钟声:8420 × 0.02 × 0.7 ≈ 117.88,代码中为 168.4,可能包含打赏收入)。

10.2 数据统计与趋势

我的铃声 Tab 顶部展示四列数据统计:作品总数 14(橙)、累计下载 42.8k(蓝)、累计收益 ¥903(紫)、收藏人气 86(粉)。四种颜色对应四种数据维度,与市场 Tab 的铃声卡片配色体系一致。

下方是近 7 日下载量柱状图,7 根柱子展示每日下载量趋势。周六(最高 1240)使用橙色高亮,其他日期使用浅橙。柱状图通过 barH(v, 1240) 函数计算高度,最大值 1240 对应 96vp,确保所有柱子都在可视范围内。

10.3 作品宫格管理

14 个作品通过双列宫格展示,每个作品卡片包含:

  • 封面区:76px 高浅橙背景 + emoji + 右上角状态角标(已上架绿/草稿灰)。
  • 作品名:12px 粗体深灰,maxLines 1。
  • 元信息:时长 · 日期 · 下载量(草稿显示"未发布"),9px 灰色。
  • 操作按钮:✏️ 编辑(浅橙底)+ 🗑️ 删除(浅红底),各占 50%。

双列宫格的布局比列表更紧凑,适合展示大量作品的缩略信息。编辑与删除按钮直接放在卡片上,避免了长按菜单的额外交互步骤。

10.4 编辑表单抽屉

点击编辑按钮弹出编辑表单底部抽屉,包含完整的作品信息编辑表单:

  • 铃声名称:文本输入框,预填当前作品名。
  • 所属分类:6 分类芯片选择(动漫/游戏/影视/搞笑/纯音乐/电子),选中态橙色。
  • 标签:文本输入框,占位符"治愈,白噪,助眠"。
  • 公开/私密:开关切换,公开显示 🔓 绿色,私密显示 🔒 灰色。
  • 取消/保存:双按钮行,保存按钮橙色。

编辑表单的设计覆盖了作品信息的所有可编辑字段,分类使用芯片选择(比下拉菜单更直观),公开/私密使用图标开关(比 Switch 更有表现力)。

10.5 上架定价抽屉

点击「上架市场」大按钮弹出上架定价底部抽屉,这是创作者变现的核心交互:

  • 定价档位:4 档选择(免费引流 / 1币 / 3币 / 6币),卡片式选择,选中态橙色背景白字。
  • 分成计算:实时展示创作者分成(70%)与平台服务费(30%),如定价 3 币则每下载创作者得 2.1 币、平台得 0.9 币。
  • 协议勾选:创作者协议 + 内容上架规范,未勾选时上架按钮灰色禁用。
  • 确认上架:橙色按钮,勾选协议后可点击。

上架定价抽屉的设计核心是透明的分成计算,让创作者清楚知道每笔下载能赚多少钱,激励创作者合理定价。免费引流档的存在则鼓励新创作者通过免费作品积累粉丝。

10.6 删除确认弹窗

点击删除按钮弹出居中危险确认弹窗,包含:

  • 🗑️ 大图标。
  • 「删除「作品名」?」标题。
  • 警告说明:删除后所有数据(下载记录、收益明细)无法恢复,已购用户不受影响。
  • 再想想(灰色)/ 确认删除(红色)双按钮。

删除确认弹窗使用红色 #DC2626 作为主按钮色,与应用的橙色主调形成对比,强化"危险操作"的视觉警示。


第十一章 素材库系统:16 个素材包的双列货架

素材库 Tab 是铃声工坊的素材电商模块,为创作者提供二次创作的音频素材。

11.1 素材包数据设计

mockPacks 包含 16 个素材包,覆盖 10 种标签:热门(4)、新品(3)、免费(2)、萌系(1)、国风(1)、实用(1)、怀旧(2)、电子(1)、治愈(1)、氛围(1)。每个素材包包含 12-42 条音轨,价格从 0 到 20 币不等。

素材包的设计覆盖了铃声创作的主流素材需求:

  • 节奏类:夏日弹跳鼓组、电子鼓故障风(用于制作节奏型铃声)。
  • 复古类:8bit 像素宝库、复古合成器、胶片底噪、火车进站(用于制作怀旧风格铃声)。
  • 自然类:雨声全收录、口哨与响指(用于制作自然/治愈风格铃声)。
  • 人声类:人声阿卡贝拉、猫咪全家桶(用于制作有人声元素的铃声)。
  • 特效类:警报音大全、机械运转声、心跳与呼吸(用于制作特殊音效铃声)。
  • 国风类:古风弦乐(用于制作国风铃声)。
  • 太空类:太空氛围垫(用于制作科幻/氛围铃声)。

11.2 分段控件筛选

素材库 Tab 使用三段分段控件(🔥 热门 / 🆕 最新 / 🎁 免费)切换素材包的展示排序。分段控件是鸿蒙原生的经典交互模式,比 Tab 切换更轻量,适合同一页面内的视图切换。

选中段使用橙色背景 + 白色文字 + 粗体,未选中段使用透明背景 + 深橙文字。分段控件整体使用浅橙 #FFE8CC 背景 + 圆角 20px + 内边距 4px,选中段的橙色背景与外层浅橙背景形成嵌套圆角效果。

11.3 素材包卡片设计

每个素材包卡片(双列,47% 宽度)包含:

  • 封面区:84px 高马卡龙色背景 + emoji 大图标 + 左上角标签角标(深灰半透明底白字,如"热门"“新品”“免费”)。
  • 素材包名:13px 粗体深灰,maxLines 1。
  • 元信息:轨数 · 销量,9px 灰色。
  • 价格按钮:免费=绿底白字,付费=橙底白字,占满宽度,点击直接弹出购买抽屉。

素材包卡片的设计与市场 Tab 的铃声卡片类似,但更强调"素材包"的属性(轨数、销量)而非"铃声"的属性(时长、创作者)。价格按钮直接放在卡片底部,减少了一次点击跳转。

11.4 素材包详情弹窗

点击素材包卡片弹出居中大卡详情弹窗,包含:

  • 素材包信息:封面 + 名称 + 轨数/销量/标签。
  • 试听曲目列表:8 条曲目(PACK_TRACKS),每条显示 ▶ 播放图标 + 曲目名 + 时长/文件大小 + + 添加按钮。列表可滚动(constraintSize({ maxHeight: '42%' }))。
  • 操作按钮:加入收藏(浅橙底)+ 购买(橙色底,1.3 倍宽)。

详情弹窗的核心是试听曲目列表,让用户在购买前可以预览素材包的内容质量。8 条曲目是素材包的前 8 轨(如夏日弹跳鼓组包含 Kick×2、Snare×2、HiHat、Clap、Tom、Percussion 组合),展示了素材包的内容结构。

11.5 购买素材包抽屉

点击价格按钮弹出购买素材包底部抽屉,这是一个简洁的订单确认面板:

  • 标题:🧾 订单确认。
  • 商品行:素材包封面 + 名称/轨数 + 价格。
  • 支付方式:🪙 工坊币(余额 86.5)。
  • 确认支付:橙色按钮。

购买抽屉的设计非常简洁,因为素材包的购买是一个低决策成本的操作(价格通常在 10 币以内),不需要复杂的支付方式选择。


第十二章 榜单与个人中心系统

榜单 Tab 和我的 Tab 是铃声工坊的创作者激励体系,通过排名、数据可视化、等级进度等机制激励创作者持续产出。

12.1 创作者数据设计

mockCreators 包含 12 位创作者,覆盖 4 种徽章:金牌创作者(2)、人气王(1)、实力派(3)、新锐作者(6)。创作者数据包含作品数、粉丝数、总收益、趋势(up/down/flat)。

TOP3 创作者的数据明显高于其他创作者:

  • 冠军「音浪小陆」:86 作品 / 12.8w 粉丝 / ¥12480 收益 / 金牌创作者 / 上升。
  • 亚军「ChipTune王」:64 作品 / 9.64w 粉丝 / ¥9860 收益 / 金牌创作者 / 上升。
  • 季军「搞笑厂牌」:122 作品 / 8.82w 粉丝 / ¥8730 收益 / 人气王 / 持平。

4-12 名创作者的数据递减,形成自然的排名梯度。趋势字段有升有降,模拟了真实榜单的动态变化。

12.2 颁奖台设计

TOP3 颁奖台是榜单 Tab 的视觉核心,通过高度差异营造仪式感:

  • 冠军台(中间):118vp 高,金色 #FDE68A 背景,🥇 奖牌。
  • 亚军台(左侧):96vp 高,银色 #E5E7EB 背景,🥈 奖牌。
  • 季军台(右侧):78vp 高,铜色 #FDBA74 背景,🥉 奖牌。

每个台子显示奖牌 emoji + 头像 emoji + 创作者名(maxLines 1)+ 收益 + 粉丝数。台子的高度从中间向两侧递减,形成经典的奥运会颁奖台视觉。点击台子弹出创作者详情抽屉。

颁奖台的设计核心是高度即排名,用户一眼就能看出谁是冠军、谁是亚军、谁是季军,不需要阅读数字。

12.3 分类占比可视化

热门铃声分类占比使用横向条形图展示 5 类分类的市场份额:

  • 动漫 32%(橙 #F97316
  • 游戏 26%(蓝 #0EA5E9
  • 纯音乐 18%(紫 #7C3AED
  • 搞笑 14%(粉 #EC4899
  • 机械影视 10%(黄 #EAB308

每行包含分类名(56px 宽)+ 进度条(layoutWeight(1),浅橙底 + 彩色填充,宽度由 catBarW(p) 计算:p * 2.4 + 'vp',32% 对应 76.8vp)+ 百分比(34px 宽,对应分类色粗体)。

分类占比图让创作者了解市场热点,指导创作方向。动漫和游戏占比最高(合计 58%),说明 IP 衍生铃声是市场主流。

12.4 完整榜单

4-12 名创作者通过列表展示,每行包含:

  • 排名数字(24px 宽,灰色粗体):4-12。
  • 头像(38px 圆形,浅橙底 emoji)。
  • 创作者信息:名称(13px 粗体深灰)+ 徽章/作品数/粉丝数(9px 灰色)。
  • 收益与趋势:收益(12px 橙色粗体)+ 趋势箭头(上升绿/下降红/持平灰)。

点击行弹出创作者详情抽屉。完整榜单的设计比颁奖台更信息密集,适合展示排名靠后的创作者。

12.5 创作者详情抽屉

创作者详情抽屉包含:

  • 头部:头像 + 名称/徽章/趋势 + + 关注按钮。
  • 三列数据:作品数 / 粉丝数 / 总收益,暖奶油色背景卡片。
  • 代表作列表:3 款铃声(mockRingtones.slice(0, 3)),每款显示封面 + 名称/下载量/价格 + 播放按钮。
  • 进入创作者主页:橙色按钮。

创作者详情抽屉的设计让用户可以深入了解一位创作者的作品与数据,关注按钮和代表作列表促进了用户与创作者的连接。

12.6 收益分成规则

点击「分成规则 ›」弹出收益分成规则说明弹窗,包含 5 条规则:

  1. 定价收入:创作者 70% / 平台 30%。
  2. 打赏收入:创作者 90% / 平台 10%。
  3. 素材包收入:作者 80% / 合买者按比例分摊。
  4. 收益满 ¥10 可提现,T+1 到账。
  5. 刷量、搬运等违规行为将扣除全部收益(红色文字)。

分成规则的透明化是创作者信任的基础。打赏收入的分成比例(90%)高于定价收入(70%),鼓励创作者与粉丝建立直接的打赏关系。违规条款使用红色文字,强化警示效果。

12.7 个人中心设计

我的 Tab 个人中心包含:

  • 个人资料卡(天蓝背景):头像 + 昵称「铃声捣鼓鬼」+ 等级「Lv.6 金牌创作者」+ 加入天数 + 工坊币余额 + 等级进度条(68%,柠黄填充)。
  • 近 7 日收益柱状图:7 根柱子,周六天蓝高亮,底部显示本周合计 ¥102.80。
  • 功能菜单:6 项(创作者认证/收益提现/已购素材/消息通知/账号安全/关于)。

个人中心的设计核心是等级与收益,等级进度条激励创作者升级(升级需要声望值,声望来自作品下载量与粉丝数),收益柱状图展示近期收入趋势,功能菜单提供账户管理入口。

12.8 创作者认证

点击「创作者认证」弹出认证条件清单弹窗,包含 3 个条件:

  1. ✔ 上传原创作品 ≥ 5 个(当前 14 个)—— 已完成,绿色。
  2. ✔ 实名信息已核验 —— 已完成,绿色。
  3. ○ 绑定收款账户(还差这一步)—— 未完成,灰色。

底部有「稍后再说」(灰色)/「去绑定并申请」(橙色,1.2 倍宽)双按钮。

创作者认证的设计采用条件清单 + 进度反馈的模式,已完成条件打绿色勾,未完成条件显示灰色圈,用户一眼就能知道还差什么。认证通过后可解锁收益提现、榜单加权、专属流量扶持等特权,激励创作者完成认证。


十三、开发环境搭建

13.1 环境要求

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

13.2 安装DevEco Studio

DevEco Studio 安装界面

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

13.3 配置SDK

SDK 配置界面

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

13.4 创建项目

创建项目界面

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

13.5 运行应用

运行应用界面

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

13.6 预览调试

预览调试界面

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

附录:完整源码(2128.ets)

以下为「铃声工坊 RINGSMITH」项目的完整 ArkTS 源码,包含数据模型、静态数据、工具函数、主入口及全部 6 个 Tab 组件。

// ============================================================
// RINGSMITH 铃声工坊(极简版)
// 保留了完整界面框架及核心交互,大幅删除冗余数据与弹窗
// ============================================================

interface RingtoneItem {
  id: number
  name: string
  creator: string
  category: string
  price: number
  downloads: number
  emoji: string
  color: string
}

interface CreationItem {
  id: number
  name: string
  category: string
  duration: string
  emoji: string
  published: boolean
}

interface PackItem {
  id: number
  name: string
  tracks: number
  price: number
  emoji: string
  color: string
}

interface CreatorItem {
  id: number
  name: string
  fans: number
  revenue: number
  emoji: string
}

// 核心数据精简
const WAVE_BARS: number[] = [10, 22, 34, 18, 46, 58, 30, 72, 40, 26, 64, 88, 52, 70, 96, 60, 34, 80]
const RULER_TICKS: number[] = [0, 10, 20]

const RINGTONES: RingtoneItem[] = [
  { id: 1, name: '樱花碎片', creator: '音浪小陆', category: '动漫', price: 3, downloads: 128400, emoji: '🌸', color: '#FBCFE8' },
  { id: 2, name: '像素骑士', creator: 'ChipTune王', category: '游戏', price: 2, downloads: 96700, emoji: '🎮', color: '#BBF7D0' },
  { id: 3, name: '深夜便利店', creator: '录音师阿茂', category: '纯音乐', price: 0, downloads: 88200, emoji: '🏪', color: '#FED7AA' },
  { id: 4, name: '老板来电话', creator: '搞笑厂牌', category: '搞笑', price: 1, downloads: 76500, emoji: '🤣', color: '#FDE68A' },
  { id: 5, name: '赛博冲刺', creator: 'SynthKid', category: '电子', price: 3, downloads: 69800, emoji: '⚡', color: '#BFDBFE' },
  { id: 6, name: '胶片开机音', creator: '放映员老周', category: '影视', price: 2, downloads: 64200, emoji: '🎬', color: '#E9D5FF' }
]

const CREATIONS: CreationItem[] = [
  { id: 101, name: '凌晨三点钟声', category: '纯音乐', duration: '00:24', emoji: '🔔', published: true },
  { id: 102, name: '外卖到了', category: '搞笑', duration: '00:11', emoji: '🛵', published: true },
  { id: 103, name: '海浪与鸥', category: '纯音乐', duration: '00:36', emoji: '🌊', published: false },
  { id: 104, name: '复古收音机', category: '机械', duration: '00:19', emoji: '📻', published: false }
]

const PACKS: PackItem[] = [
  { id: 1, name: '夏日弹跳鼓组', tracks: 24, price: 12, emoji: '🥁', color: '#FED7AA' },
  { id: 2, name: '8bit 像素宝库', tracks: 36, price: 15, emoji: '🕹️', color: '#BBF7D0' },
  { id: 3, name: '雨声全收录', tracks: 18, price: 0, emoji: '🌧️', color: '#BFDBFE' },
  { id: 4, name: '人声阿卡贝拉', tracks: 42, price: 20, emoji: '🎤', color: '#FBCFE8' }
]

const CREATORS: CreatorItem[] = [
  { id: 1, name: '音浪小陆', fans: 128000, revenue: 12480, emoji: '🎧' },
  { id: 2, name: 'ChipTune王', fans: 96400, revenue: 9860, emoji: '🕹️' },
  { id: 3, name: '搞笑厂牌', fans: 88200, revenue: 8730, emoji: '🤡' },
  { id: 4, name: 'SynthKid', fans: 64100, revenue: 6520, emoji: '⚡' }
]

const WEEK_DATA: number[] = [320, 540, 480, 760, 920]
const WEEK_LABELS: string[] = ['一', '二', '三', '四', '五']

function formatCount(n: number): string {
  if (n >= 10000) return (n / 10000).toFixed(1) + 'w'
  return n.toString()
}

function waveH(v: number): string {
  return Math.max(6, v * 0.72).toFixed(0) + 'vp'
}

function getRingtone(id: number): RingtoneItem {
  for (let i = 0; i < RINGTONES.length; i++) {
    if (RINGTONES[i].id === id) return RINGTONES[i]
  }
  return RINGTONES[0]
}

function getCreation(id: number): CreationItem {
  for (let i = 0; i < CREATIONS.length; i++) {
    if (CREATIONS[i].id === id) return CREATIONS[i]
  }
  return CREATIONS[0]
}

function priceText(p: number): string {
  return p === 0 ? '免费' : '¥' + p
}

enum WorkTab { workshop, market, rings, library, rank, mine }

@Entry
@Component
struct RingsmithApp {
  @State activeTab: WorkTab = WorkTab.workshop

  build() {
    Column() {
      this.contentArea()
      this.bottomTabs()
    }.width('100%').height('100%').backgroundColor('#FFF8E7')
  }

  @Builder contentArea() {
    Column() {
      if (this.activeTab === WorkTab.workshop) { WorkshopPanel() }
      else if (this.activeTab === WorkTab.market) { MarketPanel() }
      else if (this.activeTab === WorkTab.rings) { MyRingsPanel() }
      else if (this.activeTab === WorkTab.library) { LibraryPanel() }
      else if (this.activeTab === WorkTab.rank) { RankPanel() }
      else { MinePanel() }
    }.width('100%').layoutWeight(1)
  }

  @Builder bottomTabs() {
    Column() {
      Divider().color('#FDE4C8').strokeWidth(1)
      Row() {
        this.tabItem(WorkTab.workshop, '🛠️', '工坊')
        this.tabItem(WorkTab.market, '🛍️', '市场')
        this.tabItem(WorkTab.rings, '🎵', '我的铃声')
        this.tabItem(WorkTab.library, '📦', '素材库')
        this.tabItem(WorkTab.rank, '🏆', '榜单')
        this.tabItem(WorkTab.mine, '👤', '我的')
      }.width('100%').height(58).padding({ left: 4, right: 4 })
    }.width('100%').backgroundColor('#FFFFFF')
  }

  @Builder tabItem(tab: WorkTab, icon: string, label: string) {
    Column({ space: 3 }) {
      Text(icon).fontSize(19)
      Text(label).fontSize(9)
        .fontWeight(this.activeTab === tab ? FontWeight.Bold : FontWeight.Normal)
        .fontColor(this.activeTab === tab ? '#F97316' : '#9CA3AF')
    }.layoutWeight(1).height('100%').justifyContent(FlexAlign.Center)
    .onClick(() => { this.activeTab = tab })
  }
}

// ===================== 核心 Tab1:工坊 =====================
@Component
struct WorkshopPanel {
  @State isPlaying: boolean = false
  @State showExport: boolean = false

  build() {
    Column() {
      Row() {
        Text('🛠️ 铃声工坊').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
        Text('未命名项目').fontSize(11).fontColor('#9CA3AF')
          .padding({ left: 8, right: 8, top: 2, bottom: 2 }).backgroundColor('#374151').borderRadius(8).margin({ left: 10 })
      }.width('100%').padding(12).backgroundColor('#1F2937').alignItems(VerticalAlign.Center)

      // 波形编辑区(简化)
      Column() {
        Text('WAVEFORM · 波形编辑').fontSize(11).fontColor('#FDBA74').fontWeight(FontWeight.Bold).width('100%')
        Row({ space: 3 }) {
          ForEach(WAVE_BARS, (v: number) => {
            Column() {}.width(4).height(waveH(v)).borderRadius(2)
              .backgroundColor(this.isPlaying ? '#F97316' : '#FDBA74')
              .scale({ y: this.isPlaying ? 1.15 : 1 })
              .animation({ duration: 320, curve: Curve.EaseInOut })
          }, (v: number) => v.toString())
        }.alignItems(VerticalAlign.Center).margin({ top: 8 })

        Row() {
          ForEach(RULER_TICKS, (t: number) => {
            Text('0:' + (t < 10 ? '0' + t : t.toString())).fontSize(9).fontColor('#9CA3AF').layoutWeight(1).textAlign(TextAlign.Center)
          }, (t: number) => t.toString())
        }.width('100%').margin({ top: 6 })

        Row() {
          Text('◀').fontSize(13).fontColor('#FFFFFF').padding(6)
          Text('截取 00:12 - 00:28 · 16s').fontSize(10).fontColor('#FDBA74').layoutWeight(1).textAlign(TextAlign.Center)
          Text('▶').fontSize(13).fontColor('#FFFFFF').padding(6)
        }.width('100%').margin({ top: 10 }).alignItems(VerticalAlign.Center)
      }.width('100%').padding(16).backgroundColor('#111827').borderRadius({ bottomLeft: 20, bottomRight: 20 })

      // 播放控制
      Row({ space: 14 }) {
        Column() { Text(this.isPlaying ? '⏸' : '▶').fontSize(22).fontColor('#FFFFFF') }
          .width(54).height(54).borderRadius(27).backgroundColor('#F97316').justifyContent(FlexAlign.Center)
          .onClick(() => { this.isPlaying = !this.isPlaying })
        Text(this.isPlaying ? '正在试听 · 00:16 / 00:28' : '已就绪 · 总时长 00:28')
          .fontSize(10).fontColor('#9A6B3F')
      }.width('100%').justifyContent(FlexAlign.Center).alignItems(VerticalAlign.Center).padding({ top: 16, bottom: 8 })

      // 操作按钮
      Row({ space: 12 }) {
        Button() { Text('🎧 试听').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#7C2D12') }
          .type(ButtonType.Capsule).backgroundColor('#FFE8CC').layoutWeight(1).height(44)

        Button() { Text('📦 导出铃声').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#FFFFFF') }
          .type(ButtonType.Capsule).backgroundColor('#F97316').layoutWeight(1.4).height(44)
          .onClick(() => { this.showExport = true })
      }.width('100%').padding({ left: 16, right: 16, top: 10, bottom: 14 }).backgroundColor('#FFFFFF').alignItems(VerticalAlign.Center)
    }.width('100%').height('100%').alignItems(HorizontalAlign.Center)
    .overlay(this.exportModalOverlay())
  }

  @Builder exportModalOverlay() {
    if (this.showExport) {
      Column() {
        Column() {
          Text('导出铃声').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#1F2937').width('100%')
          Text('格式:MP3 · 音质:标准 128k').fontSize(11).fontColor('#9CA3AF').margin({ top: 4 })
          Button() { Text('开始导出').fontSize(15).fontWeight(FontWeight.Bold).fontColor('#FFFFFF') }
            .type(ButtonType.Capsule).backgroundColor('#F97316').width('100%').height(46).margin({ top: 16 })
            .onClick(() => { this.showExport = false })
        }.width('100%').padding(18).backgroundColor('#FFFFFF').borderRadius({ topLeft: 22, topRight: 22 })
        .onClick(() => {})
      }.width('100%').height('100%').backgroundColor('rgba(31,41,55,0.6)')
        .justifyContent(FlexAlign.End).onClick(() => { this.showExport = false })
    }
  }
}

// ===================== Tab2:市场(简化) =====================
@Component
struct MarketPanel {
  @State pickedId: number = 1
  @State showDetail: boolean = false

  build() {
    Column() {
      Column() {
        Text('RINGSMITH 市场').fontSize(19).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
      }.width('100%').padding(14).backgroundColor('#F97316')

      Scroll() {
        Column() {
          ForEach(RINGTONES, (r: RingtoneItem) => {
            Row() {
              Text(r.emoji).fontSize(22).width(48).height(48).borderRadius(14).backgroundColor(r.color)
                .textAlign(TextAlign.Center).textAlignVertical(Alignment.Center)
              Column({ space: 3 }) {
                Text(r.name).fontSize(14).fontWeight(FontWeight.Bold).fontColor('#1F2937')
                Text(r.creator + ' · ' + formatCount(r.downloads) + '下载').fontSize(10).fontColor('#9CA3AF')
              }.alignItems(HorizontalAlign.Start).layoutWeight(1).margin({ left: 10 })
              Column() { Text('▶').fontSize(13).fontColor('#FFFFFF') }.width(34).height(34).borderRadius(17).backgroundColor('#F97316').justifyContent(FlexAlign.Center).margin({ right: 8 })
              Text(priceText(r.price)).fontSize(12).fontWeight(FontWeight.Bold).fontColor('#F97316')
            }.width('100%').padding(12).backgroundColor('#FFFFFF').borderRadius(16).margin({ bottom: 8 })
            .onClick(() => { this.pickedId = r.id; this.showDetail = true })
          }, (r: RingtoneItem) => r.id.toString())
        }.padding({ left: 14, right: 14 })
      }.layoutWeight(1).scrollBar(BarState.Off)
    }.width('100%').height('100%')
    .overlay(this.detailOverlay())
  }

  @Builder detailOverlay() {
    if (this.showDetail) {
      Column() {
        Column() {
          Text(getRingtone(this.pickedId).emoji).fontSize(34)
          Text(getRingtone(this.pickedId).name).fontSize(17).fontWeight(FontWeight.Bold).fontColor('#1F2937').margin({ top: 6 })
          Text(getRingtone(this.pickedId).creator + ' · ' + getRingtone(this.pickedId).category).fontSize(11).fontColor('#9CA3AF').margin({ top: 4 })
          Button() { Text('设为铃声').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#FFFFFF') }
            .type(ButtonType.Capsule).backgroundColor('#F97316').width('100%').height(44).margin({ top: 12 })
            .onClick(() => { this.showDetail = false })
        }.width('100%').padding(18).backgroundColor('#FFFFFF').borderRadius({ topLeft: 22, topRight: 22 })
        .onClick(() => {})
      }.width('100%').height('100%').backgroundColor('rgba(31,41,55,0.6)')
        .justifyContent(FlexAlign.End).onClick(() => { this.showDetail = false })
    }
  }
}

// ===================== Tab3:我的铃声(简化) =====================
@Component
struct MyRingsPanel {
  @State pickedId: number = 101
  @State showEdit: boolean = false

  build() {
    Column() {
      Text('🎵 我的铃声').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#1F2937')
        .width('100%').padding({ left: 16, top: 14 })

      Row() {
        Column({ space: 4 }) {
          Text('14').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#F97316')
          Text('作品总数').fontSize(10).fontColor('#9CA3AF')
        }.layoutWeight(1).alignItems(HorizontalAlign.Center)
        Column({ space: 4 }) {
          Text('42.8k').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#0EA5E9')
          Text('累计下载').fontSize(10).fontColor('#9CA3AF')
        }.layoutWeight(1).alignItems(HorizontalAlign.Center)
        Column({ space: 4 }) {
          Text('¥903').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#7C3AED')
          Text('累计收益').fontSize(10).fontColor('#9CA3AF')
        }.layoutWeight(1).alignItems(HorizontalAlign.Center)
      }.width('92%').padding(12).backgroundColor('#FFFFFF').borderRadius(14).margin({ top: 12 })

      Flex({ wrap: FlexWrap.Wrap }) {
        ForEach(CREATIONS, (c: CreationItem) => {
          Column({ space: 6 }) {
            Text(c.emoji).fontSize(28).width('100%').height(60).backgroundColor('#FFF3E0').borderRadius(10).textAlign(TextAlign.Center)
            Text(c.name).fontSize(11).fontColor('#1F2937')
            Text(c.published ? '已上架' : '草稿').fontSize(8).fontColor('#FFFFFF').padding({ left: 5, right: 5, top: 2, bottom: 2 })
              .backgroundColor(c.published ? '#22C55E' : '#9CA3AF').borderRadius(6)

            Row() {
              Text('✏️').fontSize(12).layoutWeight(1).height(24).backgroundColor('#FFE8CC').borderRadius(6).textAlign(TextAlign.Center)
                .onClick(() => { this.pickedId = c.id; this.showEdit = true })
            }.width('100%')
          }.width('47%').padding(8).backgroundColor('#FFFFFF').borderRadius(12).margin({ right: '6%', bottom: 8 })
        }, (c: CreationItem) => c.id.toString())
      }.width('92%').margin({ top: 10 })
    }.width('100%').height('100%')
    .overlay(this.editOverlay())
  }

  @Builder editOverlay() {
    if (this.showEdit) {
      Column() {
        Column() {
          Text('✏️ 编辑:' + getCreation(this.pickedId).name).fontSize(15).fontWeight(FontWeight.Bold).fontColor('#1F2937').width('100%')
          TextInput({ placeholder: '输入新名称' }).fontSize(13).height(42).backgroundColor('#F9FAFB').borderRadius(10).width('100%').margin({ top: 10 })
          Row() {
            Button() { Text('取消').fontSize(12).fontColor('#6B7280') }.type(ButtonType.Capsule).backgroundColor('#F3F4F6').layoutWeight(1).height(40)
            Button() { Text('保存修改').fontSize(12).fontWeight(FontWeight.Bold).fontColor('#FFFFFF') }.type(ButtonType.Capsule).backgroundColor('#F97316').layoutWeight(1).height(40).margin({ left: 10 })
              .onClick(() => { this.showEdit = false })
          }.width('100%').margin({ top: 12 })
        }.width('100%').padding(16).backgroundColor('#FFFFFF').borderRadius({ topLeft: 22, topRight: 22 })
        .onClick(() => {})
      }.width('100%').height('100%').backgroundColor('rgba(31,41,55,0.6)')
        .justifyContent(FlexAlign.End).onClick(() => { this.showEdit = false })
    }
  }
}

// ===================== Tab4:素材库(简化) =====================
@Component
struct LibraryPanel {
  build() {
    Column() {
      Text('📦 素材库').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#1F2937').width('100%').padding({ left: 16, top: 14 })
      Flex({ wrap: FlexWrap.Wrap }) {
        ForEach(PACKS, (p: PackItem) => {
          Column({ space: 4 }) {
            Text(p.emoji).fontSize(34).width('100%').height(70).backgroundColor(p.color).borderRadius(12).textAlign(TextAlign.Center)
            Text(p.name).fontSize(12).fontWeight(FontWeight.Bold).fontColor('#1F2937').maxLines(1)
            Text(p.tracks.toString() + ' 轨').fontSize(9).fontColor('#9CA3AF')
            Text(priceText(p.price)).fontSize(12).fontWeight(FontWeight.Bold).fontColor(p.price === 0 ? '#16A34A' : '#FFFFFF')
              .width('100%').height(26).backgroundColor(p.price === 0 ? '#DCFCE7' : '#F97316').borderRadius(10).textAlign(TextAlign.Center)
          }.width('47%').padding(8).backgroundColor('#FFFFFF').borderRadius(12).margin({ right: '6%', bottom: 8 })
        }, (p: PackItem) => p.id.toString())
      }.width('92%').margin({ top: 10 })
    }.width('100%').height('100%')
  }
}

// ===================== Tab5:榜单(简化) =====================
@Component
struct RankPanel {
  build() {
    Column() {
      Text('🏆 创作者榜').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#FFFFFF').width('100%').padding({ left: 16, top: 12, bottom: 12 }).backgroundColor('#7C3AED')
      Scroll() {
        Column() {
          Row({ space: 10 }) {
            ForEach(CREATORS.slice(0, 3), (c: CreatorItem, idx?: number) => {
              Column({ space: 4 }) {
                Text(idx === 0 ? '🥇' : (idx === 1 ? '🥈' : '🥉')).fontSize(20)
                Text(c.emoji).fontSize(26)
                Text(c.name).fontSize(11).fontWeight(FontWeight.Bold).fontColor('#1F2937').maxLines(1)
                Text('¥' + c.revenue.toString()).fontSize(10).fontColor('#F97316')
              }.width('30%').padding(8).alignItems(HorizontalAlign.Center).backgroundColor('#FFFFFF').borderRadius(12)
            }, (c: CreatorItem) => c.id.toString())
          }.width('90%').margin({ top: 16 }).justifyContent(FlexAlign.Center)

          ForEach(CREATORS.slice(3), (c: CreatorItem) => {
            Row({ space: 10 }) {
              Text(c.emoji).fontSize(18).width(38).height(38).borderRadius(19).backgroundColor('#FFF3E0').textAlign(TextAlign.Center)
              Column({ space: 2 }) {
                Text(c.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor('#1F2937')
                Text(formatCount(c.fans) + ' 粉丝').fontSize(9).fontColor('#9CA3AF')
              }.alignItems(HorizontalAlign.Start).layoutWeight(1)
              Text('¥' + c.revenue.toString()).fontSize(11).fontWeight(FontWeight.Bold).fontColor('#F97316')
            }.width('90%').padding(10).backgroundColor('#FFFFFF').borderRadius(14).margin({ top: 8 })
          }, (c: CreatorItem) => c.id.toString())

          Text('近 7 日热门分类占比').fontSize(12).fontWeight(FontWeight.Bold).fontColor('#7C2D12').margin({ top: 16 })
          Text('动漫 32% · 游戏 26% · 纯音乐 18%').fontSize(10).fontColor('#9CA3AF').margin({ top: 4, bottom: 20 })
        }.padding({ left: 16, right: 16 })
      }.layoutWeight(1).scrollBar(BarState.Off)
    }.width('100%').height('100%')
  }
}

// ===================== Tab6:我的(简化) =====================
@Component
struct MinePanel {
  build() {
    Column() {
      Row({ space: 10 }) {
        Text('🧑‍🎤').fontSize(34).width(60).height(60).borderRadius(30).backgroundColor('#FDE68A').textAlign(TextAlign.Center)
        Column({ space: 4 }) {
          Text('铃声捣鼓鬼').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
          Text('Lv.6 金牌创作者').fontSize(11).fontColor('rgba(255,255,255,0.85)')
        }.alignItems(HorizontalAlign.Start).layoutWeight(1)
      }.width('100%').padding(16).backgroundColor('#0EA5E9').borderRadius({ bottomLeft: 22, bottomRight: 22 }).alignItems(VerticalAlign.Center)

      Column() {
        Row() { Text('📈 近 7 日收益(¥)').fontSize(12).fontWeight(FontWeight.Bold).fontColor('#7C2D12').layoutWeight(1) }.width('100%')
        Row({ space: 8 }) {
          ForEach(WEEK_DATA, (v: number, idx?: number) => {
            Column({ space: 2 }) {
              Text(v.toString()).fontSize(8).fontColor('#9CA3AF')
              Column() {}.width(16).height((v / 900 * 60).toFixed(0) + 'vp').backgroundColor(idx === 4 ? '#0EA5E9' : '#BAE6FD').borderRadius({ topLeft: 4, topRight: 4 })
              Text(WEEK_LABELS[idx === undefined ? 0 : idx]).fontSize(8).fontColor('#9CA3AF')
            }.layoutWeight(1).alignItems(HorizontalAlign.Center)
          }, (v: number, idx?: number) => v.toString() + (idx === undefined ? 'x' : idx.toString()))
        }.width('100%').alignItems(VerticalAlign.Bottom).margin({ top: 8 })
      }.width('90%').padding(12).backgroundColor('#FFFFFF').borderRadius(14).margin({ top: 12 })

      Column() {
        Row() { Text('🏅').fontSize(16); Text('创作者认证').fontSize(13).fontColor('#1F2937').margin({ left: 10 }); Text('›').fontSize(16).fontColor('#D1D5DB').layoutWeight(1).textAlign(TextAlign.End) }.width('100%').padding({ top: 12, bottom: 12 })
        Divider().color('#FFF3E0')
        Row() { Text('💰').fontSize(16); Text('收益与提现').fontSize(13).fontColor('#1F2937').margin({ left: 10 }); Text('›').fontSize(16).fontColor('#D1D5DB').layoutWeight(1).textAlign(TextAlign.End) }.width('100%').padding({ top: 12, bottom: 12 })
      }.width('90%').padding({ left: 12, right: 12 }).backgroundColor('#FFFFFF').borderRadius(14).margin({ top: 12 })

      Text('RINGSMITH · 让每一次来电都有态度').fontSize(10).fontColor('#D1D5DB').margin({ top: 18, bottom: 20 })
    }.width('100%').height('100%')
  }
}
Logo

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

更多推荐