鸿蒙原生应用实战四歌单管理 —— 创建歌单与歌曲编排前言上一篇实现了发现页和专辑详情页。本篇将实现用户最常交互的页面——歌单管理页PlaylistPage。歌单是音乐App的核心功能之一。在本篇中你将学到歌单卡片的创建、展开/收起从歌单中删除歌曲Modal弹窗实现新建歌单Stack叠加样式实现背景色歌单封面一、页面功能分析PlaylistPage ├── 顶部导航栏返回 标题我的歌单 新建按钮 ├── 歌单列表展开/收起 │ ├── 歌单卡片带背景色的封面 标题 歌曲数 │ └── 展开的曲目列表歌曲名 歌手 删除按钮 ├── 新建歌单Modal弹窗 └── 空状态还没有歌单时展示引导1.1 数据结构interfacePlaylistSong{id:number;title:string;artist:string;duration:string;addedDate:string;// 添加日期}interfacePlaylist{id:number;title:string;// 歌单名desc:string;// 描述cover:string;// 封面预留songCount:number;// 歌曲数totalDuration:string;// 总时长songs:PlaylistSong[];// 歌曲列表color:string;// 背景色每个歌单不同色}color字段的设计每个歌单在创建时分配一个颜色用作卡片背景色让歌单在视觉上更易区分。二、状态变量与数据初始化2.1 状态声明Componentstruct PlaylistPage{Stateplaylists:Playlist[][];// 全部歌单StateexpandedPlaylist:number-1;// 当前展开的歌单ID-1表示无StateshowCreateModal:booleanfalse;// 新建弹窗显隐StatenewPlaylistName:string;// 新建歌单名称StatenewPlaylistDesc:string;// 新建歌单描述}expandedPlaylist的设计值为-1表示所有歌单都收起值为某个歌单的id表示该歌单展开。每次只能展开一个歌单。2.2 初始化数据initPlaylists():void{this.playlists[{id:1,title:深夜循环,desc:适合深夜静静聆听的歌单,cover:,songCount:4,totalDuration:16分钟,color:#1E1B4B,songs:[{id:1,title:借过一下,artist:周深,duration:04:32,addedDate:2025-01-10},{id:2,title:奇妙能力歌,artist:陈粒,duration:03:48,addedDate:2025-01-10},{id:3,title:山水之间,artist:许嵩,duration:04:05,addedDate:2025-01-12},{id:4,title:夜曲,artist:周杰伦,duration:04:16,addedDate:2025-01-15}]},// ... 共4个歌单深夜循环/运动燃脂/华语金曲/旅行路上];}三、歌单卡片实现3.1 卡片设计BuilderbuildPlaylistCard(playlist:Playlist){Column(){Stack(){// 背景色块140px高度Column().width(100%).height(140).backgroundColor(playlist.color).borderRadius(16)// 前景内容居中Column(){Text().fontSize(40)Text(playlist.title).fontSize(20).fontWeight(FontWeight.Bold).fontColor(Color.White).margin({top:8})Text(${playlist.songCount}首 ·${playlist.totalDuration}).fontSize(12).fontColor(#C4B5FD).margin({top:4})}.alignItems(HorizontalAlign.Center)}.width(100%).height(140)Text(playlist.desc).fontSize(12).fontColor(#6B7280).width(100%).margin({top:6})}.width(100%).margin({top:12}).onClick((){if(this.expandedPlaylistplaylist.id){this.expandedPlaylist-1;// 点击已展开的歌单 → 收起}else{this.expandedPlaylistplaylist.id;// 点击其他歌单 → 展开它}})}Stack叠放实现背景色卡片底层Column填充背景色borderRadius(16)圆角上层内容居中Emoji 标题 副信息每个歌单不同的color值视觉差异化展开/收起逻辑点击已展开的歌单 → expandedPlaylist -1 → 收起 点击其他歌单 → expandedPlaylist 新ID → 旧收起新展开3.2 展开的曲目列表BuilderbuildSongList(playlist:Playlist){Column(){ForEach(playlist.songs,(song:PlaylistSong){Row(){// 歌曲图标Stack(){Column().width(36).height(36).backgroundColor(#EDE9FE).borderRadius(8)Text().fontSize(16)}// 歌曲信息Column(){Text(song.title).fontSize(13).fontColor(#1F1B2E)Text(${song.artist}·${song.duration}).fontSize(11).fontColor(#9CA3AF).margin({top:2})}.layoutWeight(1).alignItems(HorizontalAlign.Start).margin({left:10})// 删除按钮Text(️).fontSize(16).onClick((){this.deleteSong(playlist.id,song.id);})}.width(100%).padding({left:16,right:16,top:8,bottom:8}).backgroundColor(#FAFAFA).borderRadius(8).margin({top:4})},(song:PlaylistSong)song.id.toString())}.width(100%).padding({top:8})}每首歌曲显示 图标 歌曲名/歌手/时长 删除按钮。从数组删除元素deleteSong(playlistId:number,songId:number):void{for(leti:number0;ithis.playlists.length;i){if(this.playlists[i].idplaylistId){constpl:Playlistthis.playlists[i];constnewSongs:PlaylistSong[][];for(letj:number0;jpl.songs.length;j){if(pl.songs[j].id!songId){newSongs.push(pl.songs[j]);// 跳过要删除的}}this.playlists[i].songsnewSongs;// 更新歌曲列表this.playlists[i].songCountnewSongs.length;// 更新数量break;}}}为什么不用splice或filterArkTS严格模式下部分数组方法可能不兼容。使用传统的for循环 新数组构建是最安全的方式。重新赋值this.playlists[i].songs可以触发UI刷新。四、新建歌单Modal弹窗4.1 弹窗结构BuilderbuildCreateModal(){if(this.showCreateModal){Stack(){// 半透明遮罩层Column().width(100%).height(100%).backgroundColor(#00000033).onClick((){this.showCreateModalfalse;})// 弹窗卡片Column(){Text(新建歌单).fontSize(18).fontWeight(FontWeight.Bold).fontColor(#1F1B2E).margin({bottom:20})TextInput({placeholder:歌单名称}).width(100%).height(44).backgroundColor(#F8F7FF).borderRadius(10).padding({left:16}).fontSize(14).placeholderColor(#9CA3AF).onChange((val:string){this.newPlaylistNameval;})TextInput({placeholder:歌单描述选填}).width(100%).height(44).backgroundColor(#F8F7FF).borderRadius(10).padding({left:16}).fontSize(14).placeholderColor(#9CA3AF).margin({top:12}).onChange((val:string){this.newPlaylistDescval;})// 按钮组Row(){Text(取消).fontSize(14).fontColor(#6B7280).padding({left:24,right:24,top:10,bottom:10}).backgroundColor(#F3F4F6).borderRadius(20).onClick((){this.showCreateModalfalse;})Text(创建).fontSize(14).fontColor(Color.White).padding({left:24,right:24,top:10,bottom:10}).backgroundColor(#7C3AED).borderRadius(20).margin({left:12}).onClick((){this.createPlaylist();})}.margin({top:24})}.width(90%).padding(24).backgroundColor(#FFFFFF).borderRadius(20).alignItems(HorizontalAlign.Start)}.width(100%).height(100%).position({top:0,left:0})}}Modal弹窗的标准实现Stack (全屏) ├── 遮罩层 (透明黑色, 点击关闭) └── 卡片层 (白色背景, 圆角20, 90%宽度) ├── 标题 ├── 输入框 × 2 └── 取消/创建 按钮4.2 创建逻辑createPlaylist():void{if(this.newPlaylistName.trim().length0)return;constcolors:string[][#1E1B4B,#7C3AED,#B91C1C,#047857,#B45309,#1D4ED8];constnewId:numberthis.playlists.length0?this.playlists[this.playlists.length-1].id1:1;this.playlists.push({id:newId,title:this.newPlaylistName.trim(),desc:this.newPlaylistDesc.trim()||新建歌单,cover:,songCount:0,totalDuration:0分钟,songs:[],color:colors[this.playlists.length%colors.length]// 轮询分配颜色});this.newPlaylistName;this.newPlaylistDesc;this.showCreateModalfalse;}细节设计名称不能为空trim().length 0时直接return描述可选为空时默认为新建歌单ID自动递增颜色轮询分配每个新歌单有不同的背景色创建后清空输入、关闭弹窗五、空状态设计BuilderbuildEmptyState(){Column(){Text().fontSize(48)Text(还没有歌单).fontSize(16).fontColor(#9CA3AF).margin({top:12})Text(点击右上角新建创建你的第一个歌单).fontSize(13).fontColor(#D1D5DB).margin({top:8})}.width(100%).alignItems(HorizontalAlign.Center).padding({top:60})}与之前项目的空状态不同——这里不提供跳转按钮因为创建歌单的操作就在当前页面的右上角 新建按钮操作路径更短。六、主布局与弹窗叠放build():void{Stack(){// 主页面内容Column(){this.buildHeader()Scroll(){Column(){if(this.playlists.length0){this.buildEmptyState()}else{Text(共${this.playlists.length}个歌单).fontSize(12).fontColor(#6B7280).width(100%).padding({left:20,top:12})ForEach(this.playlists,(pl:Playlist){Column(){this.buildPlaylistCard(pl)if(this.expandedPlaylistpl.id){this.buildSongList(pl)// 展开的曲目列表}}.width(100%).padding({left:20,right:20})},(pl:Playlist)pl.id.toString())}}.width(100%).padding({bottom:20})}.scrollable(ScrollDirection.Vertical).layoutWeight(1).width(100%)}.width(100%).height(100%).backgroundColor(#F8F7FF)// Modal弹窗叠放在页面之上this.buildCreateModal()}.width(100%).height(100%)}Stack叠放的关键作用底层页面主内容导航栏 歌单列表上层Modal弹窗仅在showCreateModal true时渲染position({ top: 0, left: 0 })让弹窗覆盖全屏这种设计避免Modal被页面布局影响始终叠放在最上层。七、与之前项目追剧日历的对比对比维度追剧日历 MyListPage乐迷笔记 PlaylistPage核心操作切换Tab在看/想看/看完展开/收起歌单新增功能无Modal弹窗创建新歌单删除操作无从歌单中删除单曲卡片样式白色卡片 进度条彩色背景 Emoji封面交互模式Tab切换点击展开手风琴式背景色统一白色每个歌单不同色八、性能优化8.1 展开/收起状态管理只使用一个expandedPlaylist变量控制展开状态而不是为每个歌单维护独立的isExpanded字段。这种设计保证同时最多展开一个歌单手风琴效果减少状态数量4个歌单只需1个变量逻辑清晰 id展开 -1收起8.2 删除操作的数组重建// 避免直接修改原数组而是重建新数组constnewSongs:PlaylistSong[][];for(...){if(filter condition)newSongs.push(songs[j]);}this.playlists[i].songsnewSongs;重新赋值songs数组能确保 State 深度监听检测到变化。九、篇末总结本篇完成了歌单管理页核心内容包括✅ Stack叠放实现背景色歌单卡片✅ 手风琴式展开/收起曲目列表✅ Modal全屏弹窗实现新建歌单✅ 从歌单中删除单曲数组重建✅ 空状态引导设计✅ 创建歌单的交互流程输入 → 验证 → 创建 → 关闭下一篇是本系列的完结篇将实现个人中心页包含统计数据卡片歌曲数/歌单数/听歌时长/天数的Grid音乐口味水平条状图成就徽章系统已解锁/未解锁最近播放列表功能菜单入口文章索引:一项目初始化与Stage模型架构设计二首页开发 —— Grid分类网格与热歌排行榜三发现页与专辑详情 —— 多维筛选与曲目管理四歌单管理 —— 创建歌单与歌曲编排← 当前五个人中心与数据可视化 —— 统计图表与成就徽章