HarmonyOS ArkTS 实战:实现一个校园广播点歌与祝福应用

发布时间:2026/7/23 7:49:06
HarmonyOS ArkTS 实战:实现一个校园广播点歌与祝福应用 HarmonyOS ArkTS 实战从零实现一个校园广播点歌与祝福应用HarmonyOS ArkTS 实战从零实现一个校园广播点歌与祝福应用本文将带你使用 HarmonyOS ArkTS 从零搭建一个功能完整的校园广播点歌与祝福应用涵盖点歌队列、祝福墙、热门排行、主播互动等核心模块附完整可运行代码。目录一、项目背景与效果预览二、技术栈与环境准备三、项目结构设计四、数据模型设计五、页面布局与 UI 实现5.1 整体布局架构5.2 顶部播放栏组件5.3 统计数据面板5.4 点歌队列列表5.5 祝福墙瀑布流5.6 热门歌曲排行5.7 点歌弹窗表单六、核心功能实现详解6.1 点歌功能6.2 祝福发布与点赞6.3 队列状态流转6.4 音频播放模拟七、样式与交互动效八、完整代码九、运行与调试十、技术要点总结十一、扩展方向一、项目背景与效果预览校园广播是大学校园里一道独特的风景线——午餐时间的一首流行歌、毕业季的祝福、生日当天的专属点播……这些都是校园生活中温暖的记忆。本项目以校园广播点歌为场景使用 HarmonyOS ArkTS 开发一个集点歌、送祝福、队列管理、互动社交于一体的移动应用。核心功能一览功能模块说明 在线点歌输入歌曲名、歌手、送给谁、祝福语一键点歌 祝福墙瀑布流展示全校祝福支持点赞互动 点歌队列实时查看待播放/正在播放/已播放的歌曲队列 广播收听顶部常驻播放栏显示当前播放歌曲 热门排行按点赞数排行的热门歌曲榜单 数据统计今日点歌数、祝福数等统计面板 生日祝福专属生日祝福模板与标识 主播互动主播推荐、主播留言功能运行效果应用整体采用深玫红#9D174D为主色调搭配粉色渐变背景营造温馨、浪漫的校园氛围。顶部为常驻播放栏中部为功能区底部为 Tab 切换支持上下滑动浏览。二、技术栈与环境准备开发环境项目版本要求说明DevEco Studio5.0HarmonyOS 官方 IDEHarmonyOS SDKAPI 24基于 ArkTS 声明式开发范式运行设备HarmonyOS 4.0手机 / 平板均可为什么选择 ArkTS 声明式开发ArkTS 是 HarmonyOS 的主力应用开发语言在 TypeScript 的基础上扩展了声明式 UI 范式、状态管理、自定义组件等能力。相比传统命令式开发它有以下优势声明式 UI用build()方法描述界面长什么样框架自动处理渲染更新状态驱动State装饰器标记的变量变化时UI 自动刷新组件化Component装饰器支持自定义组件复用性强内置组件丰富List、Grid、Swiper等开箱即用三、项目结构设计entry/src/main/ ├── ets/ │ ├── pages/ │ │ └── Index.ets # 主页面单页应用所有功能在一个页面内 │ ├── model/ │ │ ├── SongRequest.ets # 点歌请求数据模型 │ │ └── Blessing.ets # 祝福数据模型 │ ├── components/ │ │ ├── PlayerBar.ets # 顶部播放栏组件 │ │ ├── SongCard.ets # 歌曲卡片组件 │ │ ├── BlessingCard.ets # 祝福卡片组件 │ │ └── RequestDialog.ets # 点歌弹窗组件 │ └── utils/ │ └── MockData.ets # 模拟数据 ├── resources/ │ ├── base/ │ │ ├── element/ # 颜色、尺寸等资源 │ │ └── media/ # 图片资源 │ └── rawfile/ # 音频资源可选 └── module.json5 # 模块配置设计说明为了降低学习门槛本项目采用单页面架构所有功能都在Index.ets中通过状态切换实现。对于更复杂的项目建议拆分为多个页面并使用 Navigation 路由管理。四、数据模型设计好的数据模型是应用的基石。我们需要两个核心实体点歌请求SongRequest和祝福Blessing。4.1 点歌请求模型/** * 点歌请求数据结构 * 描述一首被点播的歌曲及其相关信息 */ interface SongRequest { id: number; // 唯一标识 songName: string; // 歌曲名称 singer: string; // 歌手 dedicatee: string; // 送给谁点歌对象 message: string; // 祝福语 requester: string; // 点歌人 requestTime: string; // 点歌时间 playTime: string; // 播放时间待播放时为空 status: PlayStatus; // 播放状态 likes: number; // 点赞数 coverUrl?: string; // 封面图 URL可选 isBirthday?: boolean; // 是否为生日祝福可选 } /** * 播放状态枚举 * pending: 待播放 * playing: 正在播放 * played: 已播放 */ type PlayStatus pending | playing | played;4.2 祝福模型/** * 祝福墙数据结构 * 描述一条公开发布的祝福信息 */ interface Blessing { id: number; // 唯一标识 content: string; // 祝福内容 from: string; // 发送者 to: string; // 接收者 time: string; // 发布时间 likes: number; // 点赞数 type: BlessingType; // 祝福类型 color: string; // 卡片背景色用于瀑布流多样化 } /** * 祝福类型 * birthday: 生日祝福 * graduation: 毕业祝福 * encouragement: 鼓励祝福 * confession: 表白祝福 * other: 其他 */ type BlessingType birthday | graduation | encouragement | confession | other;4.3 为什么这样设计设计决策理由使用interface而非classArkTS 中纯数据结构用 interface 更轻量无需实例化status使用联合类型比 string 更安全编辑器有智能提示和类型检查时间字段用 string简化展示逻辑实际项目中建议用时间戳 格式化工具可选字段用?标记保持向后兼容新增字段不破坏旧数据五、页面布局与 UI 实现5.1 整体布局架构页面采用垂直滚动 分区布局的设计┌─────────────────────────┐ │ 顶部播放栏固定 │ ← PlayerBar ├─────────────────────────┤ │ │ │ 统计数据面板 │ ← StatsPanel │ │ ├─────────────────────────┤ │ │ │ 点歌队列区域 │ ← SongQueueList │ │ ├─────────────────────────┤ │ │ │ 祝福墙瀑布流 │ ← BlessingWall │ │ ├─────────────────────────┤ │ │ │ 热门歌曲排行 │ ← HotSongsRank │ │ └─────────────────────────┘ 悬浮点歌按钮 ← FAB核心布局代码骨架Entry Component struct Index { build() { Stack() { Scroll() { Column() { // 1. 顶部播放栏 PlayerBar({ currentSong: this.currentSong }) // 2. 统计数据面板 StatsPanel({ stats: this.stats }) // 3. 点歌队列 SectionTitle({ title: 点歌队列, subtitle: 共 ${this.requests.length} 首 }) SongQueueList({ requests: this.requests }) // 4. 祝福墙 SectionTitle({ title: 祝福墙, subtitle: 写下你的祝福 }) BlessingWall({ blessings: this.blessings }) // 5. 热门排行 SectionTitle({ title: 热门歌曲, subtitle: 本周 TOP 5 }) HotSongsRank({ songs: this.hotSongs }) } .width(100%) .padding({ bottom: 100 }) // 底部留白给悬浮按钮 } .width(100%) .height(100%) .backgroundColor(#FFF5F7) // 浅粉背景 // 悬浮点歌按钮 Button(点歌送祝福) .position({ x: 50%, y: 90% }) .translate({ x: -50% }) .width(180) .height(48) .borderRadius(24) .backgroundColor(#9D174D) .fontColor(#FFFFFF) .fontSize(16) .fontWeight(FontWeight.Bold) .shadow({ radius: 10, color: #9D174D, offsetY: 4 }) .onClick(() { this.showRequestDialog true; }) } .width(100%) .height(100%) } }5.2 顶部播放栏组件顶部播放栏是应用的视觉焦点展示当前正在播放的歌曲信息带有旋转动画效果。Component struct PlayerBar { Prop currentSong: SongRequest | null; State isPlaying: boolean true; build() { Row() { // 左侧封面 歌曲信息 Row() { // 旋转的封面图 Stack() { Circle({ width: 48, height: 48 }) .fill(#9D174D) .opacity(0.2) Image($r(app.media.music_note)) .width(24) .height(24) .fillColor(#9D174D) } .width(48) .height(48) .rotate({ angle: this.isPlaying ? 360 : 0, duration: 3000, iterations: -1, curve: Curve.Linear }) // 歌曲信息 Column() { Text(this.currentSong?.songName ?? 暂无播放) .fontSize(16) .fontWeight(FontWeight.Medium) .fontColor(#FFFFFF) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) Text(this.currentSong ? ${this.currentSong.singer} · 送给 ${this.currentSong.dedicatee} : 等待点歌...) .fontSize(12) .fontColor(#FFFFFF) .opacity(0.8) .margin({ top: 2 }) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) } .margin({ left: 12 }) .alignItems(HorizontalAlign.Start) .layoutWeight(1) } .layoutWeight(1) // 右侧播放控制按钮 Row() { Image(this.isPlaying ? $r(app.media.pause) : $r(app.media.play)) .width(24) .height(24) .fillColor(#FFFFFF) .onClick(() { this.isPlaying !this.isPlaying; }) } } .width(100%) .padding(16) .backgroundColor(#9D174D) .borderRadius(16) .margin({ top: 16, left: 16, right: 16 }) } }设计要点使用Stack叠加圆形背景和音乐图标模拟唱片效果rotate动画实现唱片旋转iterations: -1表示无限循环TextOverflow.Ellipsis处理长文本截断深浅搭配的文字层级主标题 100% 不透明副标题 80% 不透明5.3 统计数据面板用四个数据卡片展示关键指标增强数据感知。Component struct StatsPanel { Prop stats: StatsData; build() { Row() { this.StatItem(今日点歌, this.stats.todayRequests.toString(), #9D174D) this.StatItem(正在播放, this.stats.playingCount.toString(), #F59E0B) this.StatItem(待播放, this.stats.pendingCount.toString(), #3B82F6) this.StatItem(祝福数, this.stats.blessingCount.toString(), #10B981) } .width(100%) .padding(16) .justifyContent(FlexAlign.SpaceAround) } Builder StatItem(label: string, value: string, color: string) { Column() { Text(value) .fontSize(24) .fontWeight(FontWeight.Bold) .fontColor(color) Text(label) .fontSize(12) .fontColor(#666666) .margin({ top: 4 }) } .width(25%) .alignItems(HorizontalAlign.Center) } } interface StatsData { todayRequests: number; playingCount: number; pendingCount: number; blessingCount: number; }设计要点使用Builder封装重复的统计项减少代码冗余每个指标用不同颜色区分视觉上一目了然数字大、标签小的层级设计符合信息层级原则5.4 点歌队列列表点歌队列是核心功能之一展示所有点歌请求及其状态。Component struct SongQueueList { State requests: SongRequest[] []; State selectedTab: number 0; // 0: 全部 1: 待播放 2: 已播放 private get filteredRequests(): SongRequest[] { if (this.selectedTab 0) return this.requests; if (this.selectedTab 1) return this.requests.filter(r r.status pending || r.status playing); return this.requests.filter(r r.status played); } build() { Column() { // Tab 切换 Row() { this.TabItem(全部, 0) this.TabItem(待播放, 1) this.TabItem(已播放, 2) } .width(100%) .padding({ left: 16, right: 16, bottom: 12 }) // 队列列表 List({ space: 8 }) { ForEach(this.filteredRequests, (item: SongRequest, index: number) { ListItem() { this.SongItem(item, index) } }, (item: SongRequest) item.id.toString()) } .width(100%) .padding({ left: 16, right: 16 }) } } Builder TabItem(label: string, index: number) { Column() { Text(label) .fontSize(14) .fontColor(this.selectedTab index ? #9D174D : #999999) .fontWeight(this.selectedTab index ? FontWeight.Bold : FontWeight.Normal) // 底部指示条 if (this.selectedTab index) { Circle({ width: 6, height: 6 }) .fill(#9D174D) .margin({ top: 4 }) } } .width(33%) .alignItems(HorizontalAlign.Center) .onClick(() { this.selectedTab index; }) } Builder SongItem(item: SongRequest, index: number) { Row() { // 序号 Text((index 1).toString()) .fontSize(14) .fontColor(#999999) .width(24) // 歌曲信息 Column() { Row() { Text(item.songName) .fontSize(15) .fontWeight(FontWeight.Medium) .fontColor(#333333) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) .layoutWeight(1) // 状态标签 this.StatusBadge(item.status) } .width(100%) Text(${item.singer} · 送给 ${item.dedicatee}) .fontSize(12) .fontColor(#999999) .margin({ top: 4 }) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) if (item.message) { Text(${item.message}) .fontSize(12) .fontColor(#666666) .fontStyle(FontStyle.Italic) .margin({ top: 4 }) .maxLines(2) .textOverflow({ overflow: TextOverflow.Ellipsis }) } } .layoutWeight(1) .margin({ left: 12 }) .alignItems(HorizontalAlign.Start) // 点赞 Row() { Image($r(app.media.heart)) .width(16) .height(16) .fillColor(#9D174D) Text(item.likes.toString()) .fontSize(12) .fontColor(#9D174D) .margin({ left: 4 }) } } .width(100%) .padding(12) .backgroundColor(#FFFFFF) .borderRadius(12) .onClick(() { // 点击查看详情 }) } Builder StatusBadge(status: PlayStatus) { Row() { Text(status playing ? 播放中 : status pending ? 待播放 : 已播放) .fontSize(11) .fontColor(#FFFFFF) } .padding({ left: 8, right: 8, top: 2, bottom: 2 }) .borderRadius(10) .backgroundColor( status playing ? #F59E0B : status pending ? #3B82F6 : #10B981 ) } }设计要点使用ListForEach渲染列表性能优于ColumnForEach支持懒加载key参数第三个参数指定唯一标识提升列表更新性能Tab 切换通过selectedTab状态驱动配合get filteredRequests()计算属性过滤数据状态标签用不同颜色区分增强可读性5.5 祝福墙瀑布流祝福墙采用两列瀑布流布局每张卡片高度不一视觉上更活泼。Component struct BlessingWall { State blessings: Blessing[] []; // 将祝福分为左右两列 private get leftColumn(): Blessing[] { return this.blessings.filter((_, i) i % 2 0); } private get rightColumn(): Blessing[] { return this.blessings.filter((_, i) i % 2 1); } build() { Row() { // 左列 Column({ space: 8 }) { ForEach(this.leftColumn, (item: Blessing) { this.BlessingCard(item) }, (item: Blessing) item.id.toString()) } .layoutWeight(1) // 右列 Column({ space: 8 }) { ForEach(this.rightColumn, (item: Blessing) { this.BlessingCard(item) }, (item: Blessing) item.id.toString()) } .layoutWeight(1) } .width(100%) .padding({ left: 16, right: 16 }) } Builder BlessingCard(item: Blessing) { Column() { // 祝福类型标签 Row() { Text(this.getTypeLabel(item.type)) .fontSize(10) .fontColor(#FFFFFF) } .padding({ left: 8, right: 8, top: 3, bottom: 3 }) .borderRadius(8) .backgroundColor(item.color) .alignSelf(ItemAlign.Start) // 祝福内容 Text(item.content) .fontSize(14) .fontColor(#333333) .margin({ top: 8 }) .lineHeight(20) // 发送者信息 Row() { Text(—— ${item.from} 致 ${item.to}) .fontSize(11) .fontColor(#999999) .layoutWeight(1) // 点赞按钮 Row() { Image($r(app.media.heart_outline)) .width(14) .height(14) .fillColor(#9D174D) Text(item.likes.toString()) .fontSize(11) .fontColor(#9D174D) .margin({ left: 2 }) } } .width(100%) .margin({ top: 12 }) } .width(100%) .padding(12) .backgroundColor(#FFFFFF) .borderRadius(12) .onClick(() { // 点赞 const index this.blessings.findIndex(b b.id item.id); if (index ! -1) { this.blessings[index].likes; } }) } private getTypeLabel(type: BlessingType): string { const map: RecordBlessingType, string { birthday: 生日, graduation: 毕业, encouragement: 鼓励, confession: 表白, other: ✨ 祝福 }; return map[type] || ✨ 祝福; } }设计要点瀑布流通过两列Column分别渲染奇偶索引元素实现每张卡片有不同的背景色标签增加视觉多样性点击卡片即点赞交互简单直接lineHeight控制行间距提升长文本可读性5.6 热门歌曲排行展示本周点赞最多的 TOP 5 歌曲。Component struct HotSongsRank { State songs: SongRequest[] []; build() { Column({ space: 8 }) { ForEach(this.songs.slice(0, 5), (item: SongRequest, index: number) { Row() { // 排名 Stack() { Text((index 1).toString()) .fontSize(14) .fontWeight(FontWeight.Bold) .fontColor(index 3 ? #FFFFFF : #999999) } .width(28) .height(28) .borderRadius(14) .backgroundColor( index 0 ? #F59E0B : // 金 index 1 ? #9CA3AF : // 银 index 2 ? #D97706 : // 铜 #F3F4F6 // 灰 ) .justifyContent(FlexAlign.Center) // 歌曲信息 Column() { Text(item.songName) .fontSize(14) .fontColor(#333333) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) Text(item.singer) .fontSize(12) .fontColor(#999999) .margin({ top: 2 }) } .layoutWeight(1) .margin({ left: 12 }) .alignItems(HorizontalAlign.Start) // 点赞数 Row() { Image($r(app.media.heart_filled)) .width(14) .height(14) .fillColor(#9D174D) Text(item.likes.toString()) .fontSize(12) .fontColor(#9D174D) .margin({ left: 4 }) } } .width(100%) .padding(12) .backgroundColor(#FFFFFF) .borderRadius(12) }, (item: SongRequest) item.id.toString()) } .width(100%) .padding({ left: 16, right: 16 }) } }5.7 点歌弹窗表单点击悬浮按钮弹出点歌表单用户填写后提交。CustomDialog struct RequestDialog { controller: CustomDialogController; onConfirm: (songName: string, singer: string, dedicatee: string, message: string) void; State songName: string ; State singer: string ; State dedicatee: string ; State message: string ; build() { Column() { Text(点歌送祝福) .fontSize(18) .fontWeight(FontWeight.Bold) .fontColor(#333333) .margin({ bottom: 20 }) // 歌曲名称 Text(歌曲名称) .fontSize(14) .fontColor(#666666) .alignSelf(ItemAlign.Start) .margin({ bottom: 6 }) TextInput({ placeholder: 请输入歌曲名称, text: this.songName }) .height(44) .backgroundColor(#F5F5F5) .borderRadius(8) .padding({ left: 12, right: 12 }) .onChange((value: string) { this.songName value; }) // 歌手 Text(歌手) .fontSize(14) .fontColor(#666666) .alignSelf(ItemAlign.Start) .margin({ top: 16, bottom: 6 }) TextInput({ placeholder: 请输入歌手名, text: this.singer }) .height(44) .backgroundColor(#F5F5F5) .borderRadius(8) .padding({ left: 12, right: 12 }) .onChange((value: string) { this.singer value; }) // 送给谁 Text(送给谁) .fontSize(14) .fontColor(#666666) .alignSelf(ItemAlign.Start) .margin({ top: 16, bottom: 6 }) TextInput({ placeholder: 想把这首歌送给谁, text: this.dedicatee }) .height(44) .backgroundColor(#F5F5F5) .borderRadius(8) .padding({ left: 12, right: 12 }) .onChange((value: string) { this.dedicatee value; }) // 祝福语 Text(祝福语选填) .fontSize(14) .fontColor(#666666) .alignSelf(ItemAlign.Start) .margin({ top: 16, bottom: 6 }) TextArea({ placeholder: 写下你想说的话..., text: this.message }) .height(80) .backgroundColor(#F5F5F5) .borderRadius(8) .padding(12) .onChange((value: string) { this.message value; }) // 提交按钮 Button(确认点歌) .width(100%) .height(48) .margin({ top: 24 }) .borderRadius(24) .backgroundColor(#9D174D) .fontColor(#FFFFFF) .fontSize(16) .fontWeight(FontWeight.Bold) .enabled(this.songName.trim().length 0 this.singer.trim().length 0 this.dedicatee.trim().length 0) .opacity(this.songName.trim().length 0 this.singer.trim().length 0 this.dedicatee.trim().length 0 ? 1 : 0.5) .onClick(() { this.onConfirm(this.songName, this.singer, this.dedicatee, this.message); this.controller.close(); }) } .width(100%) .padding(24) .backgroundColor(#FFFFFF) .borderRadius(20) } }设计要点使用CustomDialog装饰器创建自定义弹窗enabledopacity实现表单校验必填项未填时按钮置灰不可点击TextArea用于多行文本输入输入框统一使用浅灰背景 圆角设计风格一致六、核心功能实现详解6.1 点歌功能点歌是应用的核心操作。用户填写表单后系统创建一个新的点歌请求并加入队列头部。/** * 提交点歌请求 * param songName 歌曲名称 * param singer 歌手 * param dedicatee 送给谁 * param message 祝福语 */ private requestSong(songName: string, singer: string, dedicatee: string, message: string): void { // 1. 校验输入 if (!songName.trim() || !singer.trim() || !dedicatee.trim()) { // 实际项目中应使用 Toast 提示 console.warn(请填写完整的点歌信息); return; } // 2. 创建新的点歌请求 const newRequest: SongRequest { id: this.nextRequestId, songName: songName.trim(), singer: singer.trim(), dedicatee: dedicatee.trim(), message: message.trim(), requester: 我, requestTime: this.formatDate(new Date()), playTime: , status: pending, likes: 0, isBirthday: message.includes(生日) }; // 3. 插入队列头部最新点歌排在最前 this.requests [newRequest, ...this.requests]; // 4. 更新 ID 计数器 this.nextRequestId; // 5. 更新统计数据 this.stats.todayRequests; this.stats.pendingCount; } /** * 格式化日期为本地字符串 */ private formatDate(date: Date): string { return date.toLocaleString(zh-CN, { year: numeric, month: 2-digit, day: 2-digit, hour: 2-digit, minute: 2-digit }); }关键设计输入校验前端先做一层校验减少无效请求队列头部插入最新点的歌排在最前面符合即时感生日自动识别通过关键词自动标记生日祝福6.2 祝福发布与点赞祝福墙支持发布祝福和点赞互动。/** * 发布新祝福 */ private publishBlessing(content: string, from: string, to: string, type: BlessingType): void { if (!content.trim()) return; const colors [#9D174D, #7C3AED, #2563EB, #059669, #D97706]; const newBlessing: Blessing { id: this.nextBlessingId, content: content.trim(), from: from || 匿名, to: to || 所有人, time: this.formatDate(new Date()), likes: 0, type, color: colors[Math.floor(Math.random() * colors.length)] }; this.blessings [newBlessing, ...this.blessings]; this.nextBlessingId; this.stats.blessingCount; } /** * 点赞祝福 */ private likeBlessing(blessingId: number): void { this.blessings this.blessings.map(b b.id blessingId ? { ...b, likes: b.likes 1 } : b ); }为什么用map而不是直接修改在 ArkTS 中State装饰的数组需要整体替换才能触发 UI 更新。直接修改数组元素如this.blessings[0].likes不会触发重新渲染。使用map返回新数组是最简洁的方式。6.3 队列状态流转歌曲在队列中有三种状态pending待播放→playing播放中→played已播放。/** * 播放下一首 * 将当前播放中的歌曲标记为已播放取下一首待播放的开始播放 */ private playNext(): void { // 1. 将当前播放中的标记为已播放 this.requests this.requests.map(r r.status playing ? { ...r, status: played as PlayStatus, playTime: this.formatDate(new Date()) } : r ); // 2. 找到第一首待播放的开始播放 const nextSong this.requests.find(r r.status pending); if (nextSong) { this.requests this.requests.map(r r.id nextSong.id ? { ...r, status: playing as PlayStatus, playTime: this.formatDate(new Date()) } : r ); this.currentSong nextSong; this.stats.playingCount 1; this.stats.pendingCount--; } else { this.currentSong null; this.stats.playingCount 0; } } /** * 模拟自动播放每 30 秒切一首 */ private startAutoPlay(): void { setInterval(() { this.playNext(); }, 30000); }6.4 音频播放模拟由于本项目聚焦 UI 与交互音频播放部分采用模拟实现。实际项目中可接入ohos.multimedia.audio或第三方音频 SDK。/** * 模拟播放进度 * 实际项目中应使用音频播放器的真实进度回调 */ State playProgress: number 0; private startProgressSimulation(): void { setInterval(() { if (this.isPlaying this.currentSong) { this.playProgress (this.playProgress 1) % 100; } }, 1000); }七、样式与交互动效7.1 主题色系统应用采用深玫红为主色调建立统一的颜色系统用途色值说明主色#9D174D深玫红用于按钮、标题、重点元素主色浅#FDF2F8浅粉用于背景、选中态辅助色-橙#F59E0B播放中状态辅助色-蓝#3B82F6待播放状态辅助色-绿#10B981已完成状态文字主色#333333正文文字次色#666666辅助信息文字弱色#999999次要信息背景色#FFF5F7页面背景卡片背景#FFFFFF卡片、输入框7.2 交互动效// 点赞动画 private animateLike(): void { this.isLiked true; animateTo({ duration: 300, curve: Curve.EaseOut, onFinish: () { this.isLiked false; } }, () { this.likeScale 1.3; }); }7.3 响应式适配使用百分比宽度和layoutWeight确保在不同屏幕尺寸下都有良好表现// 所有容器使用 width(100%) 自适应 // 使用 layoutWeight(1) 实现弹性布局 // 间距使用固定值16、12、8保证一致性八、完整代码由于篇幅限制以上展示了核心组件和逻辑。完整的Index.ets文件包含所有组件定义、状态管理和模拟数据可直接在 DevEco Studio 中运行。完整代码结构概览// Index.ets 完整结构 Entry Component struct Index { // 状态定义 State requests: SongRequest[] []; State blessings: Blessing[] []; State currentSong: SongRequest | null null; State stats: StatsData { ... }; State nextRequestId: number 30; State nextBlessingId: number 30; State showRequestDialog: boolean false; // 生命周期 aboutToAppear() { this.initMockData(); this.startAutoPlay(); } // 核心方法 private initMockData() { ... } private requestSong(...) { ... } private likeBlessing(...) { ... } private playNext() { ... } private startAutoPlay() { ... } // 页面构建 build() { Stack() { Scroll() { Column() { PlayerBar({ ... }) StatsPanel({ ... }) SectionTitle({ title: 点歌队列 }) SongQueueList({ ... }) SectionTitle({ title: 祝福墙 }) BlessingWall({ ... }) SectionTitle({ title: 热门歌曲 }) HotSongsRank({ ... }) } } // 悬浮按钮 Button(点歌送祝福) { ... } } } } // 子组件定义 Component struct PlayerBar { ... } Component struct StatsPanel { ... } Component struct SectionTitle { ... } Component struct SongQueueList { ... } Component struct BlessingWall { ... } Component struct HotSongsRank { ... } // 接口定义 interface SongRequest { ... } interface Blessing { ... } interface StatsData { ... } type PlayStatus ...; type BlessingType ...;九、运行与调试9.1 运行步骤打开 DevEco Studio新建 HarmonyOS 项目选择 Empty Ability将Index.ets替换为完整代码在resources/base/media中添加所需图标音乐图标、心形图标、播放/暂停图标等连接设备或启动模拟器点击运行按钮▶️9.2 常见问题Q: 运行后页面空白A: 检查Entry装饰器是否正确添加build()方法是否有返回内容。Q: 列表不更新A: 确保修改State数组时使用整体替换map、filter、展开运算符等不要直接修改元素属性。Q: 自定义弹窗不显示A: 检查CustomDialogController是否正确初始化CustomDialog装饰器是否添加。Q: 图片资源找不到A: 确认图片已放入resources/base/media目录文件名使用小写字母和下划线引用时用$r(app.media.xxx)。9.3 调试技巧使用console.log()输出状态变化观察数据流在 DevEco Studio 的 Previewer 中实时预览 UI 效果使用 DevTools 查看组件树和状态值十、技术要点总结通过本项目你将掌握以下 ArkTS 核心技能技术点应用场景重要程度State状态管理所有动态数据⭐⭐⭐⭐⭐Component自定义组件模块化 UI⭐⭐⭐⭐⭐Prop父子组件传值组件间数据传递⭐⭐⭐⭐⭐ListForEach列表渲染队列、排行等列表⭐⭐⭐⭐⭐CustomDialog自定义弹窗表单、确认框⭐⭐⭐⭐Builder构建函数复用 UI 片段⭐⭐⭐⭐动画rotate/animateTo唱片旋转、点赞动效⭐⭐⭐瀑布流布局祝福墙⭐⭐⭐状态驱动 UI 更新整个应用的核心思想⭐⭐⭐⭐⭐十一、扩展方向本项目作为学习示例还有很多可以扩展的方向功能扩展真实音频播放接入ohos.multimedia.audio实现真实音乐播放语音祝福录制语音祝福支持语音点播点歌打赏虚拟打赏系统增加互动趣味性主播连线主播与点歌人实时互动节目单按时间段编排广播节目单广播回放历史广播节目回放功能歌曲搜索接入音乐 API 搜索歌曲库私信功能用户间私信交流技术优化数据持久化使用ohos.data.preferences或数据库保存数据网络请求接入后端 API实现真实数据交互多页面架构使用Navigation拆分为多个页面状态管理引入ohos.arkui.observed或全局状态管理性能优化使用LazyForEach优化长列表性能深色模式适配系统深色模式国际化支持多语言切换架构升级MVVM 架构引入 ViewModel 层分离业务逻辑组件库抽离通用组件建立项目组件库单元测试为核心逻辑编写单元测试运行效果