소스 검색

feat(playlist):优化歌单详情页功能与界面

- 改进歌曲列表展示逻辑,完善VideoItem对象创建
-优化播放事件发送方式,明确事件数据结构
- 更新歌单信息编辑功能,改进数据库更新调用- 调整页面UI元素,更换返回和更多操作图标
- 修改列表项样式,使用序号和文本展示歌曲信息- 添加新页面路由配置项
chendeben 10 달 전
부모
커밋
6ed09d9603

+ 239 - 0
.cursor/rules/music-player-patterns.mdc

@@ -0,0 +1,239 @@
+---
+description: 音乐播放器开发模式与最佳实践
+globs: ["**/view/**/*.ets", "**/viewmodel/**/*.ets", "**/controller/**/*.ets"]
+alwaysApply: false
+---
+
+# 音乐播放器开发模式与最佳实践
+
+## 播放状态管理
+
+### 播放状态枚举
+使用 `PlayStatus` 枚举管理播放状态:
+```typescript
+import { PlayStatus } from '../common/PlayStatus';
+
+// 在组件中使用
+@State playStatus: PlayStatus = PlayStatus.INIT;
+```
+
+### 状态转换模式
+```typescript
+// 播放/暂停切换
+togglePlay() {
+  if (this.playStatus === PlayStatus.PLAY) {
+    this.pauseMusic();
+    this.playStatus = PlayStatus.PAUSE;
+  } else {
+    this.playMusic();
+    this.playStatus = PlayStatus.PLAY;
+  }
+}
+```
+
+## 音频会话管理
+
+### AvSessionController使用
+```typescript
+import { AvSessionController } from '../controller/AvSessionController';
+
+// 获取控制器实例
+private avSessionController = AvSessionController.getInstance();
+
+// 在页面生命周期中注册/注销
+aboutToAppear() {
+  this.avSessionController.registerSessionListener();
+}
+
+aboutToDisappear() {
+  this.avSessionController.unregisterSessionListener();
+}
+```
+
+## 歌词处理模式
+
+### 歌词解析与显示
+```typescript
+// 使用lib中的LyricHelper
+import { LyricHelper } from '@lib/LyricHelper';
+
+// 解析歌词文件
+LyricHelper.parseLyricFile(lyricPath)
+  .then(lyrics => {
+    this.lyrics = lyrics;
+  })
+  .catch((err: Error) => {
+    Logger.error(`歌词解析失败: ${err.message}`);
+  });
+```
+
+### 歌词同步显示
+```typescript
+// 根据当前播放时间获取对应歌词行
+getCurrentLyricLine(currentTime: number): LyricLine | null {
+  if (!this.lyrics || this.lyrics.length === 0) {
+    return null;
+  }
+  
+  for (let i = 0; i < this.lyrics.length; i++) {
+    if (this.lyrics[i].time > currentTime) {
+      return i > 0 ? this.lyrics[i - 1] : null;
+    }
+  }
+  
+  return this.lyrics[this.lyrics.length - 1];
+}
+```
+
+## 媒体文件处理
+
+### 支持的音频格式
+使用 `CommonConstants.REAL_MUSIC_FORMAT` 检查文件格式:
+```typescript
+import { CommonConstants } from '../common/constants/CommonConstants';
+
+function isMusicFile(fileName: string): boolean {
+  const ext = fileName.substring(fileName.lastIndexOf('.')).toLowerCase();
+  return CommonConstants.REAL_MUSIC_FORMAT.includes(ext);
+}
+```
+
+### 媒体扫描与索引
+```typescript
+// 扫描本地音乐文件
+async scanLocalMusic(): Promise<MusicItem[]> {
+  const musicFiles: MusicItem[] = [];
+  const context = getContext(this);
+  
+  // 使用文件系统API扫描
+  // 实现细节取决于具体需求
+  
+  return musicFiles;
+}
+```
+
+## 播放列表管理
+
+### 播放列表模型
+```typescript
+import { Playlist } from '../viewmodel/Playlist';
+
+// 创建播放列表
+const playlist = new Playlist();
+playlist.name = "我的播放列表";
+playlist.songs = [song1, song2, song3];
+
+// 保存到本地存储
+PreferencesUtil.saveObject('playlist_' + playlist.id, playlist);
+```
+
+### 播放模式
+```typescript
+// 播放模式枚举
+enum PlayMode {
+  SEQUENCE,  // 顺序播放
+  LOOP,      // 循环播放
+  RANDOM,    // 随机播放
+  SINGLE     // 单曲循环
+}
+
+// 切换播放模式
+switchPlayMode() {
+  const modes = Object.values(PlayMode);
+  const currentIndex = modes.indexOf(this.playMode);
+  this.playMode = modes[(currentIndex + 1) % modes.length];
+}
+```
+
+## 主题适配
+
+### 主题切换
+```typescript
+import { myTheme } from '../common/AppTheme';
+
+// 在组件中使用主题颜色
+@Builder
+PlayerControl() {
+  Row() {
+    Button('播放')
+      .backgroundColor($r('app.color.brand'))
+      .fontColor($r('app.color.fontOnPrimary'))
+  }
+  .backgroundColor($r('app.color.backgroundPrimary'))
+}
+```
+
+### 动态主题更新
+```typescript
+// 更新主题
+updateTheme(themeIndex: number) {
+  const themeList = [DefaultTheme, TwilightTheme, ForestTheme, CoralTheme, MidnightTheme];
+  AppStorage.SetOrCreate('themeColor', themeList[themeIndex].colors.brand);
+}
+```
+
+## 性能优化
+
+### 图片加载优化
+```typescript
+// 使用缓存和懒加载
+Image(this.coverUrl)
+  .width(50)
+  .height(50)
+  .borderRadius(8)
+  .objectFit(ImageFit.Cover)
+  .alt($r('app.media.default_music_icon'))
+  .onComplete(() => {
+    // 加载完成回调
+  })
+  .onError(() => {
+    // 加载失败回调
+  })
+```
+
+### 列表性能优化
+```typescript
+// 使用LazyForEach和缓存
+LazyForEach(this.musicData, (item: MusicItem, index: number) => {
+  ListItem() {
+    MusicItemComponent({ musicItem: item })
+  }
+}, (item: MusicItem) => item.id.toString())
+```
+
+## 错误处理
+
+### 播放错误处理
+```typescript
+// 播放错误处理
+handlePlaybackError(error: Error) {
+  Logger.error(`播放错误: ${error.message}`);
+  this.playStatus = PlayStatus.INIT;
+  
+  // 显示错误提示
+  ToastUtil.showToast(`播放失败: ${error.message}`);
+}
+```
+
+### 网络请求错误处理
+```typescript
+// API请求错误处理
+fetchLyrics(title: string, artist: string) {
+  const url = `${CommonConstants.LRC_API}?title=${encodeURIComponent(title)}&artist=${encodeURIComponent(artist)}`;
+  
+  fetch(url)
+    .then(response => {
+      if (!response.ok) {
+        throw new Error(`HTTP error! status: ${response.status}`);
+      }
+      return response.json();
+    })
+    .then(data => {
+      this.processLyrics(data);
+    })
+    .catch((err: Error) => {
+      Logger.error(`获取歌词失败: ${err.message}`);
+      // 使用备用API或显示错误
+    });
+}
+```

+ 331 - 0
.cursor/rules/native-integration.mdc

@@ -0,0 +1,331 @@
+---
+description: 原生模块集成指南
+globs: ["**/cpp/**/*", "**/napi/**/*", "**/ijkplayer/**/*", "**/lib/**/*"]
+alwaysApply: false
+---
+
+# 原生模块集成指南
+
+## ijkplayer播放器集成
+
+### 基本使用
+```typescript
+import { IjkMediaPlayer } from '@ohos/ijkplayer';
+
+// 创建播放器实例
+private player: IjkMediaPlayer = new IjkMediaPlayer();
+
+// 设置数据源
+async setDataSource(path: string) {
+  try {
+    await this.player.setDataSource(path);
+    await this.player.prepare();
+  } catch (err: Error) {
+    Logger.error(`设置数据源失败: ${err.message}`);
+  }
+}
+```
+
+### 播放控制
+```typescript
+// 播放
+async play() {
+  try {
+    await this.player.start();
+    this.playStatus = PlayStatus.PLAY;
+  } catch (err: Error) {
+    Logger.error(`播放失败: ${err.message}`);
+  }
+}
+
+// 暂停
+async pause() {
+  try {
+    await this.player.pause();
+    this.playStatus = PlayStatus.PAUSE;
+  } catch (err: Error) {
+    Logger.error(`暂停失败: ${err.message}`);
+  }
+}
+
+// 停止
+async stop() {
+  try {
+    await this.player.stop();
+    this.playStatus = PlayStatus.STOP;
+  } catch (err: Error) {
+    Logger.error(`停止失败: ${err.message}`);
+  }
+}
+```
+
+### 播放状态监听
+```typescript
+// 设置播放状态监听器
+setupPlayerListener() {
+  this.player.on('stateChange', (state: string) => {
+    switch (state) {
+      case 'prepared':
+        // 准备完成
+        break;
+      case 'playing':
+        this.playStatus = PlayStatus.PLAY;
+        break;
+      case 'paused':
+        this.playStatus = PlayStatus.PAUSE;
+        break;
+      case 'completed':
+        this.playStatus = PlayStatus.DONE;
+        this.playNext(); // 播放下一首
+        break;
+      case 'error':
+        this.handlePlaybackError(new Error('播放器错误'));
+        break;
+    }
+  });
+
+  this.player.on('timeUpdate', (currentTime: number, duration: number) => {
+    this.currentTime = currentTime;
+    this.duration = duration;
+    this.updateProgress();
+  });
+}
+```
+
+### 音频焦点处理
+```typescript
+import { avSession } from '@kit.ArkAVSessionKit';
+
+// 请求音频焦点
+async requestAudioFocus() {
+  try {
+    const audioSession = await avSession.createAVSession(getContext(this), 'audio', 'music');
+    await audioSession.activate();
+    this.audioSession = audioSession;
+  } catch (err: Error) {
+    Logger.error(`获取音频焦点失败: ${err.message}`);
+  }
+}
+
+// 释放音频焦点
+async releaseAudioFocus() {
+  if (this.audioSession) {
+    try {
+      await this.audioSession.deactivate();
+      await this.audioSession.destroy();
+      this.audioSession = null;
+    } catch (err: Error) {
+      Logger.error(`释放音频焦点失败: ${err.message}`);
+    }
+  }
+}
+```
+
+## 歌词库集成
+
+### LyricHelper使用
+```typescript
+import { LyricHelper } from '@lib/LyricHelper';
+
+// 解析歌词文件
+async parseLyricFile(filePath: string): Promise<LyricLine[]> {
+  try {
+    const lyrics = await LyricHelper.parseLyricFile(filePath);
+    return lyrics;
+  } catch (err: Error) {
+    Logger.error(`解析歌词文件失败: ${err.message}`);
+    return [];
+  }
+}
+
+// 解析歌词文本
+parseLyricText(lyricText: string): LyricLine[] {
+  try {
+    return LyricHelper.parseLyricText(lyricText);
+  } catch (err: Error) {
+    Logger.error(`解析歌词文本失败: ${err.message}`);
+    return [];
+  }
+}
+```
+
+### 歌词同步
+```typescript
+// 获取当前时间对应的歌词行
+getCurrentLyric(currentTime: number): LyricLine | null {
+  if (!this.lyrics || this.lyrics.length === 0) {
+    return null;
+  }
+
+  for (let i = 0; i < this.lyrics.length; i++) {
+    if (this.lyrics[i].time > currentTime) {
+      return i > 0 ? this.lyrics[i - 1] : null;
+    }
+  }
+
+  return this.lyrics[this.lyrics.length - 1];
+}
+
+// 获取下一句歌词
+getNextLyric(currentTime: number): LyricLine | null {
+  if (!this.lyrics || this.lyrics.length === 0) {
+    return null;
+  }
+
+  for (let i = 0; i < this.lyrics.length; i++) {
+    if (this.lyrics[i].time > currentTime) {
+      return this.lyrics[i];
+    }
+  }
+
+  return null;
+}
+```
+
+## NAPI开发指南
+
+### 基本NAPI模块结构
+```cpp
+// napi_init.cpp
+#include "napi/native_api.h"
+
+static napi_value Init(napi_env env, napi_value exports) {
+  // 导出函数
+  napi_property_descriptor desc[] = {
+    {"createPlayer", nullptr, CreatePlayer, nullptr, nullptr, nullptr, napi_default, nullptr},
+    {"destroyPlayer", nullptr, DestroyPlayer, nullptr, nullptr, nullptr, napi_default, nullptr},
+  };
+  
+  napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
+  return exports;
+}
+
+static napi_module demoModule = {
+  .nm_version = 1,
+  .nm_flags = 0,
+  .nm_filename = nullptr,
+  .nm_register_func = Init,
+  .nm_modname = "entry",
+  .nm_priv = ((void*)0),
+  .reserved = {0},
+};
+
+extern "C" __attribute__((constructor)) void RegisterEntryModule(void) {
+  napi_module_register(&demoModule);
+}
+```
+
+### 异步操作处理
+```cpp
+// 异步操作结构体
+struct AsyncData {
+  napi_async_work work;
+  napi_deferred deferred;
+  napi_ref callback;
+  std::string result;
+  std::string error;
+};
+
+// 异步执行函数
+static void ExecuteCallback(napi_env env, void* data) {
+  AsyncData* asyncData = (AsyncData*)data;
+  
+  try {
+    // 执行耗时操作
+    asyncData->result = performOperation();
+  } catch (const std::exception& e) {
+    asyncData->error = e.what();
+  }
+}
+
+// 完成回调函数
+static void CompleteCallback(napi_env env, napi_status status, void* data) {
+  AsyncData* asyncData = (AsyncData*)data;
+  
+  napi_value callback;
+  napi_get_reference_value(env, asyncData->callback, &callback);
+  
+  napi_value result;
+  if (asyncData->error.empty()) {
+    napi_create_string_utf8(env, asyncData->result.c_str(), NAPI_AUTO_LENGTH, &result);
+    napi_call_function(env, nullptr, callback, 1, &result, nullptr);
+  } else {
+    napi_value error;
+    napi_create_string_utf8(env, asyncData->error.c_str(), NAPI_AUTO_LENGTH, &error);
+    napi_call_function(env, nullptr, callback, 1, &error, nullptr);
+  }
+  
+  // 清理资源
+  napi_delete_async_work(env, asyncData->work);
+  napi_delete_reference(env, asyncData->callback);
+  delete asyncData;
+}
+```
+
+## 性能优化建议
+
+### 播放器优化
+1. 使用对象池管理播放器实例,避免频繁创建和销毁
+2. 预加载下一首歌曲,减少切换歌曲时的延迟
+3. 使用硬件解码加速,降低CPU占用
+4. 合理设置缓冲区大小,平衡播放流畅度和内存占用
+
+### 内存管理
+1. 及时释放不再使用的资源,如播放器实例、音频会话等
+2. 使用弱引用避免循环引用导致的内存泄漏
+3. 监控内存使用情况,及时处理内存警告
+
+### 线程管理
+1. 将耗时操作放在工作线程中执行,避免阻塞UI线程
+2. 使用线程池管理并发任务,避免创建过多线程
+3. 合理使用同步机制,避免死锁和竞态条件
+
+## 错误处理
+
+### 播放器错误处理
+```typescript
+// 播放器错误处理
+handlePlayerError(error: Error) {
+  Logger.error(`播放器错误: ${error.message}`);
+  
+  // 根据错误类型采取不同处理策略
+  if (error.message.includes('网络')) {
+    // 网络错误,尝试重试或使用本地缓存
+    this.retryWithCache();
+  } else if (error.message.includes('解码')) {
+    // 解码错误,尝试使用备用解码器
+    this.switchToBackupDecoder();
+  } else {
+    // 其他错误,显示错误提示并停止播放
+    this.showErrorMessage(error.message);
+    this.stop();
+  }
+}
+```
+
+### NAPI错误处理
+```cpp
+// NAPI错误处理
+static napi_value SomeFunction(napi_env env, napi_callback_info info) {
+  size_t argc = 1;
+  napi_value args[1];
+  napi_status status = napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
+  
+  if (status != napi_ok || argc < 1) {
+    napi_throw_error(env, nullptr, "Invalid arguments");
+    return nullptr;
+  }
+  
+  // 参数验证
+  bool isString;
+  napi_is_string(env, args[0], &isString);
+  if (!isString) {
+    napi_throw_type_error(env, nullptr, "Expected string");
+    return nullptr;
+  }
+  
+  // 执行操作...
+  
+  return result;
+}
+```

+ 85 - 0
.cursor/rules/project-structure.mdc

@@ -0,0 +1,85 @@
+---
+description: TTMusic项目结构与关键文件指南
+globs: ["**/*.ets", "**/*.ts"]
+alwaysApply: true
+---
+
+# TTMusic项目结构与关键文件指南
+
+## 项目概述
+TTMusic是一个基于鸿蒙ArkTS开发的音乐播放器应用,支持本地音乐播放、歌词显示、主题切换等功能。
+
+## 核心目录结构
+
+### 1. 应用入口
+- `entry/src/main/ets/entryability/EntryAbility.ets` - 应用入口点
+- `entry/src/main/ets/MyAbilityStage.ets` - 应用生命周期管理
+
+### 2. 页面组件
+- `entry/src/main/ets/pages/` - 所有页面组件
+  - `MainIndex.ets` - 主页面,包含底部导航栏
+  - `SplashIndex.ets` - 启动页
+  - `SettingPage.ets` - 设置页面
+  - `PlaylistDetailPage.ets` - 播放列表详情页
+  - `UserCenter.ets` - 用户中心页面
+
+### 3. 视图组件
+- `entry/src/main/ets/view/` - 可复用视图组件
+  - 音乐播放控制组件
+  - 歌词显示组件
+  - 列表项组件
+
+### 4. 视图模型
+- `entry/src/main/ets/viewmodel/` - 数据模型与业务逻辑
+  - `MainViewModel.ets` - 主页面数据模型
+  - `ItemData.ets` - 列表项数据模型
+  - `Playlist.ets` - 播放列表模型
+
+### 5. 控制器
+- `entry/src/main/ets/controller/` - 控制器层
+  - `AvSessionController.ets` - 音频会话控制器
+  - `KnockController.ets` - 敲击检测控制器
+
+### 6. 公共资源
+- `entry/src/main/ets/common/` - 公共工具和常量
+  - `AppTheme.ets` - 主题配置
+  - `PlayStatus.ets` - 播放状态枚举
+  - `constants/CommonConstants.ets` - 通用常量
+  - `util/` - 工具类集合
+
+### 7. 对话框
+- `entry/src/main/ets/dialog/` - 对话框组件
+  - `PlaylistDialog.ets` - 播放列表对话框
+  - `UserPrivacyDialog.ets` - 用户隐私对话框
+
+### 8. 原生模块
+- `ijkplayer/` - FFmpeg播放器原生模块
+- `lib/` - 歌词解析库
+
+## 关键文件说明
+
+### 主题系统
+项目支持多主题切换,主题配置在 `AppTheme.ets` 中定义:
+- `DefaultColors` - 默认浅色主题
+- `TwilightColors` - 暮色深色主题
+- `ForestColors` - 森林浅色主题
+- `CoralColors` - 珊瑚浅色主题
+- `MidnightColors` - 极夜深色主题
+
+### 常量配置
+`CommonConstants.ets` 包含应用中使用的所有常量:
+- API端点
+- 文件格式支持列表
+- 播放状态枚举
+- 微信支付配置
+
+### 日志系统
+使用 `common/util/Logger.ets` 进行统一日志记录,基于鸿蒙系统的hilog实现。
+
+## 开发注意事项
+
+1. 所有页面组件应使用 `@Entry` 和 `@Component` 装饰器
+2. 状态管理使用 `@State` 和 `@StorageProp` 装饰器
+3. 遵循ArkTS语法限制,特别是避免使用解构赋值和计算属性名
+4. 使用项目定义的常量而非硬编码值
+5. 遵循项目的命名规范和代码风格

+ 545 - 0
.cursor/rules/ui-components.mdc

@@ -0,0 +1,545 @@
+---
+description: UI组件开发模式与最佳实践
+globs: ["**/pages/**/*.ets", "**/view/**/*.ets", "**/dialog/**/*.ets"]
+alwaysApply: false
+---
+
+# UI组件开发模式与最佳实践
+
+## 页面组件结构
+
+### 基本页面模板
+```typescript
+import { CommonConstants } from '../common/constants/CommonConstants';
+import Logger from '../common/util/Logger';
+
+@Entry
+@Component
+struct PageName {
+  // 状态变量
+  @State isLoading: boolean = false;
+  @State dataList: Array<ItemType> = [];
+  
+  // 上下文
+  private context = getContext(this);
+  
+  // 生命周期
+  aboutToAppear() {
+    this.initData();
+  }
+  
+  aboutToDisappear() {
+    this.cleanup();
+  }
+  
+  // 初始化数据
+  private initData() {
+    // 初始化逻辑
+  }
+  
+  // 清理资源
+  private cleanup() {
+    // 清理逻辑
+  }
+  
+  // 构建方法
+  build() {
+    Column() {
+      // 页面内容
+    }
+    .width('100%')
+    .height('100%')
+    .backgroundColor($r('app.color.backgroundPrimary'))
+  }
+}
+```
+
+### 导航栏组件
+```typescript
+@Builder
+NavigationArea(title: string, showBack: boolean = true) {
+  Row() {
+    if (showBack) {
+      Image($r('app.media.ic_back'))
+        .width(24)
+        .height(24)
+        .margin({ left: 16 })
+        .onClick(() => {
+          router.back();
+        })
+    }
+    
+    Text(title)
+      .fontSize(18)
+      .fontWeight(FontWeight.Medium)
+      .fontColor($r('app.color.fontPrimary'))
+      .layoutWeight(1)
+      .textAlign(TextAlign.Center)
+      .margin({ right: showBack ? 40 : 16 })
+  }
+  .width('100%')
+  .height(56)
+  .backgroundColor($r('app.color.backgroundPrimary'))
+}
+```
+
+## 列表组件
+
+### 基本列表组件
+```typescript
+@Component
+struct MusicListItem {
+  @Prop musicItem: MusicItem;
+  @Prop isPlaying: boolean = false;
+  private onItemClick?: (item: MusicItem) => void;
+  
+  build() {
+    Row() {
+      Image(this.musicItem.cover || $r('app.media.default_music_icon'))
+        .width(50)
+        .height(50)
+        .borderRadius(8)
+        .objectFit(ImageFit.Cover)
+        .margin({ right: 12 })
+      
+      Column() {
+        Text(this.musicItem.title)
+          .fontSize(16)
+          .fontColor($r('app.color.fontPrimary'))
+          .maxLines(1)
+          .textOverflow({ overflow: TextOverflow.Ellipsis })
+          .width('100%')
+        
+        Text(this.musicItem.artist)
+          .fontSize(14)
+          .fontColor($r('app.color.fontSecondary'))
+          .maxLines(1)
+          .textOverflow({ overflow: TextOverflow.Ellipsis })
+          .width('100%')
+          .margin({ top: 4 })
+      }
+      .layoutWeight(1)
+      .alignItems(HorizontalAlign.Start)
+      
+      if (this.isPlaying) {
+        Image($r('app.media.ic_playing'))
+          .width(24)
+          .height(24)
+          .margin({ left: 12 })
+      }
+    }
+    .width('100%')
+    .height(70)
+    .padding({ horizontal: 16, vertical: 10 })
+    .onClick(() => {
+      if (this.onItemClick) {
+        this.onItemClick(this.musicItem);
+      }
+    })
+  }
+}
+```
+
+### 高性能列表
+```typescript
+@Component
+struct MusicList {
+  @State musicList: MusicItem[] = [];
+  @State currentPlayingId: string = '';
+  
+  build() {
+    List({ space: 1 }) {
+      LazyForEach(new MusicDataSource(this.musicList), (item: MusicItem, index: number) => {
+        ListItem() {
+          MusicListItem({
+            musicItem: item,
+            isPlaying: item.id === this.currentPlayingId,
+            onItemClick: (musicItem: MusicItem) => {
+              this.playMusic(musicItem);
+            }
+          })
+        }
+      }, (item: MusicItem) => item.id)
+    }
+    .width('100%')
+    .layoutWeight(1)
+    .divider({ strokeWidth: 1, color: $r('app.color.compDivider') })
+  }
+  
+  private playMusic(musicItem: MusicItem) {
+    this.currentPlayingId = musicItem.id;
+    // 播放音乐逻辑
+  }
+}
+
+// 数据源类
+class MusicDataSource implements IDataSource {
+  private listeners: DataChangeListener[] = [];
+  private data: MusicItem[] = [];
+  
+  constructor(data: MusicItem[]) {
+    this.data = data;
+  }
+  
+  totalCount(): number {
+    return this.data.length;
+  }
+  
+  getData(index: number): MusicItem {
+    return this.data[index];
+  }
+  
+  registerDataChangeListener(listener: DataChangeListener): void {
+    if (this.listeners.indexOf(listener) < 0) {
+      this.listeners.push(listener);
+    }
+  }
+  
+  unregisterDataChangeListener(listener: DataChangeListener): void {
+    const pos = this.listeners.indexOf(listener);
+    if (pos >= 0) {
+      this.listeners.splice(pos, 1);
+    }
+  }
+  
+  notifyDataReload(): void {
+    this.listeners.forEach(listener => {
+      listener.onDataReloaded();
+    });
+  }
+}
+```
+
+## 播放控制组件
+
+### 播放控制栏
+```typescript
+@Component
+struct PlayerControlBar {
+  @Prop isPlaying: boolean = false;
+  @Prop currentTime: number = 0;
+  @Prop duration: number = 0;
+  private onPlayPause?: () => void;
+  private onPrevious?: () => void;
+  private onNext?: () => void;
+  private onSeek?: (position: number) => void;
+  
+  build() {
+    Column() {
+      // 进度条
+      Row() {
+        Text(this.formatTime(this.currentTime))
+          .fontSize(12)
+          .fontColor($r('app.color.fontSecondary'))
+        
+        Slider({
+          value: this.currentTime,
+          min: 0,
+          max: this.duration || 1,
+          style: SliderStyle.InSet
+        })
+          .layoutWeight(1)
+          .margin({ horizontal: 12 })
+          .trackColor($r('app.color.compBackgroundTertiary'))
+          .selectedColor($r('app.color.brand'))
+          .blockColor($r('app.color.brand'))
+          .onChange((value: number) => {
+            if (this.onSeek) {
+              this.onSeek(value);
+            }
+          })
+        
+        Text(this.formatTime(this.duration))
+          .fontSize(12)
+          .fontColor($r('app.color.fontSecondary'))
+      }
+      .width('100%')
+      .margin({ bottom: 20 })
+      
+      // 控制按钮
+      Row() {
+        Button() {
+          Image($r('app.media.ic_previous'))
+            .width(28)
+            .height(28)
+            .fillColor($r('app.color.iconPrimary'))
+        }
+        .type(ButtonType.Circle)
+        .backgroundColor(Color.Transparent)
+        .width(48)
+        .height(48)
+        .onClick(() => {
+          if (this.onPrevious) {
+            this.onPrevious();
+          }
+        })
+        
+        Button() {
+          Image(this.isPlaying ? $r('app.media.ic_pause') : $r('app.media.ic_play'))
+            .width(36)
+            .height(36)
+            .fillColor($r('app.color.iconOnPrimary'))
+        }
+        .type(ButtonType.Circle)
+        .backgroundColor($r('app.color.brand'))
+        .width(64)
+        .height(64)
+        .margin({ horizontal: 20 })
+        .onClick(() => {
+          if (this.onPlayPause) {
+            this.onPlayPause();
+          }
+        })
+        
+        Button() {
+          Image($r('app.media.ic_next'))
+            .width(28)
+            .height(28)
+            .fillColor($r('app.color.iconPrimary'))
+        }
+        .type(ButtonType.Circle)
+        .backgroundColor(Color.Transparent)
+        .width(48)
+        .height(48)
+        .onClick(() => {
+          if (this.onNext) {
+            this.onNext();
+          }
+        })
+      }
+      .width('100%')
+      .justifyContent(FlexAlign.Center)
+    }
+    .width('100%')
+    .padding({ horizontal: 20, vertical: 16 })
+  }
+  
+  private formatTime(time: number): string {
+    if (isNaN(time) || time < 0) return '00:00';
+    
+    const minutes = Math.floor(time / 60);
+    const seconds = Math.floor(time % 60);
+    return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
+  }
+}
+```
+
+## 对话框组件
+
+### 自定义对话框
+```typescript
+@Component
+struct CustomDialog {
+  @Prop title: string = '';
+  @Prop content: string = '';
+  @Prop confirmText: string = '确定';
+  @Prop cancelText: string = '取消';
+  @Prop showCancel: boolean = true;
+  private onConfirm?: () => void;
+  private onCancel?: () => void;
+  
+  build() {
+    Column() {
+      // 标题
+      Text(this.title)
+        .fontSize(18)
+        .fontWeight(FontWeight.Medium)
+        .fontColor($r('app.color.fontPrimary'))
+        .margin({ top: 24, bottom: 12 })
+        .padding({ horizontal: 20 })
+      
+      // 内容
+      Text(this.content)
+        .fontSize(14)
+        .fontColor($r('app.color.fontSecondary'))
+        .textAlign(TextAlign.Center)
+        .padding({ horizontal: 20 })
+        .margin({ bottom: 24 })
+      
+      // 按钮
+      Row() {
+        if (this.showCancel) {
+          Button(this.cancelText)
+            .fontSize(16)
+            .fontColor($r('app.color.fontPrimary'))
+            .backgroundColor(Color.Transparent)
+            .layoutWeight(1)
+            .onClick(() => {
+              if (this.onCancel) {
+                this.onCancel();
+              }
+            })
+        }
+        
+        Button(this.confirmText)
+          .fontSize(16)
+          .fontColor($r('app.color.brand'))
+          .backgroundColor(Color.Transparent)
+          .layoutWeight(1)
+          .onClick(() => {
+            if (this.onConfirm) {
+              this.onConfirm();
+            }
+          })
+      }
+      .width('100%')
+      .height(48)
+    }
+    .backgroundColor($r('app.color.backgroundPrimary'))
+    .borderRadius(12)
+    .width('80%')
+  }
+}
+```
+
+## 主题适配
+
+### 主题感知组件
+```typescript
+@Component
+struct ThemeAwareButton {
+  @Prop text: string = '';
+  @Prop type: 'primary' | 'secondary' = 'primary';
+  private onClick?: () => void;
+  
+  build() {
+    Button(this.text)
+      .fontSize(16)
+      .fontColor(this.type === 'primary' ? 
+        $r('app.color.fontOnPrimary') : 
+        $r('app.color.fontPrimary'))
+      .backgroundColor(this.type === 'primary' ? 
+        $r('app.color.brand') : 
+        $r('app.color.compBackgroundSecondary'))
+      .borderRadius(8)
+      .padding({ horizontal: 20, vertical: 10 })
+      .onClick(() => {
+        if (this.onClick) {
+          this.onClick();
+        }
+      })
+  }
+}
+```
+
+## 动画效果
+
+### 页面转场动画
+```typescript
+// 页面跳转带动画
+router.pushUrl({
+  url: 'pages/DetailPage',
+  params: { id: this.itemId }
+}).then(() => {
+  // 页面跳转成功
+}).catch((err: Error) => {
+  Logger.error(`页面跳转失败: ${err.message}`);
+});
+
+// 在目标页面中
+@Entry
+@Component
+struct DetailPage {
+  // 页面转场动画
+  pageTransition() {
+    PageTransitionEnter({ duration: 300, curve: Curve.EaseInOut })
+      .slide(SlideEffect.Right)
+    
+    PageTransitionExit({ duration: 300, curve: Curve.EaseInOut })
+      .slide(SlideEffect.Left)
+  }
+  
+  build() {
+    // 页面内容
+  }
+}
+```
+
+### 状态变化动画
+```typescript
+@Component
+struct AnimatedButton {
+  @State isPressed: boolean = false;
+  
+  build() {
+    Button('点击我')
+      .scale({ x: this.isPressed ? 0.95 : 1, y: this.isPressed ? 0.95 : 1 })
+      .animation({
+        duration: 100,
+        curve: Curve.EaseInOut
+      })
+      .onTouch((event: TouchEvent) => {
+        if (event.type === TouchType.Down) {
+          this.isPressed = true;
+        } else if (event.type === TouchType.Up || event.type === TouchType.Cancel) {
+          this.isPressed = false;
+        }
+      })
+  }
+}
+```
+
+## 响应式布局
+
+### 断点适配
+```typescript
+@Component
+struct ResponsiveLayout {
+  @State currentBreakpoint: string = 'sm';
+  
+  aboutToAppear() {
+    // 监听窗口大小变化
+    window.getLastWindow(getContext(this))
+      .then((windowClass) => {
+        windowClass.on('windowSizeChange', (windowSize) => {
+          this.updateBreakpoint(windowSize.width);
+        });
+        this.updateBreakpoint(windowClass.getWindowProperties().windowRect.width);
+      });
+  }
+  
+  private updateBreakpoint(width: number) {
+    if (width < 600) {
+      this.currentBreakpoint = 'sm';
+    } else if (width < 840) {
+      this.currentBreakpoint = 'md';
+    } else {
+      this.currentBreakpoint = 'lg';
+    }
+  }
+  
+  build() {
+    if (this.currentBreakpoint === 'sm') {
+      // 小屏幕布局
+      this.buildSmallLayout();
+    } else if (this.currentBreakpoint === 'md') {
+      // 中等屏幕布局
+      this.buildMediumLayout();
+    } else {
+      // 大屏幕布局
+      this.buildLargeLayout();
+    }
+  }
+  
+  @Builder
+  buildSmallLayout() {
+    Column() {
+      // 小屏幕布局内容
+    }
+  }
+  
+  @Builder
+  buildMediumLayout() {
+    Row() {
+      // 中等屏幕布局内容
+    }
+  }
+  
+  @Builder
+  buildLargeLayout() {
+    Grid() {
+      // 大屏幕布局内容
+    }
+  }
+}
+```

+ 77 - 37
entry/src/main/ets/pages/PlaylistDetailPage.ets

@@ -1,10 +1,10 @@
-import { Playlist } from '../viewmodel/Playlist';
+import { Playlist, PlaylistSong } from '../viewmodel/Playlist';
 import { VideoItem } from '../viewmodel/VideoItem';
 import PlaylistTable from '../common/util/PlaylistTable';
-import { MusicItem } from '../view/LocalMusic';
 import { emitter } from '@kit.BasicServicesKit';
-import { ToastUtil } from '@pura/harmony-utils';
+import { ToastUtil, AppUtil } from '@pura/harmony-utils';
 import { showEditPlaylistDialog } from '../dialog/PlaylistDialog';
+import { router } from '@kit.ArkUI';
 
 /**
  * 歌单详情页面
@@ -65,17 +65,27 @@ export struct PlaylistDetailPage {
     
     for (const playlistSong of playlistSongs) {
       try {
-        // 这里需要根据文件路径查询媒体库获取完整的 VideoItem 信息
-        // 暂时创建一个基本的 VideoItem 对象
-        const videoItem: VideoItem = {
-          filePath: playlistSong.songFilePath,
-          fileName: playlistSong.songFilePath.split('/').pop() || '',
-          fileSize: 0,
-          duration: 0,
-          type: 0, // 音乐类型
-          isSelected: false,
-          isPlaying: false
-        }
+        // 获取文件名
+        const fileName = playlistSong.songFilePath.split('/').pop() || ''
+        const name = fileName.replace(/\.[^/.]+$/, "") // 移除文件扩展名
+        
+        // 创建基本的 VideoItem 对象
+        const videoItem = new VideoItem(
+          name, // name
+          Date.now().toString(), // id
+          playlistSong.songFilePath, // filePath
+          0, // type (音乐类型)
+          0, // videoSize
+          new Date().toISOString(), // cTime
+          undefined, // pixelMap
+          undefined, // size
+          undefined, // pixelMapPath
+          undefined, // artist
+          undefined, // album
+          fileName, // fileName
+          undefined // lastPlayed
+        )
+        
         videoItems.push(videoItem)
       } catch (error) {
         console.error('转换歌曲失败:', error)
@@ -95,11 +105,15 @@ export struct PlaylistDetailPage {
     }
 
     // 发送播放歌单事件
-    emitter.emit({ eventId: 2002 }, {
-      playlist: this.playlist,
-      songs: this.songList,
-      startIndex: 0
-    })
+    const eventPlaylistPlay: emitter.InnerEvent = { eventId: 2002 }
+    const eventData: emitter.EventData = {
+      data: {
+        playlist: this.playlist,
+        songs: this.songList,
+        startIndex: 0
+      }
+    };
+    emitter.emit(eventPlaylistPlay, eventData)
     
     ToastUtil.showToast('开始播放歌单')
   }
@@ -109,11 +123,15 @@ export struct PlaylistDetailPage {
    */
   playSong(song: VideoItem, index: number) {
     // 发送播放歌单事件,指定开始播放的歌曲
-    emitter.emit({ eventId: 2002 }, {
-      playlist: this.playlist,
-      songs: this.songList,
-      startIndex: index
-    })
+    const eventPlaylistPlay: emitter.InnerEvent = { eventId: 2002 }
+    const eventData: emitter.EventData = {
+      data: {
+        playlist: this.playlist,
+        songs: this.songList,
+        startIndex: index
+      }
+    };
+    emitter.emit(eventPlaylistPlay, eventData)
   }
 
   /**
@@ -127,11 +145,12 @@ export struct PlaylistDetailPage {
           if (this.playlist) {
             this.playlist.name = name
             this.playlist.description = description
-            const success = await this.playlistTable.updatePlaylist(this.playlist)
+            const success = await this.playlistTable.updatePlaylist(this.playlist.id, name, description)
             if (success) {
               ToastUtil.showToast('歌单更新成功')
               // 发送刷新事件
-              emitter.emit({ eventId: 2001 }, {})
+              const eventRefresh: emitter.InnerEvent = { eventId: 2001 }
+              emitter.emit(eventRefresh, {})
             } else {
               ToastUtil.showToast('歌单更新失败')
             }
@@ -189,7 +208,7 @@ export struct PlaylistDetailPage {
         
         // 更新歌单信息
         this.playlist.songCount = this.songList.length
-        await this.playlistTable.updatePlaylist(this.playlist)
+        await this.playlistTable.updatePlaylist(this.playlist.id, this.playlist.name, this.playlist.description)
         
         ToastUtil.showToast('已从歌单移除')
       } else {
@@ -209,7 +228,7 @@ export struct PlaylistDetailPage {
         
         // 标题栏
         Row() {
-          Image($r('app.media.arrow_left'))
+          Image($r('app.media.back'))
             .width(24)
             .height(24)
             .margin({ left: 12, right: 8 })
@@ -225,7 +244,7 @@ export struct PlaylistDetailPage {
             .textAlign(TextAlign.Center)
           
           // 更多操作按钮
-          Image($r('app.media.more'))
+          Image($r('app.media.ic_more_vert_black_24dp'))
             .width(24)
             .height(24)
             .margin({ right: 12 })
@@ -323,7 +342,7 @@ export struct PlaylistDetailPage {
           .width('100%')
           .padding({ left: 16, right: 16, bottom: 16 })
         }
-        .backgroundColor($r('app.color.card_background'))
+        .backgroundColor($r('app.color.bg_card'))
         .margin({ left: 16, right: 16, top: 16 })
         .borderRadius(12)
 
@@ -332,13 +351,34 @@ export struct PlaylistDetailPage {
           List() {
             ForEach(this.songList, (song: VideoItem, index: number) => {
               ListItem() {
-                MusicItem(song, index)
-                  .onClick(() => {
-                    this.playSong(song, index)
-                  })
-                  .gesture(LongPressGesture().onAction(() => {
-                    this.showSongMenu(song)
-                  }))
+                Row() {
+                  Text(`${index + 1}`)
+                    .fontSize(14)
+                    .fontColor($r('app.color.text_color'))
+                    .width(40)
+                    .textAlign(TextAlign.Center)
+                  
+                  Column() {
+                    Text(song.name || song.fileName || '')
+                      .fontSize(16)
+                      .fontColor($r('app.color.text_color'))
+                      .maxLines(1)
+                      .textOverflow({ overflow: TextOverflow.Ellipsis })
+                      .width('100%')
+                  }
+                  .layoutWeight(1)
+                  .alignItems(HorizontalAlign.Start)
+                  .margin({ left: 16 })
+                }
+                .width('100%')
+                .height(56)
+                .padding({ left: 16, right: 16 })
+                .onClick(() => {
+                  this.playSong(song, index)
+                })
+                .gesture(LongPressGesture().onAction(() => {
+                  this.showSongMenu(song)
+                }))
               }
             })
           }

+ 2 - 1
entry/src/main/resources/base/profile/main_pages.json

@@ -6,6 +6,7 @@
     "pages/NewIndex",
     "pages/VerifyPage",
     "pages/VipPage",
-    "pages/Demo"
+    "pages/Demo",
+    "pages/PlaylistDetailPage"
   ]
 }