HarmonyOS音乐播放器开发实战:从零到一打造完整鸿蒙系统音乐播放器应用 2

HarmonyOS音乐播放器项目文件2

**项目概述

这个音乐播放器应用支持本地音乐扫描、播放控制、歌词显示、主题切换等核心功能,采用了现代化的架构设计和用户界面,为用户提供流畅的音乐播放体验。**

**技术架构

核心技术栈
开发语言: ArkTS
开发框架: HarmonyOS UI Kit
开发工具: DevEco Studio
音频播放: AVPlayer
数据存储: SQLite**

复制代码
// utils/FileUtils.ets - 文件处理工具类
import { fileIo as fs } from '@kit.CoreFileKit';
import { util } from '@kit.ArkTS';
import { BusinessError } from '@kit.BasicServicesKit';

/**
 * 音乐信息接口
 */
export interface MusicInfo {
  title: string;
  artist: string;
}

/**
 * 文件处理工具类
 */
export class FileUtils {
  /**
   * 从 URI 提取文件名
   */
  static getFileNameFromUri(uri: string): string {
    const parts = uri.split('/');
    const fileName = parts[parts.length - 1];
    // URL 解码
    try {
      return decodeURIComponent(fileName);
    } catch {
      return fileName;
    }
  }

  /**
   * 从文件名提取音乐信息(标题和歌手)
   */
  static getMusicInfoFromFileName(fileName: string): MusicInfo {
    // 移除扩展名
    const lastDot = fileName.lastIndexOf('.');
    let nameWithoutExt = lastDot > 0 ? fileName.substring(0, lastDot) : fileName;

    // 常见格式: "歌手 - 歌名", "歌手-歌名"
    // 尝试用 " - " 分割
    let parts = nameWithoutExt.split(' - ');
    if (parts.length >= 2) {
      return { artist: parts[0].trim(), title: parts.slice(1).join(' - ').trim() };
    }

    // 尝试用 "-" 分割
    parts = nameWithoutExt.split('-');
    if (parts.length >= 2) {
      return { artist: parts[0].trim(), title: parts.slice(1).join('-').trim() };
    }

    // 尝试用 "_" 分割
    parts = nameWithoutExt.split('_');
    if (parts.length >= 2) {
      return { artist: parts[0].trim(), title: parts.slice(1).join('_').trim() };
    }

    // 无法解析,返回文件名作为标题
    return { title: nameWithoutExt, artist: '未知歌手' };
  }

  /**
   * 检查是否是音频文件
   */
  static isAudioFile(fileName: string): boolean {
    const audioExtensions = ['.mp3', '.flac', '.wav', '.aac', '.m4a', '.ogg', '.wma', '.ape'];
    const lowerName = fileName.toLowerCase();
    return audioExtensions.some(ext => lowerName.endsWith(ext));
  }

  /**
   * 获取文件大小
   */
  static async getFileSize(uri: string): Promise<number> {
    try {
      const file = fs.openSync(uri, fs.OpenMode.READ_ONLY);
      const stat = fs.statSync(file.fd);
      const size = stat.size;
      fs.closeSync(file);
      return size;
    } catch (error) {
      const err = error as BusinessError;
      console.warn(`[FileUtils] 获取文件大小失败: code=${err.code}, message=${err.message}`);
      return 0;
    }
  }

  /**
   * 支持的音频文件扩展名列表
   */
  static readonly AUDIO_EXTENSIONS = ['.mp3', '.flac', '.wav', '.aac', '.m4a', '.ogg', '.wma', '.ape'];

  /**
   * 检查是否是歌词文件
   */
  static isLyricFile(fileName: string): boolean {
    return fileName.toLowerCase().endsWith('.lrc');
  }

  /**
   * 根据音乐文件名获取对应的歌词文件名
   * 例:陈奕迅-富士山下.mp3 -> 陈奕迅-富士山下.lrc
   */
  static getLyricFileName(musicFileName: string): string {
    const lastDot = musicFileName.lastIndexOf('.');
    if (lastDot > 0) {
      return musicFileName.substring(0, lastDot) + '.lrc';
    }
    return musicFileName + '.lrc';
  }

  /**
   * 读取歌词文件内容(支持多种编码)
   */
  static async readLyricFile(uri: string): Promise<string> {
    try {
      const file = fs.openSync(uri, fs.OpenMode.READ_ONLY);
      const buffer = new ArrayBuffer(1024 * 100); // 100KB缓冲区
      const readLen = fs.readSync(file.fd, buffer);
      fs.closeSync(file);
  
      const uint8Array = new Uint8Array(buffer, 0, readLen);
        
      // 检测 BOM 标记
      const hasBOM = uint8Array.length >= 3 && 
                     uint8Array[0] === 0xEF && 
                     uint8Array[1] === 0xBB && 
                     uint8Array[2] === 0xBF;
        
      // 先尝试 UTF-8 解码
      try {
        const decoder = new util.TextDecoder('utf-8', { fatal: true, ignoreBOM: true });
        const content = decoder.decodeWithStream(uint8Array);
          
        // 验证解码结果:检查是否包含常见 LRC 标签
        if (content.includes('[') && content.includes(']')) {
          console.info(`[FileUtils] UTF-8 解码成功${hasBOM ? ' (with BOM)' : ''}`);
          return content;
        }
      } catch (utf8Error) {
        console.warn(`[FileUtils] UTF-8 解码失败,尝试其他编码`);
      }
        
      // 尝试 GBK/GB2312 等中文编码(通过 GB18030 解码)
      try {
        const decoder = new util.TextDecoder('gb18030', { fatal: false });
        const content = decoder.decodeWithStream(uint8Array);
        if (content.includes('[') && content.includes(']')) {
          console.info(`[FileUtils] GB18030 解码成功`);
          return content;
        }
      } catch (gbError) {
        console.warn(`[FileUtils] GB18030 解码失败`);
      }
        
      // 最后尝试宽松的 UTF-8 解码(不抛错误)
      const decoder = new util.TextDecoder('utf-8', { fatal: false, ignoreBOM: true });
      const content = decoder.decodeWithStream(uint8Array);
      console.info(`[FileUtils] 使用宽松 UTF-8 解码`);
      return content;
        
    } catch (error) {
      const err = error as BusinessError;
      console.error(`[FileUtils] 读取歌词文件失败: code=${err.code}, message=${err.message}`);
      return '';
    }
  }

  /**
   * 匹配歌词文件:支持多种匹配策略
   * @param musicFilePath 音乐文件完整路径
   * @returns 歌词文件路径,如果不存在则返回 null
   */
  static async findMatchingLyricFile(musicFilePath: string): Promise<string | null> {
    try {
      // 获取音乐文件所在目录
      const lastSlash = musicFilePath.lastIndexOf('/');
      if (lastSlash === -1) {
        return null;
      }
      const dirPath = musicFilePath.substring(0, lastSlash);
      const musicFileName = musicFilePath.substring(lastSlash + 1);
      
      // 移除扩展名
      const lastDot = musicFileName.lastIndexOf('.');
      const nameWithoutExt = lastDot > 0 ? musicFileName.substring(0, lastDot) : musicFileName;
      
      console.info(`[FileUtils] 开始匹配歌词: ${nameWithoutExt}`);
      
      // 策略 1:同名匹配(歌手-歌名.lrc)
      const strategy1 = `${dirPath}/${nameWithoutExt}.lrc`;
      if (await FileUtils.checkFileExists(strategy1)) {
        console.info(`[FileUtils] ✅ 策略 1 匹配成功: ${nameWithoutExt}.lrc`);
        return strategy1;
      }
      
      // 策略 2:尝试颜倒顺序(如果有 - 分隔符)
      if (nameWithoutExt.includes('-')) {
        const parts = nameWithoutExt.split('-');
        if (parts.length === 2) {
          const artist = parts[0].trim();
          const title = parts[1].trim();
          
          // 策略 2a:歌名-歌手.lrc
          const strategy2a = `${dirPath}/${title}-${artist}.lrc`;
          if (await FileUtils.checkFileExists(strategy2a)) {
            console.info(`[FileUtils] ✅ 策略 2a 匹配成功: ${title}-${artist}.lrc`);
            return strategy2a;
          }
          
          // 策略 2b:歌名 - 歌手.lrc(带空格)
          const strategy2b = `${dirPath}/${title} - ${artist}.lrc`;
          if (await FileUtils.checkFileExists(strategy2b)) {
            console.info(`[FileUtils] ✅ 策略 2b 匹配成功: ${title} - ${artist}.lrc`);
            return strategy2b;
          }
          
          // 策略 2c:只有歌名.lrc
          const strategy2c = `${dirPath}/${title}.lrc`;
          if (await FileUtils.checkFileExists(strategy2c)) {
            console.info(`[FileUtils] ✅ 策略 2c 匹配成功: ${title}.lrc`);
            return strategy2c;
          }
        }
      }
      
      // 策略 3:尝试空格分隔
      if (nameWithoutExt.includes(' ')) {
        const parts = nameWithoutExt.split(' ');
        if (parts.length >= 2) {
          // 去掉空元素
          const cleanParts = parts.filter(p => p.length > 0);
          if (cleanParts.length === 2) {
            const part1 = cleanParts[0];
            const part2 = cleanParts[1];
            
            // 策略 3a:part2-part1.lrc
            const strategy3a = `${dirPath}/${part2}-${part1}.lrc`;
            if (await FileUtils.checkFileExists(strategy3a)) {
              console.info(`[FileUtils] ✅ 策略 3a 匹配成功: ${part2}-${part1}.lrc`);
              return strategy3a;
            }
            
            // 策略 3b:part2 part1.lrc
            const strategy3b = `${dirPath}/${part2} ${part1}.lrc`;
            if (await FileUtils.checkFileExists(strategy3b)) {
              console.info(`[FileUtils] ✅ 策略 3b 匹配成功: ${part2} ${part1}.lrc`);
              return strategy3b;
            }
          }
        }
      }
      
      console.warn(`[FileUtils] ⚠️ 未找到匹配的歌词文件: ${nameWithoutExt}`);
      return null;
      
    } catch (error) {
      const err = error as BusinessError;
      console.warn(`[FileUtils] 匹配歌词失败: code=${err.code}, message=${err.message}`);
      return null;
    }
  }
  
  /**
   * 检查文件是否存在
   */
  private static async checkFileExists(filePath: string): Promise<boolean> {
    try {
      const file = fs.openSync(filePath, fs.OpenMode.READ_ONLY);
      fs.closeSync(file);
      return true;
    } catch (error) {
      return false;
    }
  }
}
复制代码
import media from '@ohos.multimedia.media';

// 音乐项接口
export interface MusicItem {
  id: string;
  title: string;
  artist: string;
  duration: number;
}

// 播放状态接口
export interface PlaybackState {
  currentSong: MusicItem | null;
  isPlaying: boolean;
  playlist: MusicItem[];
}

// 音乐服务类
export class MusicService {
  private static instance: MusicService;
  private player: media.AVPlayer | null = null;
  private currentSong: MusicItem | null = null;
  private isPlaying: boolean = false;
  private playlist: MusicItem[] = [];

  private constructor() {
    this.initPlaylist();
  }

  static getInstance(): MusicService {
    if (!MusicService.instance) {
      MusicService.instance = new MusicService();
    }
    return MusicService.instance;
  }

  // 初始化播放列表(使用模拟数据)
  private initPlaylist(): void {
    this.playlist = [
      {
        id: '1',
        title: '青花瓷',
        artist: '周杰伦',
        duration: 245000
      },
      {
        id: '2',
        title: '晴天',
        artist: '周杰伦',
        duration: 258000
      },
      {
        id: '3',
        title: '七里香',
        artist: '周杰伦',
        duration: 295000
      },
      {
        id: '4',
        title: '夜曲',
        artist: '周杰伦',
        duration: 223000
      },
      {
        id: '5',
        title: '简单爱',
        artist: '周杰伦',
        duration: 268000
      }
    ];
  }

  // 获取播放列表
  getPlaylist(): MusicItem[] {
    return this.playlist;
  }

  // 模拟播放(不使用真实文件)
  async play(song: MusicItem): Promise<boolean> {
    try {
      console.log('开始播放:', song.title);

      this.currentSong = song;

      // 创建播放器
      if (!this.player) {
        this.player = await media.createAVPlayer();
      }

      // 使用一个虚拟的URL(避免文件权限问题)
      // 在实际应用中,这里应该是真实的音乐文件路径
      this.player.url = 'resource://rawfile/music_sample.mp3';

      // 准备播放
      await this.player.prepare();

      // 开始播放
      await this.player.play();
      this.isPlaying = true;

      return true;
    } catch (error) {
      console.error('播放失败:', error);

      // 播放失败时,模拟播放成功状态
      this.currentSong = song;
      this.isPlaying = true;

      // 模拟播放进度
      this.simulatePlayback();

      return true; // 返回true让UI可以更新
    }
  }

  // 模拟播放进度
  private simulatePlayback(): void {
    // 在真实应用中,这里应该使用真实的播放器事件
    // 这里使用setTimeout模拟
    setTimeout(() => {
      this.isPlaying = false;
      // 通知UI播放结束
    }, 30000); // 30秒后自动停止
  }

  // 暂停播放
  async pause(): Promise<void> {
    if (this.player && this.isPlaying) {
      try {
        await this.player.pause();
      } catch (error) {
        console.error('暂停失败:', error);
      }
    }
    this.isPlaying = false;
  }

  // 继续播放
  async resume(): Promise<void> {
    if (this.player && !this.isPlaying) {
      try {
        await this.player.play();
        this.isPlaying = true;
      } catch (error) {
        console.error('继续播放失败:', error);
      }
    }
  }

  // 下一曲
  async next(): Promise<boolean> {
    if (!this.currentSong || this.playlist.length === 0) {
      return false;
    }

    const currentIndex = this.playlist.findIndex(song => song.id === this.currentSong!.id);
    const nextIndex = (currentIndex + 1) % this.playlist.length;

    if (nextIndex < this.playlist.length) {
      return await this.play(this.playlist[nextIndex]);
    }

    return false;
  }

  // 上一曲
  async previous(): Promise<boolean> {
    if (!this.currentSong || this.playlist.length === 0) {
      return false;
    }

    const currentIndex = this.playlist.findIndex(song => song.id === this.currentSong!.id);
    const prevIndex = currentIndex > 0 ? currentIndex - 1 : this.playlist.length - 1;

    return await this.play(this.playlist[prevIndex]);
  }

  // 获取当前播放状态
  getCurrentState(): PlaybackState {
    const state: PlaybackState = {
      currentSong: this.currentSong,
      isPlaying: this.isPlaying,
      playlist: this.playlist
    };
    return state;
  }

  // 停止播放并释放资源
  async stop(): Promise<void> {
    if (this.player) {
      try {
        await this.player.stop();
        await this.player.release();
        this.player = null;
      } catch (error) {
        console.error('停止播放失败:', error);
      }
    }
    this.isPlaying = false;
    this.currentSong = null;
  }
}
复制代码
// viewmodels/MusicViewModel.ets - 音乐业务逻辑视图模型
import { common, abilityAccessCtrl, Permissions } from '@kit.AbilityKit';
import { picker } from '@kit.CoreFileKit';
import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { dataSharePredicates } from '@kit.ArkData';
import { BusinessError } from '@kit.BasicServicesKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { MusicItem, PlayMode } from '../models/MusicItem';
import { MusicDatabase } from '../services/Database';
import { AudioPlayer } from '../services/AudioPlayer';
import { FileUtils } from '../utils/FileUtils';

const TAG = 'MusicViewModel';
const DOMAIN = 0x0001;

/**
 * 音乐业务逻辑视图模型
 * 负责管理音乐列表、播放控制、文件扫描等业务逻辑
 */
export class MusicViewModel {
  // 数据列表
  public musicList: MusicItem[] = [];
  public favoriteList: MusicItem[] = [];
  public isLoading: boolean = true;

  // 上下文和服务
  private context: common.UIAbilityContext;
  private database: MusicDatabase;
  private audioPlayer: AudioPlayer;

  // 回调函数
  private onDataChanged?: () => void;
  private onToast?: (message: string) => void;

  constructor(context: common.UIAbilityContext) {
    this.context = context;
    this.database = new MusicDatabase(context);
    this.audioPlayer = AudioPlayer.getInstance(context);
  }

  /**
   * 设置数据变化回调
   */
  setOnDataChanged(callback: () => void): void {
    this.onDataChanged = callback;
  }

  /**
   * 设置 Toast 提示回调
   */
  setOnToast(callback: (message: string) => void): void {
    this.onToast = callback;
  }

  /**
   * 显示提示消息
   */
  private showTip(message: string): void {
    if (this.onToast) {
      this.onToast(message);
    }
  }

  /**
   * 通知数据变化
   */
  private notifyDataChanged(): void {
    if (this.onDataChanged) {
      this.onDataChanged();
    }
  }

  /**
   * 获取收藏列表
   */
  async getFavoriteList(): Promise<MusicItem[]> {
    return await this.database.getFavoriteMusic();
  }

  /**
   * 初始化
   */
  async init(): Promise<void> {
    hilog.info(DOMAIN, TAG, '========== ViewModel 初始化 ==========');
    try {
      await this.requestPermissions();
      await this.database.initDatabase();
      await this.audioPlayer.init();
      await this.loadMusicList();
      this.setupPlayerListeners();
      hilog.info(DOMAIN, TAG, '========== ViewModel 初始化完成 ==========');
    } catch (error) {
      hilog.error(DOMAIN, TAG, '初始化失败: %{public}s', JSON.stringify(error));
      throw new Error('ViewModel 初始化失败: ' + JSON.stringify(error));
    }
  }

  /**
   * 请求媒体权限
   */
  async requestPermissions(): Promise<void> {
    console.info('[MusicViewModel] ===== 开始请求权限 =====');
    const permissions: Permissions[] = [
      'ohos.permission.READ_MEDIA',
      'ohos.permission.WRITE_MEDIA',
      'ohos.permission.READ_AUDIO',
      'ohos.permission.READ_IMAGEVIDEO'
    ];

    try {
      const atManager = abilityAccessCtrl.createAtManager();
      const result = await atManager.requestPermissionsFromUser(this.context, permissions);

      let allGranted = true;
      for (let i = 0; i < result.authResults.length; i++) {
        const granted = result.authResults[i] === 0;
        console.info(`[MusicViewModel] ${permissions[i]}: ${granted ? '✅' : '❌'}`);
        if (!granted) {
          allGranted = false;
        }
      }

      if (!allGranted) {
        this.showTip('部分权限未授权,可能无法扫描本地音乐');
      }
    } catch (error) {
      const err = error as BusinessError;
      console.error(`[MusicViewModel] 请求权限失败: ${err.message}`);
    }
    console.info('[MusicViewModel] ===== 权限请求结束 =====');
  }

  /**
   * 加载音乐列表
   */
  async loadMusicList(): Promise<void> {
    this.isLoading = true;
    this.notifyDataChanged();

    try {
      this.musicList = await this.database.getAllMusic();
      this.favoriteList = await this.database.getFavoriteMusic();
      this.audioPlayer.setPlaylist(this.musicList);
      hilog.info(DOMAIN, TAG, '加载音乐列表: %{public}d 首', this.musicList.length);
    } catch (error) {
      console.error(`加载音乐列表失败: ${JSON.stringify(error)}`);
    } finally {
      this.isLoading = false;
      this.notifyDataChanged();
    }
  }

  /**
   * 设置播放器监听器
   */
  setupPlayerListeners(): void {
    this.audioPlayer.setOnStateChangeListener((state: string) => {
      hilog.info(DOMAIN, TAG, '播放状态变化: %{public}s', state);
    });

    this.audioPlayer.setOnMusicChangeListener((music: MusicItem) => {
      hilog.info(DOMAIN, TAG, '音乐变化: %{public}s', music.title);
    });

    // 监听收藏状态变化
    this.audioPlayer.setOnFavoriteChangeListener((musicId: number, isFavorite: boolean) => {
      console.info(`[MusicViewModel] 收藏状态变化: musicId=${musicId}, isFavorite=${isFavorite}`);
      this.updateMusicFavoriteState(musicId, isFavorite);
    });
  }

  /**
   * 更新音乐收藏状态
   */
  private updateMusicFavoriteState(musicId: number, isFavorite: boolean): void {
    const newList: MusicItem[] = [];
    for (const m of this.musicList) {
      if (m.id === musicId) {
        newList.push({
          id: m.id,
          title: m.title,
          artist: m.artist,
          album: m.album,
          duration: m.duration,
          filePath: m.filePath,
          cover: m.cover,
          fileName: m.fileName,
          fileSize: m.fileSize,
          createTime: m.createTime,
          playCount: m.playCount,
          isFavorite: isFavorite,
          lyrics: m.lyrics
        });
      } else {
        newList.push(m);
      }
    }
    this.musicList = newList;

    // 重新加载收藏列表
    this.database.getFavoriteMusic().then((favorites) => {
      this.favoriteList = favorites;
      this.notifyDataChanged();
    });
  }

  /**
   * 播放音乐
   */
  async playMusic(music: MusicItem): Promise<void> {
    console.info(`[MusicViewModel] 播放音乐: ${music.title}`);
    try {
      // 如果该音乐没有歌词,尝试自动匹配
      if (!music.lyrics || music.lyrics.length === 0) {
        await this.autoMatchLyrics(music);
      }

      await this.audioPlayer.play(music);
      await this.database.updatePlayCount(music.id);
      console.info(`[MusicViewModel] ✅ 播放已启动`);
    } catch (error) {
      const err = error as BusinessError;
      console.error(`[MusicViewModel] 播放失败: ${err.message}`);
      this.showTip('播放失败: ' + (err.message || '未知错误'));
      throw new Error('播放音乐失败: ' + err.message);
    }
  }

  /**
   * 自动匹配单首音乐的歌词
   */
  private async autoMatchLyrics(music: MusicItem): Promise<void> {
    try {
      const lyricPath = await FileUtils.findMatchingLyricFile(music.filePath);
      if (lyricPath) {
        const lyricsContent = await FileUtils.readLyricFile(lyricPath);
        if (lyricsContent) {
          // 更新数据库
          await this.database.updateMusicLyrics(music.id, lyricsContent);
          // 更新内存中的对象
          music.lyrics = lyricsContent;
          // 更新列表
          const index = this.musicList.findIndex(m => m.id === music.id);
          if (index !== -1) {
            this.musicList[index].lyrics = lyricsContent;
          }
          // 同步到全局状态(关键:确保 PlayPage 立即更新)
          AppStorage.setOrCreate('currentMusicLyrics', lyricsContent);
          console.info(`[MusicViewModel] 自动匹配歌词: ${music.title}`);
        }
      }
    } catch (error) {
      // 静默失败,不影响播放
      console.warn(`[MusicViewModel] 自动匹配歌词失败: ${JSON.stringify(error)}`);
    }
  }

  /**
   * 切换播放/暂停
   */
  async togglePlayPause(isPlaying: boolean, currentMusicId: number): Promise<void> {
    try {
      if (isPlaying) {
        await this.audioPlayer.pause();
      } else {
        if (currentMusicId > 0) {
          await this.audioPlayer.resume();
        } else if (this.musicList.length > 0) {
          await this.playMusic(this.musicList[0]);
        } else {
          this.showTip('请先添加音乐');
        }
      }
    } catch (error) {
      console.error(`[MusicViewModel] 切换播放/暂停失败: ${JSON.stringify(error)}`);
      this.showTip('操作失败');
      throw new Error('切换播放状态失败: ' + JSON.stringify(error));
    }
  }

  /**
   * 切换收藏
   */
  async toggleFavorite(music: MusicItem): Promise<boolean> {
    console.info(`[MusicViewModel] 切换收藏: ${music.title}`);
    const newFavoriteState = await this.database.toggleFavorite(music.id);

    // 更新列表
    this.updateMusicFavoriteState(music.id, newFavoriteState);

    // 通知播放器
    this.audioPlayer.notifyFavoriteChanged(music.id, newFavoriteState);

    this.showTip(newFavoriteState ? '已添加到收藏' : '已取消收藏');
    
    return newFavoriteState;
  }

  /**
   * 删除音乐
   */
  async deleteMusic(music: MusicItem, currentMusicId: number): Promise<void> {
    try {
      // 如果正在播放该音乐,先停止播放
      if (currentMusicId === music.id) {
        await this.audioPlayer.stop();
      }

      const success = await this.database.deleteMusic(music.id);
      if (success) {
        await this.loadMusicList();
        this.showTip(`已删除: ${music.title}`);
      } else {
        this.showTip('删除失败');
      }
    } catch (error) {
      console.error(`删除音乐失败: ${JSON.stringify(error)}`);
      this.showTip('删除失败');
      throw new Error('删除音乐失败: ' + JSON.stringify(error));
    }
  }

  /**
   * 搜索音乐
   */
  async searchMusic(keyword: string): Promise<void> {
    if (keyword.trim()) {
      this.isLoading = true;
      this.notifyDataChanged();
      this.musicList = await this.database.searchMusic(keyword);
      this.isLoading = false;
      this.notifyDataChanged();
    } else {
      await this.loadMusicList();
    }
  }

  /**
   * 扫描媒体库 - 自动添加歌曲(一键全自动,无需手动选择)
   */
  async scanMediaLibrary(): Promise<void> {
    console.info('[MusicViewModel] ===== 自动添加歌曲(一键全自动) =====');
    
    // 先尝试使用选择器让用户选择
    // 如果用户取消,则提示使用"选择音乐文件"功能
    try {
      const audioPicker = new picker.AudioViewPicker(this.context);
      const selectOptions = new picker.AudioSelectOptions();
      selectOptions.maxSelectNumber = 500;
      
      console.info('[MusicViewModel] 打开音频选择器(支持批量选择,最多500首)...');
      const result = await audioPicker.select(selectOptions);

      if (!result || result.length === 0) {
        console.info('[MusicViewModel] 用户取消选择');
        this.showTip('已取消');
        return;
      }

      await this.processMusicFiles(result);
    } catch (error) {
      const err = error as BusinessError;
      console.error(`[MusicViewModel] 选择音频失败: ${err.message}`);
      this.showTip('选择音频失败');
    }
  }

  /**
   * 处理音乐文件(通用方法)
   */
  private async processMusicFiles(uriList: string[]): Promise<void> {
    let addedCount = 0;
    let skippedCount = 0;
    let lyricsCount = 0;

    console.info(`[MusicViewModel] 开始处理 ${uriList.length} 个音频文件...`);

    for (const uri of uriList) {
      try {
        // 检查是否已存在
        const exists = await this.database.isMusicExists(uri);
        if (exists) {
          skippedCount++;
          continue;
        }

        // 获取文件信息
        const fileName = FileUtils.getFileNameFromUri(uri);
        const musicInfo = FileUtils.getMusicInfoFromFileName(fileName);
        const fileSize = await FileUtils.getFileSize(uri);

        // 自动匹配歌词文件
        const lyricPath = await FileUtils.findMatchingLyricFile(uri);
        let lyricsContent = '';
        if (lyricPath) {
          lyricsContent = await FileUtils.readLyricFile(lyricPath);
          if (lyricsContent) {
            lyricsCount++;
            console.info(`[MusicViewModel] 自动匹配歌词: ${fileName}`);
          }
        }

        const musicItem: MusicItem = {
          id: Date.now() + addedCount,
          title: musicInfo.title,
          artist: musicInfo.artist,
          album: '未知专辑',
          duration: 0,
          filePath: uri,
          cover: $r('app.media.ic_album'),
          fileName: fileName,
          fileSize: fileSize,
          createTime: Date.now(),
          playCount: 0,
          isFavorite: false,
          lyrics: lyricsContent
        };

        await this.database.addMusic(musicItem);
        addedCount++;
        console.info(`[MusicViewModel] 已添加: ${fileName}`);
      } catch (fileError) {
        const err = fileError as BusinessError;
        console.error(`[MusicViewModel] 处理文件失败: ${err.message}`);
      }
    }

    await this.loadMusicList();

    console.info(`[MusicViewModel] ========== 处理结果 ==========`);
    console.info(`[MusicViewModel]   总文件: ${uriList.length}`);
    console.info(`[MusicViewModel]   新增: ${addedCount}`);
    console.info(`[MusicViewModel]   跳过: ${skippedCount}`);
    console.info(`[MusicViewModel]   匹配歌词: ${lyricsCount}`);
    console.info(`[MusicViewModel] ================================`);

    if (addedCount > 0) {
      let message = `成功添加 ${addedCount} 首歌曲`;
      if (lyricsCount > 0) {
        message += `,${lyricsCount} 首已匹配歌词`;
      }
      if (skippedCount > 0) {
        message += `,跳过 ${skippedCount} 首已存在`;
      }
      this.showTip(message);
    } else if (skippedCount > 0) {
      this.showTip(`所有歌曲已存在`);
    } else {
      this.showTip('未添加任何文件');
    }
  }

  /**
   * 选择音乐文件 (AudioViewPicker)
   */
  async selectMusicFiles(): Promise<void> {
    console.info('[MusicViewModel] ===== AudioViewPicker 选择音乐 =====');
    try {
      const audioPicker = new picker.AudioViewPicker(this.context);
      const selectOptions = new picker.AudioSelectOptions();
      selectOptions.maxSelectNumber = 500;  // 最多选择 500 首
        
      const result = await audioPicker.select(selectOptions);
  
      if (result && result.length > 0) {
        let addedCount = 0;
        let skippedCount = 0;
        let lyricsCount = 0;
  
        for (const uri of result) {
          try {
            // 检查是否已存在
            const exists = await this.database.isMusicExists(uri);
            if (exists) {
              skippedCount++;
              continue;
            }
  
            // 获取文件信息
            const fileName = FileUtils.getFileNameFromUri(uri);
            const musicInfo = FileUtils.getMusicInfoFromFileName(fileName);
            const fileSize = await FileUtils.getFileSize(uri);
  
            // 自动匹配歌词文件
            const lyricPath = await FileUtils.findMatchingLyricFile(uri);
            let lyricsContent = '';
            if (lyricPath) {
              lyricsContent = await FileUtils.readLyricFile(lyricPath);
              if (lyricsContent) {
                lyricsCount++;
                console.info(`[MusicViewModel] 自动匹配歌词: ${fileName}`);
              }
            }
  
            const musicItem: MusicItem = {
              id: Date.now() + addedCount,
              title: musicInfo.title,
              artist: musicInfo.artist,
              album: '未知专辑',
              duration: 0,
              filePath: uri,
              cover: $r('app.media.ic_album'),
              fileName: fileName,
              fileSize: fileSize,
              createTime: Date.now(),
              playCount: 0,
              isFavorite: false,
              lyrics: lyricsContent
            };
  
            await this.database.addMusic(musicItem);
            addedCount++;
          } catch (fileError) {
            const err = fileError as BusinessError;
            console.error(`[MusicViewModel] 处理文件失败: ${err.message}`);
          }
        }
  
        await this.loadMusicList();
  
        if (addedCount > 0) {
          let message = `添加 ${addedCount} 首音乐`;
          if (lyricsCount > 0) {
            message += `,${lyricsCount} 首已匹配歌词`;
          }
          if (skippedCount > 0) {
            message += `,跳过 ${skippedCount} 首已存在`;
          }
          this.showTip(message);
        } else if (skippedCount > 0) {
          this.showTip(`所有歌曲已存在`);
        } else {
          this.showTip('未添加任何文件');
        }
      } else {
        this.showTip('未选择文件');
      }
    } catch (error) {
      const err = error as BusinessError;
      console.error(`[MusicViewModel] 选择文件失败: ${err.message}`);
      this.showTip('选择文件失败');
      throw new Error('选择文件失败: ' + err.message);
    }
  }

  /**
   * 扫描本地音乐 (DocumentViewPicker)
   */
  async scanLocalMusic(): Promise<void> {
    console.info('[MusicViewModel] ===== DocumentViewPicker 扫描本地音乐 =====');
    try {
      const documentSelectOptions = new picker.DocumentSelectOptions();
      documentSelectOptions.maxSelectNumber = 100;
      documentSelectOptions.fileSuffixFilters = FileUtils.AUDIO_EXTENSIONS;

      const documentPicker = new picker.DocumentViewPicker(this.context);
      const result = await documentPicker.select(documentSelectOptions);

      if (result && result.length > 0) {
        let addedCount = 0;
        let skippedCount = 0;
        let lyricsCount = 0;

        for (const uri of result) {
          try {
            // 检查是否已存在
            const exists = await this.database.isMusicExists(uri);
            if (exists) {
              skippedCount++;
              continue;
            }

            const fileName = FileUtils.getFileNameFromUri(uri);
            // 检查是否是音频文件
            if (!FileUtils.isAudioFile(fileName)) {
              continue;
            }

            const musicInfo = FileUtils.getMusicInfoFromFileName(fileName);
            const fileSize = await FileUtils.getFileSize(uri);

            // 自动匹配歌词文件
            const lyricPath = await FileUtils.findMatchingLyricFile(uri);
            let lyricsContent = '';
            if (lyricPath) {
              lyricsContent = await FileUtils.readLyricFile(lyricPath);
              if (lyricsContent) {
                lyricsCount++;
                console.info(`[MusicViewModel] 自动匹配歌词: ${fileName}`);
              }
            }

            const musicItem: MusicItem = {
              id: Date.now() + addedCount,
              title: musicInfo.title,
              artist: musicInfo.artist,
              album: '未知专辑',
              duration: 0,
              filePath: uri,
              cover: $r('app.media.ic_album'),
              fileName: fileName,
              fileSize: fileSize,
              createTime: Date.now(),
              playCount: 0,
              isFavorite: false,
              lyrics: lyricsContent
            };

            await this.database.addMusic(musicItem);
            addedCount++;
          } catch (fileError) {
            const err = fileError as BusinessError;
            console.error(`[MusicViewModel] 处理文件失败: ${err.message}`);
          }
        }

        await this.loadMusicList();

        if (addedCount > 0) {
          let message = `扫描完成,添加 ${addedCount} 首`;
          if (lyricsCount > 0) {
            message += `,${lyricsCount} 首已匹配歌词`;
          }
          if (skippedCount > 0) {
            message += `,跳过 ${skippedCount} 首`;
          }
          this.showTip(message);
        } else if (skippedCount > 0) {
          this.showTip('所有歌曲已存在');
        } else {
          this.showTip('未找到音乐文件');
        }
      } else {
        this.showTip('未选择文件');
      }
    } catch (error) {
      const err = error as BusinessError;
      console.error(`[MusicViewModel] 扫描本地音乐失败: ${err.message}`);
      this.showTip('扫描失败');
      throw new Error('扫描本地音乐失败: ' + err.message);
    }
  }

  /**
   * 清空音乐库
   */
  async clearAllMusic(currentMusicId: number): Promise<void> {
    console.info('[MusicViewModel] ===== 清空音乐库 =====');
    try {
      // 停止当前播放
      if (currentMusicId > 0) {
        await this.audioPlayer.stop();
      }

      const success = await this.database.clearAllMusic();
      if (success) {
        this.musicList = [];
        this.favoriteList = [];
        this.notifyDataChanged();
        this.showTip('已清空音乐库');
      } else {
        this.showTip('清空失败');
      }
    } catch (error) {
      const err = error as BusinessError;
      console.error(`[MusicViewModel] 清空音乐库失败: ${err.message}`);
      this.showTip('清空失败');
      throw new Error('清空音乐库失败: ' + err.message);
    }
  }

  /**
   * 为已有音乐扫描匹配歌词
   */
  async scanLyricsForExistingMusic(): Promise<void> {
    console.info('[MusicViewModel] ===== 扫描匹配歌词 =====');
    try {
      this.isLoading = true;
      this.notifyDataChanged();
  
      let matchedCount = 0;
      let totalCount = this.musicList.length;
      const currentMusic = this.audioPlayer.getCurrentMusic();  // 获取当前播放的音乐
  
      for (const music of this.musicList) {
        // 跳过已有歌词的音乐
        if (music.lyrics && music.lyrics.length > 0) {
          continue;
        }
  
        // 尝试匹配歌词
        const lyricPath = await FileUtils.findMatchingLyricFile(music.filePath);
        if (lyricPath) {
          const lyricsContent = await FileUtils.readLyricFile(lyricPath);
          if (lyricsContent) {
            // 更新数据库
            await this.database.updateMusicLyrics(music.id, lyricsContent);
            matchedCount++;
            console.info(`[MusicViewModel] 匹配歌词: ${music.title}`);
              
            // 如果是当前播放的音乐,立即同步到全局状态
            if (currentMusic && currentMusic.id === music.id) {
              AppStorage.setOrCreate('currentMusicLyrics', lyricsContent);
              console.info(`[MusicViewModel] 已同步当前播放音乐的歌词`);
            }
          }
        }
      }
  
      // 重新加载音乐列表
      await this.loadMusicList();
      this.isLoading = false;
      this.notifyDataChanged();
  
      if (matchedCount > 0) {
        this.showTip(`扫描完成,为 ${matchedCount}/${totalCount} 首音乐匹配了歌词`);
      } else {
        this.showTip('未找到新的歌词文件');
      }
    } catch (error) {
      this.isLoading = false;
      this.notifyDataChanged();
      const err = error as BusinessError;
      console.error(`[MusicViewModel] 扫描歌词失败: ${err.message}`);
      this.showTip('扫描歌词失败');
      throw new Error('扫描歌词失败: ' + err.message);
    }
  }

  /**
   * 添加模拟音乐数据
   */
  async addMockMusic(): Promise<void> {
    const mockMusics: MusicItem[] = [
      {
        id: Date.now(),
        title: '示例歌曲1',
        artist: '未知歌手',
        album: '未知专辑',
        duration: 269000,
        filePath: '/data/storage/el2/base/files/song1.mp3',
        cover: $r('app.media.ic_album'),
        fileName: 'song1.mp3',
        fileSize: 1024 * 1024 * 8,
        createTime: Date.now(),
        playCount: 0,
        isFavorite: false,
        lyrics: ''
      },
      {
        id: Date.now() + 1,
        title: '示例歌曲2',
        artist: '未知歌手',
        album: '未知专辑',
        duration: 223000,
        filePath: '/data/storage/el2/base/files/song2.mp3',
        cover: $r('app.media.ic_album'),
        fileName: 'song2.mp3',
        fileSize: 1024 * 1024 * 6,
        createTime: Date.now() + 1,
        playCount: 0,
        isFavorite: true,
        lyrics: ''
      }
    ];

    try {
      for (const music of mockMusics) {
        await this.database.addMusic(music);
      }
      await this.loadMusicList();
      this.showTip('添加 2 首示例音乐');
    } catch (error) {
      console.error(`添加音乐失败: ${JSON.stringify(error)}`);
      this.showTip('添加失败');
      throw new Error('添加模拟音乐失败: ' + JSON.stringify(error));
    }
  }

  /**
   * 获取播放器实例
   */
  getAudioPlayer(): AudioPlayer {
    return this.audioPlayer;
  }

  /**
   * 释放资源
   */
  release(): void {
    this.audioPlayer.release();
  }
}
复制代码
// pages/Index.ets - 音乐播放器主页面(重构版)
import { router } from '@kit.ArkUI';
import { common } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';
import { preferences } from '@kit.ArkData';
import { MusicItem, PlayMode, formatTime } from './models/MusicItem';
import { MusicViewModel } from './viewmodels/MusicViewModel';
import { PlaylistDrawer } from './components/PlaylistDrawer';
import { BusinessError } from '@kit.BasicServicesKit';

const TAG = 'MusicApp';
const DOMAIN = 0x0001;

@Entry
@Component
struct Index {
  // ViewModel
  private viewModel: MusicViewModel = new MusicViewModel(AppStorage.get<common.UIAbilityContext>('context') as common.UIAbilityContext);
  
  // UI 状态
  @State musicList: MusicItem[] = [];
  @State favoriteList: MusicItem[] = [];
  @State isLoading: boolean = true;
  @State searchKeyword: string = '';
  @State currentTab: number = 0; // 0: 全部, 1: 收藏
  @State isScanning: boolean = false;
  @State showScanMenu: boolean = false;
  @State showPlaylistDrawer: boolean = false;
  @State toastMessage: string = '';
  @State showToast: boolean = false;
  @State isDarkTheme: boolean = false; // 主题切换
  
  // 使用 @StorageLink 实现全局播放状态同步
  @StorageLink('currentMusicId') currentMusicId: number = 0;
  @StorageLink('currentMusicTitle') currentMusicTitle: string = '';
  @StorageLink('currentMusicArtist') currentMusicArtist: string = '';
  @StorageLink('currentMusicCover') currentMusicCover: ResourceStr = '';
  @StorageLink('isPlaying') isPlaying: boolean = false;
  @StorageLink('currentPosition') currentPosition: number = 0;
  @StorageLink('playMode') playMode: PlayMode = PlayMode.LOOP;

  async aboutToAppear(): Promise<void> {
    hilog.info(DOMAIN, TAG, '========== 应用启动 ==========');
    
    // 从本地存储加载主题设置
    try {
      const context = AppStorage.get<common.UIAbilityContext>('context');
      if (context) {
        const dataPreferences = preferences.getPreferencesSync(context, { name: 'app_settings' });
        const savedTheme = dataPreferences.getSync('isDarkTheme', false) as boolean;
        this.isDarkTheme = savedTheme;
        hilog.info(DOMAIN, TAG, '加载保存的主题设置: %{public}s', savedTheme ? '暗色' : '浅色');
      }
    } catch (error) {
      hilog.error(DOMAIN, TAG, '加载主题设置失败: %{public}s', JSON.stringify(error));
    }
    
    // 初始化主题状态到 AppStorage
    AppStorage.setOrCreate('isDarkTheme', this.isDarkTheme);
    
    // 更新状态栏
    this.updateStatusBar();
    
    try {
      // 设置 ViewModel 回调
      this.viewModel.setOnDataChanged(() => {
        this.syncFromViewModel();
      });
      this.viewModel.setOnToast((message: string) => {
        this.showTip(message);
      });

      // 初始化 ViewModel
      await this.viewModel.init();
      
      // 同步数据
      this.syncFromViewModel();
      
      hilog.info(DOMAIN, TAG, '========== 应用初始化完成 ==========');
    } catch (error) {
      hilog.error(DOMAIN, TAG, '初始化失败: %{public}s', JSON.stringify(error));
    }
  }

  // 更新系统状态栏
  private updateStatusBar(): void {
    const mainWindow = AppStorage.get<window.Window>('mainWindow');
    if (mainWindow) {
      const statusBarColor = this.isDarkTheme ? '#1a1a2e' : '#F8F9FA';
      const statusBarContentColor = this.isDarkTheme ? '#FFFFFF' : '#000000';
      mainWindow.setWindowSystemBarProperties({
        statusBarColor: statusBarColor,
        statusBarContentColor: statusBarContentColor
      }).catch((error: Error) => {
        hilog.error(DOMAIN, TAG, '设置状态栏失败: %{public}s', JSON.stringify(error));
      });
    }
  }

  // 页面显示时刷新数据(从播放页返回时)
  onPageShow(): void {
    hilog.info(DOMAIN, TAG, '========== 页面显示 - 刷新数据 ==========');
    this.viewModel.loadMusicList();
    hilog.info(DOMAIN, TAG, '当前播放: %{public}s, 状态: %{public}s', 
      this.currentMusicTitle || '无', this.isPlaying ? '播放中' : '暂停');
  }

  // 同步 ViewModel 数据到 UI
  private syncFromViewModel(): void {
    // 使用数组切片创建新数组,触发响应式更新
    this.musicList = this.viewModel.musicList.slice();
    this.favoriteList = this.viewModel.favoriteList.slice();
    this.isLoading = this.viewModel.isLoading;
  }

  // 播放音乐
  async playMusic(music: MusicItem): Promise<void> {
    try {
      await this.viewModel.playMusic(music);
    } catch (error) {
      // 错误已在 ViewModel 中处理
    }
  }

  // 切换播放/暂停
  async togglePlayPause(): Promise<void> {
    try {
      await this.viewModel.togglePlayPause(this.isPlaying, this.currentMusicId);
    } catch (error) {
      // 错误已在 ViewModel 中处理
    }
  }

  // 切换收藏
  async toggleFavorite(music: MusicItem): Promise<void> {
    console.info(`[Index] 切换收藏: ${music.title}, 当前状态: ${music.isFavorite}`);
    
    const newFavoriteState = await this.viewModel.toggleFavorite(music);
    
    // 找到数组索引
    const musicIndex = this.musicList.findIndex(m => m.id === music.id);
    if (musicIndex !== -1) {
      // 创建新对象(避免展开语法)
      const oldMusic = this.musicList[musicIndex];
      const updatedMusic: MusicItem = {
        id: oldMusic.id,
        title: oldMusic.title,
        artist: oldMusic.artist,
        album: oldMusic.album,
        duration: oldMusic.duration,
        filePath: oldMusic.filePath,
        cover: oldMusic.cover,
        fileName: oldMusic.fileName,
        fileSize: oldMusic.fileSize,
        createTime: oldMusic.createTime,
        playCount: oldMusic.playCount,
        isFavorite: newFavoriteState,
        lyrics: oldMusic.lyrics
      };
      
      // 使用 splice 替换指定索引的元素(触发 UI 更新)
      this.musicList.splice(musicIndex, 1, updatedMusic);
      console.info(`[Index] ✅ 索引 ${musicIndex} 已更新, isFavorite=${newFavoriteState}`);
    }
    
    // 同步收藏列表
    this.favoriteList = await this.viewModel.getFavoriteList();
    
    console.info(`[Index] ✅ 收藏列表已更新: ${this.favoriteList.length} 首`);
  }

  // 搜索音乐
  async searchMusic(): Promise<void> {
    await this.viewModel.searchMusic(this.searchKeyword);
  }

  // 删除音乐
  async deleteMusic(music: MusicItem): Promise<void> {
    try {
      await this.viewModel.deleteMusic(music, this.currentMusicId);
    } catch (error) {
      // 错误已在 ViewModel 中处理
    }
  }

  // 清空音乐库
  async clearAllMusic(): Promise<void> {
    try {
      await this.viewModel.clearAllMusic(this.currentMusicId);
    } catch (error) {
      // 错误已在 ViewModel 中处理
    }
  }

  // 扫描媒体库
  async scanMediaLibrary(): Promise<void> {
    this.isScanning = true;
    try {
      await this.viewModel.scanMediaLibrary();
    } catch (error) {
      // 回退到 AudioViewPicker
      await this.selectMusicFiles();
    } finally {
      this.isScanning = false;
    }
  }

  // 选择音乐文件
  async selectMusicFiles(): Promise<void> {
    this.isScanning = true;
    try {
      await this.viewModel.selectMusicFiles();
    } catch (error) {
      // 错误已在 ViewModel 中处理
    } finally {
      this.isScanning = false;
    }
  }

  // 扫描本地音乐
  async scanLocalMusic(): Promise<void> {
    this.isScanning = true;
    try {
      await this.viewModel.scanLocalMusic();
    } catch (error) {
      // 错误已在 ViewModel 中处理
    } finally {
      this.isScanning = false;
    }
  }

  // 扫描匹配歌词
  async scanLyrics(): Promise<void> {
    this.isScanning = true;
    try {
      await this.viewModel.scanLyricsForExistingMusic();
    } catch (error) {
      // 错误已在 ViewModel 中处理
    } finally {
      this.isScanning = false;
    }
  }

  // 添加模拟数据
  async addMockMusic(): Promise<void> {
    try {
      await this.viewModel.addMockMusic();
    } catch (error) {
      // 错误已在 ViewModel 中处理
    }
  }

  // 获取播放模式图标
  getPlayModeIcon(): Resource {
    switch (this.playMode) {
      case PlayMode.SINGLE:
        return $r('app.media.ic_repeat_one');
      case PlayMode.RANDOM:
        return $r('app.media.ic_shuffle');
      case PlayMode.LOOP:
        return $r('app.media.ic_repeat');
      default:
        return $r('app.media.ic_repeat');
    }
  }

  // 获取播放模式名称
  getPlayModeName(): string {
    switch (this.playMode) {
      case PlayMode.SINGLE:
        return '单曲循环';
      case PlayMode.RANDOM:
        return '随机播放';
      case PlayMode.LOOP:
        return '列表循环';
      default:
        return '顺序播放';
    }
  }

  build() {
    Stack({ alignContent: Alignment.Bottom }) {
      Column() {
        // 顶部标题栏
        this.buildHeader()

        // 搜索栏
        this.buildSearchBar()

        // Tab切换
        this.buildTabs()

        // 音乐列表
        if (this.isLoading) {
          this.buildLoading()
        } else if (this.getCurrentList().length === 0) {
          this.buildEmpty()
        } else {
          this.buildMusicList()
        }
      }
      .width('100%')
      .height('100%')
      .linearGradient({
        angle: 135,
        colors: this.isDarkTheme ? [
          ['#1a1a2e', 0.0],   // 深蓝灰
          ['#16213e', 0.3],   // 暗蓝色
          ['#0f3460', 0.7],   // 深蓝色
          ['#c41e3a', 1.0]    // 鲜红色
        ] : [
          ['#F8F9FA', 0.0],   // 浅灰白
          ['#E8EAF6', 0.5],   // 淡紫色
          ['#E3F2FD', 1.0]    // 淡蓝色
        ]
      })
      .padding({ bottom: this.currentMusicId > 0 ? 80 : 0 })

      // 底部播放控制栏
      if (this.currentMusicId > 0) {
        this.buildPlayerBar()
      }

      // 播放列表抽屉
      if (this.showPlaylistDrawer) {
        PlaylistDrawer({
          showDrawer: $showPlaylistDrawer,
          playlist: this.viewModel.getAudioPlayer().getPlaylist(),
          currentMusicId: this.currentMusicId,
          onPlayMusic: (music: MusicItem) => {
            this.playMusic(music);
          }
        })
      }

      // Toast 提示
      if (this.showToast) {
        Text(this.toastMessage)
          .fontSize(14)
          .fontColor('#FFFFFF')
          .backgroundColor(this.isDarkTheme ? 'rgba(255, 65, 88, 0.9)' : 'rgba(0, 0, 0, 0.75)')
          .padding({
            left: 20,
            right: 20,
            top: 12,
            bottom: 12
          })
          .borderRadius(20)
          .position({ x: '50%', y: '45%' })
          .translate({ x: '-50%' })
          .zIndex(100)
      }

      // 扫描菜单(移到Stack顶层)
      if (this.showScanMenu) {
        // 透明背景层,点击关闭菜单
        Column()
          .width('100%')
          .height('100%')
          .backgroundColor('rgba(0,0,0,0.3)')
          .onClick(() => {
            this.showScanMenu = false;
          })
          .zIndex(200)

        Column() {
          // 自动添加歌曲(批量选择)
          Row() {
            Image($r('app.media.ic_scan'))
              .width(24)
              .height(24)
              .fillColor(this.isDarkTheme ? '#FF4158' : '#667EEA')
            Column() {
              Text('自动添加歌曲')
                .fontSize(15)
                .fontColor(this.isDarkTheme ? '#FF4158' : '#667EEA')
                .fontWeight(FontWeight.Medium)
              Text('批量选择,最多500首')
                .fontSize(11)
                .fontColor(this.isDarkTheme ? '#FF8C82' : '#667EEA')
                .opacity(0.7)
                .margin({ top: 2 })
            }
            .alignItems(HorizontalAlign.Start)
            .margin({ left: 12 })
          }
          .width('100%')
          .height(60)
          .padding({ left: 16, right: 16 })
          .onClick(() => {
            this.showScanMenu = false;
            this.scanMediaLibrary();
          })

          Divider().strokeWidth(0.5).color(this.isDarkTheme ? 'rgba(255, 255, 255, 0.1)' : '#F0F0F0')

          // 选择音乐文件
          Row() {
            Image($r('app.media.ic_folder'))
              .width(24)
              .height(24)
              .fillColor(this.isDarkTheme ? '#AAAAAA' : '#666666')
            Text('选择音乐文件')
              .fontSize(15)
              .fontColor(this.isDarkTheme ? '#FFFFFF' : '#333333')
              .margin({ left: 12 })
          }
          .width('100%')
          .height(52)
          .padding({ left: 16, right: 16 })
          .onClick(() => {
            this.showScanMenu = false;
            this.selectMusicFiles();
          })

          Divider().strokeWidth(0.5).color(this.isDarkTheme ? 'rgba(255, 255, 255, 0.1)' : '#F0F0F0')

          // 扫描本地音乐
          Row() {
            Image($r('app.media.ic_search'))
              .width(24)
              .height(24)
              .fillColor(this.isDarkTheme ? '#AAAAAA' : '#666666')
            Text('文档选择器')
              .fontSize(15)
              .fontColor(this.isDarkTheme ? '#FFFFFF' : '#333333')
              .margin({ left: 12 })
          }
          .width('100%')
          .height(52)
          .padding({ left: 16, right: 16 })
          .onClick(() => {
            this.showScanMenu = false;
            this.scanLocalMusic();
          })

          Divider().strokeWidth(0.5).color(this.isDarkTheme ? 'rgba(255, 255, 255, 0.1)' : '#F0F0F0')

          // 扫描歌词
          Row() {
            Image($r('app.media.ic_music_note'))
              .width(24)
              .height(24)
              .fillColor('#4CAF50')
            Text('扫描匹配歌词')
              .fontSize(15)
              .fontColor('#4CAF50')
              .margin({ left: 12 })
          }
          .width('100%')
          .height(52)
          .padding({ left: 16, right: 16 })
          .onClick(() => {
            this.showScanMenu = false;
            this.scanLyrics();
          })

          Divider().strokeWidth(0.5).color(this.isDarkTheme ? 'rgba(255, 255, 255, 0.1)' : '#F0F0F0')

          // 清空音乐库
          Row() {
            Image($r('app.media.ic_delete'))
              .width(24)
              .height(24)
              .fillColor(this.isDarkTheme ? '#FF4158' : '#F44336')
            Text('清空音乐库')
              .fontSize(15)
              .fontColor(this.isDarkTheme ? '#FF4158' : '#F44336')
              .margin({ left: 12 })
          }
          .width('100%')
          .height(52)
          .padding({ left: 16, right: 16 })
          .onClick(() => {
            this.showScanMenu = false;
            this.clearAllMusic();
          })
        }
        .width(200)
        .backgroundColor(this.isDarkTheme ? 'rgba(22, 33, 62, 0.95)' : '#FFFFFF')
        .borderRadius(8)
        .shadow({
          radius: 12,
          color: this.isDarkTheme ? '#40000000' : '#20000000',
          offsetX: 0,
          offsetY: 4
        })
        .position({ x: '50%', y: 100 })
        .translate({ x: '-50%' })
        .zIndex(201)
      }

      // 扫描进度遮罩
      if (this.isScanning) {
        Column() {
          LoadingProgress()
            .width(60)
            .height(60)
            .color(this.isDarkTheme ? '#FF4158' : '#667EEA')
          Text('正在自动添加歌曲...')
            .fontSize(16)
            .fontColor('#FFFFFF')
            .margin({ top: 16 })
        }
        .width('100%')
        .height('100%')
        .backgroundColor(this.isDarkTheme ? 'rgba(15, 52, 96, 0.85)' : 'rgba(0, 0, 0, 0.5)')
        .backdropBlur(10)
        .justifyContent(FlexAlign.Center)
        .alignItems(HorizontalAlign.Center)
        .zIndex(300)
      }
    }
    .width('100%')
    .height('100%')
  }

  @Builder
  buildHeader() {
    Row() {
      // Logo 和标题
      Row() {
        // 音乐图标
        Image($r('app.media.ic_music_note'))
          .width(28)
          .height(28)
          .fillColor(this.isDarkTheme ? '#FF4158' : '#667EEA')
          .margin({ right: 10 })
        
        Column() {
          Text('音乐')
            .fontSize(24)
            .fontWeight(FontWeight.Bold)
            .fontColor(this.isDarkTheme ? '#FFFFFF' : '#333333')
          Text(`${this.musicList.length} 首歌曲`)
            .fontSize(11)
            .fontColor(this.isDarkTheme ? '#AAAAAA' : '#999999')
            .margin({ top: 2 })
        }
        .alignItems(HorizontalAlign.Start)
      }

      Blank()

      // 主题切换按钮
      Button({ type: ButtonType.Circle }) {
        Image(this.isDarkTheme ? $r('app.media.ic_music_note') : $r('app.media.ic_music_note'))
          .width(20)
          .height(20)
          .fillColor('#FFFFFF')
      }
      .width(36)
      .height(36)
      .backgroundColor(this.isDarkTheme ? '#FF4158' : '#667EEA')
      .margin({ right: 10 })
      .shadow({
        radius: 8,
        color: this.isDarkTheme ? '#40FF4158' : '#30667EEA',
        offsetX: 0,
        offsetY: 3
      })
      .onClick(() => {
        this.isDarkTheme = !this.isDarkTheme;
        // 同步主题状态到 AppStorage
        AppStorage.setOrCreate('isDarkTheme', this.isDarkTheme);
        
        // 保存主题设置到本地存储
        try {
          const context = AppStorage.get<common.UIAbilityContext>('context');
          if (context) {
            const dataPreferences = preferences.getPreferencesSync(context, { name: 'app_settings' });
            dataPreferences.putSync('isDarkTheme', this.isDarkTheme);
            dataPreferences.flush();
            hilog.info(DOMAIN, TAG, '主题设置已保存: %{public}s', this.isDarkTheme ? '暗色' : '浅色');
          }
        } catch (error) {
          hilog.error(DOMAIN, TAG, '保存主题设置失败: %{public}s', JSON.stringify(error));
        }
        
        // 更新系统状态栏
        this.updateStatusBar();
      })

      // 扫描音乐按钮
      Button({ type: ButtonType.Normal }) {
        Row() {
          Image($r('app.media.ic_scan'))
            .width(18)
            .height(18)
            .fillColor('#FFFFFF')
          Text('扫描')
            .fontSize(13)
            .fontColor('#FFFFFF')
            .margin({ left: 4 })
        }
      }
      .height(36)
      .padding({ left: 16, right: 16 })
      .linearGradient({
        angle: 135,
        colors: this.isDarkTheme ? [
          ['#FF4158', 0.0],
          ['#C41E3A', 1.0]
        ] : [
          ['#667EEA', 0.0],
          ['#764BA2', 1.0]
        ]
      })
      .borderRadius(18)
      .shadow({
        radius: 8,
        color: this.isDarkTheme ? '#40FF4158' : '#30667EEA',
        offsetX: 0,
        offsetY: 3
      })
      .onClick(() => {
        this.showScanMenu = !this.showScanMenu;
      })

      // 菜单按钮
      Button({ type: ButtonType.Circle }) {
        Image($r('app.media.ic_add'))
          .width(20)
          .height(20)
          .fillColor('#FFFFFF')
      }
      .width(36)
      .height(36)
      .backgroundColor(this.isDarkTheme ? '#FF4158' : '#667EEA')
      .margin({ left: 10 })
      .shadow({
        radius: 6,
        color: this.isDarkTheme ? '#40FF4158' : '#30667EEA',
        offsetX: 0,
        offsetY: 2
      })
      .onClick(() => {
        this.addMockMusic();
      })
    }
    .width('100%')
    .height(64)
    .padding({ left: 16, right: 16, top: 8, bottom: 8 })
    .backgroundColor(this.isDarkTheme ? 'rgba(26, 26, 46, 0.85)' : 'rgba(255, 255, 255, 0.9)')
    .backdropBlur(20)
  }

  @Builder
  buildSearchBar() {
    Row() {
      Image($r('app.media.ic_search'))
        .width(20)
        .height(20)
        .fillColor(this.isDarkTheme ? '#FF4158' : '#667EEA')
        .margin({ right: 10 })

      TextInput({ placeholder: '搜索歌曲、歌手、专辑', text: this.searchKeyword })
        .layoutWeight(1)
        .height(40)
        .backgroundColor(Color.Transparent)
        .placeholderColor(this.isDarkTheme ? '#666666' : '#AAAAAA')
        .fontColor(this.isDarkTheme ? '#FFFFFF' : '#333333')
        .fontSize(15)
        .onChange((value: string) => {
          this.searchKeyword = value;
          // 当输入框清空时,自动恢复全部列表
          if (!value.trim()) {
            this.viewModel.loadMusicList();
          }
        })
        .onSubmit(() => {
          this.searchMusic();
        })

      if (this.searchKeyword) {
        Image($r('app.media.ic_close'))
          .width(20)
          .height(20)
          .fillColor(this.isDarkTheme ? '#AAAAAA' : '#999999')
          .margin({ left: 8 })
          .onClick(() => {
            this.searchKeyword = '';
            this.viewModel.loadMusicList();
          })
      }
    }
    .width('100%')
    .height(48)
    .padding({ left: 16, right: 16 })
    .margin({
      left: 16,
      right: 16,
      top: 12,
      bottom: 8
    })
    .backgroundColor(this.isDarkTheme ? 'rgba(22, 33, 62, 0.6)' : 'rgba(255, 255, 255, 0.8)')
    .borderRadius(24)
    .shadow({
      radius: 8,
      color: this.isDarkTheme ? '#20000000' : '#10000000',
      offsetX: 0,
      offsetY: 2
    })
  }

  @Builder
  buildTabs() {
    Row() {
      ForEach(['全部', '收藏'], (item: string, index: number) => {
        Column() {
          Text(item)
            .fontSize(16)
            .fontWeight(this.currentTab === index ? FontWeight.Bold : FontWeight.Normal)
            .fontColor(this.currentTab === index ? (this.isDarkTheme ? '#FF4158' : '#667EEA') : (this.isDarkTheme ? '#AAAAAA' : '#666666'))

          if (this.currentTab === index) {
            Row()
              .width(28)
              .height(3)
              .linearGradient({
                angle: 90,
                colors: this.isDarkTheme ? [
                  ['#FF4158', 0.0],
                  ['#C41E3A', 1.0]
                ] : [
                  ['#667EEA', 0.0],
                  ['#764BA2', 1.0]
                ]
              })
              .borderRadius(2)
              .margin({ top: 6 })
          }
        }
        .padding({ left: 20, right: 20, top: 8, bottom: 8 })
        .onClick(() => {
          this.currentTab = index;
        })
      })

      Blank()

      // 播放模式按钮
      Row() {
        Image(this.getPlayModeIcon())
          .width(18)
          .height(18)
          .fillColor(this.isDarkTheme ? '#FF4158' : '#667EEA')
        Text(this.getPlayModeName())
          .fontSize(12)
          .fontColor(this.isDarkTheme ? '#FF4158' : '#667EEA')
          .margin({ left: 6 })
      }
      .padding({ left: 12, right: 12, top: 6, bottom: 6 })
      .backgroundColor(this.isDarkTheme ? 'rgba(255, 65, 88, 0.15)' : 'rgba(102, 126, 234, 0.1)')
      .borderRadius(16)
      .margin({ right: 12 })
      .onClick(() => {
        this.playMode = this.viewModel.getAudioPlayer().togglePlayMode();
      })
    }
    .width('100%')
    .height(52)
    .backgroundColor(this.isDarkTheme ? 'rgba(22, 33, 62, 0.5)' : 'rgba(255, 255, 255, 0.7)')
    .backdropBlur(10)
    .alignItems(VerticalAlign.Center)
  }

  @Builder
  buildLoading() {
    Column() {
      LoadingProgress()
        .width(56)
        .height(56)
        .color(this.isDarkTheme ? '#FF4158' : '#667EEA')
      Text('加载中...')
        .margin({ top: 16 })
        .fontSize(15)
        .fontColor(this.isDarkTheme ? '#FF4158' : '#667EEA')
    }
    .width('100%')
    .layoutWeight(1)
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center)
  }

  @Builder
  buildEmpty() {
    Column() {
      // 空状态图标
      Column() {
        Image($r('app.media.ic_music_note'))
          .width(72)
          .height(72)
          .fillColor(this.isDarkTheme ? '#FF4158' : '#667EEA')
      }
      .width(120)
      .height(120)
      .justifyContent(FlexAlign.Center)
      .backgroundColor(this.isDarkTheme ? 'rgba(255, 65, 88, 0.15)' : 'rgba(102, 126, 234, 0.1)')
      .borderRadius(60)
      .margin({ bottom: 24 })

      Text(this.currentTab === 0 ? '暂无音乐' : '暂无收藏')
        .fontSize(18)
        .fontWeight(FontWeight.Medium)
        .fontColor(this.isDarkTheme ? '#FFFFFF' : '#333333')
        .margin({ bottom: 8 })

      Text(this.currentTab === 0 ? '点击右上角扫描按钮添加音乐' : '收藏喜欢的歌曲吧')
        .fontSize(14)
        .fontColor(this.isDarkTheme ? '#AAAAAA' : '#999999')
    }
    .width('100%')
    .layoutWeight(1)
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center)
  }

  @Builder
  buildMusicList() {
    List({ space: 1 }) {
      ForEach(this.getCurrentList(), (item: MusicItem) => {
        ListItem() {
          this.buildMusicItem(item)
        }
        .swipeAction({
          end: {
            builder: () => {
              this.buildSwipeActions(item)
            }
          }
        })
      }, (item: MusicItem) => `${item.id}_${item.isFavorite}`)  // ✅ 加入 isFavorite 状态
    }
    .width('100%')
    .layoutWeight(1)
    .backgroundColor(this.isDarkTheme ? 'rgba(22, 33, 62, 0.3)' : '#FFFFFF')
    .divider({
      strokeWidth: 0.5,
      color: this.isDarkTheme ? 'rgba(255, 255, 255, 0.1)' : '#F0F0F0',
      startMargin: 72,
      endMargin: 16
    })
  }

  @Builder
  buildSwipeActions(music: MusicItem) {
    Row() {
      // 收藏按钮
      Column() {
        Image(music.isFavorite ? $r('app.media.ic_favorite') : $r('app.media.ic_favorite_border'))
          .width(24)
          .height(24)
          .fillColor('#FFFFFF')
        Text(music.isFavorite ? '取消' : '收藏')
          .fontSize(11)
          .fontColor('#FFFFFF')
          .margin({ top: 4 })
      }
      .width(72)
      .height('100%')
      .backgroundColor(this.isDarkTheme ? '#FF8C42' : '#FFA726')
      .justifyContent(FlexAlign.Center)
      .onClick(() => {
        this.toggleFavorite(music);
      })

      // 删除按钮
      Column() {
        Image($r('app.media.ic_delete'))
          .width(24)
          .height(24)
          .fillColor('#FFFFFF')
        Text('删除')
          .fontSize(11)
          .fontColor('#FFFFFF')
          .margin({ top: 4 })
      }
      .width(72)
      .height('100%')
      .backgroundColor(this.isDarkTheme ? '#FF4158' : '#F44336')
      .justifyContent(FlexAlign.Center)
      .onClick(() => {
        this.deleteMusic(music);
      })
    }
    .height('100%')
  }

  @Builder
  buildMusicItem(music: MusicItem) {
    Row() {
      // 专辑封面
      Stack() {
        Image(music.cover)
          .width(50)
          .height(50)
          .borderRadius(8)
          .objectFit(ImageFit.Cover)

        if (this.currentMusicId === music.id && this.isPlaying) {
          Row()
            .width(50)
            .height(50)
            .borderRadius(8)
            .backgroundColor('rgba(0,0,0,0.3)')

          Image($r('app.media.ic_playing'))
            .width(24)
            .height(24)
            .fillColor('#FFFFFF')
        }
      }
      .margin({ right: 12 })

      // 音乐信息
      Column() {
        Text(music.title)
          .fontSize(16)
          .fontWeight(this.currentMusicId === music.id ? FontWeight.Bold : FontWeight.Normal)
          .fontColor(this.currentMusicId === music.id ? (this.isDarkTheme ? '#FF4158' : '#667EEA') : (this.isDarkTheme ? '#FFFFFF' : '#333333'))
          .maxLines(1)
          .textOverflow({ overflow: TextOverflow.Ellipsis })
          .width('100%')

        Row() {
          Text(`${music.artist}`)
            .fontSize(13)
            .fontColor(this.isDarkTheme ? '#AAAAAA' : '#999999')
            .maxLines(1)
            .textOverflow({ overflow: TextOverflow.Ellipsis })

          Text(` · ${formatTime(music.duration)}`)
            .fontSize(13)
            .fontColor(this.isDarkTheme ? '#888888' : '#CCCCCC')
        }
        .width('100%')
        .margin({ top: 4 })
      }
      .layoutWeight(1)
      .alignItems(HorizontalAlign.Start)

      // 收藏按钮
      Image(music.isFavorite ? $r('app.media.ic_favorite') : $r('app.media.ic_favorite_border'))
        .width(24)
        .height(24)
        .fillColor(music.isFavorite ? (this.isDarkTheme ? '#FF4158' : '#667EEA') : (this.isDarkTheme ? '#666666' : '#CCCCCC'))
        .margin({ right: 8 })
        .onClick((event: ClickEvent) => {
          // event.stopPropagation(); // HarmonyOS 4.0+ 已移除此方法
          this.toggleFavorite(music);
        })

      // 更多按钮
      Image($r('app.media.ic_more'))
        .width(24)
        .height(24)
        .fillColor(this.isDarkTheme ? '#666666' : '#CCCCCC')
    }
    .width('100%')
    .height(72)
    .padding({ left: 16, right: 16 })
    .backgroundColor(this.currentMusicId === music.id ? 
      (this.isDarkTheme ? 'rgba(255, 65, 88, 0.2)' : 'rgba(102, 126, 234, 0.1)') : 
      (this.isDarkTheme ? 'rgba(22, 33, 62, 0.4)' : '#FFFFFF')
    )
    .onClick(() => {
      console.info('[MusicApp] ======================================');
      console.info('[MusicApp] 点击音乐列表项');
      console.info(`[MusicApp]   标题: ${music.title}`);
      console.info(`[MusicApp]   ID: ${music.id}`);
      console.info(`[MusicApp]   文件: ${music.filePath}`);
      console.info('[MusicApp] ======================================');
      this.playMusic(music);
    })
  }

  @Builder
  buildPlayerBar() {
    Row() {
      // 专辑封面 - 点击跳转到播放页
      Image(this.currentMusicCover || $r('app.media.ic_album'))
        .width(48)
        .height(48)
        .borderRadius(24)
        .objectFit(ImageFit.Cover)
        .margin({ right: 12 })
        .rotate({ angle: this.isPlaying ? 360 : 0 })
        .animation({
          duration: 10000,
          iterations: -1,
          curve: Curve.Linear
        })
        .onClick(() => {
          this.navigateToPlayPage();
        })

      // 歌曲信息 - 点击跳转到播放页
      Column() {
        Text(this.currentMusicTitle || '')
          .fontSize(15)
          .fontWeight(FontWeight.Medium)
          .fontColor(this.isDarkTheme ? '#FFFFFF' : '#333333')
          .maxLines(1)
          .textOverflow({ overflow: TextOverflow.Ellipsis })
          .width('100%')

        Text(this.currentMusicArtist || '')
          .fontSize(12)
          .fontColor(this.isDarkTheme ? '#AAAAAA' : '#999999')
          .maxLines(1)
          .textOverflow({ overflow: TextOverflow.Ellipsis })
          .width('100%')
          .margin({ top: 2 })
      }
      .layoutWeight(1)
      .alignItems(HorizontalAlign.Start)
      .onClick(() => {
        this.navigateToPlayPage();
      })

      // 控制按钮区域 - 不触发页面跳转
      Row() {
        // 上一首
        Image($r('app.media.ic_previous'))
          .width(28)
          .height(28)
          .fillColor(this.isDarkTheme ? '#AAAAAA' : '#333333')
          .onClick(() => {
            console.info('[MusicApp] 点击上一首');
            this.viewModel.getAudioPlayer().playPrevious();
          })

        // 播放/暂停
        Image(this.isPlaying ? $r('app.media.ic_pause') : $r('app.media.ic_play'))
          .width(36)
          .height(36)
          .fillColor(this.isDarkTheme ? '#FF4158' : '#667EEA')
          .margin({ left: 16 })
          .onClick(() => {
            console.info('[MusicApp] 点击播放/暂停按钮');
            this.togglePlayPause();
          })

        // 下一首
        Image($r('app.media.ic_next'))
          .width(28)
          .height(28)
          .fillColor(this.isDarkTheme ? '#AAAAAA' : '#333333')
          .margin({ left: 16 })
          .onClick(() => {
            console.info('[MusicApp] 点击下一首');
            this.viewModel.getAudioPlayer().playNext();
          })

        // 播放列表
        Image($r('app.media.ic_playlist'))
          .width(24)
          .height(24)
          .fillColor(this.isDarkTheme ? '#AAAAAA' : '#666666')
          .margin({ left: 16 })
          .onClick(() => {
            console.info('[MusicApp] 点击播放列表');
            this.showPlaylistDrawer = true;
          })
      }
      .height('100%')
      .alignItems(VerticalAlign.Center)
    }
    .width('100%')
    .height(72)
    .padding({ left: 16, right: 16 })
    .backgroundColor(this.isDarkTheme ? 'rgba(22, 33, 62, 0.95)' : '#FFFFFF')
    .backdropBlur(20)
    .shadow({
      radius: 12,
      color: this.isDarkTheme ? '#40000000' : '#20000000',
      offsetX: 0,
      offsetY: -3
    })
  }

  // 获取当前显示列表
  getCurrentList(): MusicItem[] {
    return this.currentTab === 0 ? this.musicList : this.favoriteList;
  }

  // 显示提示消息
  showTip(message: string): void {
    this.toastMessage = message;
    this.showToast = true;
    setTimeout(() => {
      this.showToast = false;
    }, 2000);
  }

  // 跳转到播放页面
  navigateToPlayPage(): void {
    console.info('[MusicApp] 跳转到播放页面');
    router.pushUrl({
      url: 'pages/PlayPage',
      params: {
        musicId: this.currentMusicId
      }
    }).then(() => {
      // 成功跳转
    }).catch((error: Error) => {
      console.error(`页面跳转失败: ${JSON.stringify(error)}`);
    });
  }

  aboutToDisappear(): void {
    this.viewModel.release();
  }
}
复制代码
// pages/PlayPage.ets
import { router } from '@kit.ArkUI';
import { common } from '@kit.AbilityKit';
import { MusicItem, PlayMode, LyricLine, parseLyrics, formatTime } from './models/MusicItem';
import { AudioPlayer } from './services/AudioPlayer';
import { MusicDatabase } from './services/Database';
import { FileUtils } from './utils/FileUtils';  // 导入 FileUtils

// 路由参数接口
interface RouteParams {
  musicId: number;
}

@Entry
@Component
struct PlayPage {
  @State currentMusic: MusicItem | null = null;
  @State isPlaying: boolean = false;
  @State currentPosition: number = 0;
  @State duration: number = 0;
  @State volume: number = 0.7;
  @State playMode: PlayMode = PlayMode.LOOP;
  @State isFavorite: boolean = false;
  @State showLyrics: boolean = true;
  @State showVolumeSlider: boolean = false;
  @State showPlaylistDrawer: boolean = false;
  @State lyrics: LyricLine[] = [];
  @State currentLyricIndex: number = 0;
  @State rotationAngle: number = 0;
  @State musicList: MusicItem[] = [];
  @State toastMessage: string = '';
  @State showToast: boolean = false;
  @State isProgressHovered: boolean = false;  // 进度条悬停状态
  @State isDraggingProgress: boolean = false;  // 是否正在拖动进度条
  @State dragProgressValue: number = 0;  // 拖动时的临时进度值
  
  // 防抖控制:防止快速点击
  private isToggling: boolean = false;
  private lastToggleTime: number = 0;
  private TOGGLE_DEBOUNCE_MS: number = 300; // 300ms 防抖时间
  
  // 从 AppStorage 读取主题状态
  @StorageLink('isDarkTheme') isDarkTheme: boolean = false;
  
  // 歌词滚动控制器
  private lyricScroller: Scroller = new Scroller();
  
  // 使用 @StorageLink 监听全局歌词变化,并自动解析
  @StorageLink('currentMusicLyrics')
  @Watch('onLyricsChanged')
  globalLyrics: string = '';
  
  private rotationTimer: number = -1;
  private context: common.UIAbilityContext = AppStorage.get<common.UIAbilityContext>('context') as common.UIAbilityContext;
  private audioPlayer: AudioPlayer = AudioPlayer.getInstance(this.context);
  private database: MusicDatabase = new MusicDatabase(this.context);

  async aboutToAppear(): Promise<void> {
    console.info('[PlayPage] ===== 页面初始化 =====');
    try {
      await this.database.initDatabase();

      // 初始化播放器(如果尚未初始化)
      if (!this.audioPlayer.isReady()) {
        await this.audioPlayer.init();
      }

      // 设置初始音量
      await this.audioPlayer.setVolume(this.volume);
      console.info(`[PlayPage] 初始音量已设置: ${Math.round(this.volume * 100)}%`);

      // 加载音乐列表
      this.musicList = await this.database.getAllMusic();
      
      // 关键:设置播放列表到 AudioPlayer
      this.audioPlayer.setPlaylist(this.musicList);
      console.info(`[PlayPage] 播放列表已设置: ${this.musicList.length} 首音乐`);

      // 同步当前播放状态
      const currentPlaying = this.audioPlayer.getCurrentMusic();
      if (currentPlaying) {
        console.info(`[PlayPage] 同步当前播放: ${currentPlaying.title}`);
        this.currentMusic = currentPlaying;
        this.isPlaying = this.audioPlayer.getIsPlaying();
        this.duration = currentPlaying.duration || this.audioPlayer.getDuration();
        this.isFavorite = currentPlaying.isFavorite;
        this.playMode = this.audioPlayer.getPlayMode();
        this.parseMusicLyrics();
      }

      // 获取路由参数
      const params: RouteParams = router.getParams() as RouteParams;
      if (params && params.musicId) {
        console.info(`[PlayPage] 路由参数 musicId: ${params.musicId}`);
        const music = this.musicList.find(m => m.id === params.musicId);
        if (music) {
          // 如果路由参数中的音乐与当前播放的不同,则播放新的
          if (!currentPlaying || currentPlaying.id !== music.id) {
            console.info(`[PlayPage] 播放路由指定的音乐: ${music.title}`);
            this.currentMusic = music;
            this.duration = music.duration;
            this.isFavorite = music.isFavorite;
            this.parseMusicLyrics();
            await this.playMusic(music);
          }
        }
      }

      // 设置监听器
      this.setupPlayerListeners();
      this.startRotation();
      console.info('[PlayPage] ===== 初始化完成 =====');
    } catch (error) {
      console.error(`[PlayPage] 初始化失败: ${JSON.stringify(error)}`);
    }
  }

  parseMusicLyrics(): void {
    console.info(`[PlayPage] ===== 开始解析歌词 =====`);
    
    // 优先使用全局歌词(从 AppStorage 同步)
    const lyricsContent = this.globalLyrics || (this.currentMusic?.lyrics || '');
    
    console.info(`[PlayPage] globalLyrics 长度: ${this.globalLyrics.length}`);
    console.info(`[PlayPage] currentMusic.lyrics 长度: ${this.currentMusic?.lyrics?.length || 0}`);
    console.info(`[PlayPage] 最终使用歌词长度: ${lyricsContent.length}`);
    
    if (lyricsContent && lyricsContent.length > 0) {
      // 创建新数组,触发 UI 更新
      this.lyrics = parseLyrics(lyricsContent);
      console.info(`[PlayPage] ✅ 歌词已解析: ${this.lyrics.length} 行`);
      
      // 输出前 3 行歌词作为样例
      if (this.lyrics.length > 0) {
        console.info(`[PlayPage] 前 3 行歌词:`);
        for (let i = 0; i < Math.min(3, this.lyrics.length); i++) {
          console.info(`  [${i}] ${this.lyrics[i].text}`);
        }
      }
    } else {
      // 默认歌词,创建新数组
      this.lyrics = [
        { time: 0, text: '暂无歌词' },
        { time: 3000, text: '请添加歌词文件' }
      ];
      console.info(`[PlayPage] ⚠️ 歌词为空,显示默认歌词`);
    }
    
    console.info(`[PlayPage] ===== 歌词解析完成 =====`);
  }

  /**
   * 全局歌词变化回调 - 自动重新解析歌词并刷新 UI
   */
  onLyricsChanged(): void {
    console.info(`[PlayPage] 全局歌词变化,重新解析歌词`);
    
    // 重置歌词索引
    this.currentLyricIndex = 0;
    
    // 重新解析歌词(会创建新数组,触发 UI 刷新)
    this.parseMusicLyrics();
    
    console.info(`[PlayPage] ✅ UI 已通知刷新,当前 ${this.lyrics.length} 行歌词`);
  }

  startRotation(): void {
    if (this.rotationTimer === -1) {
      this.rotationTimer = setInterval(() => {
        if (this.isPlaying) {
          this.rotationAngle = (this.rotationAngle + 1) % 360;
        }
      }, 50);
    }
  }

  stopRotation(): void {
    if (this.rotationTimer !== -1) {
      clearInterval(this.rotationTimer);
      this.rotationTimer = -1;
    }
  }

  setupPlayerListeners(): void {
    this.audioPlayer.setOnStateChangeListener((state: string) => {
      if (state === 'playing') {
        this.isPlaying = true;
        this.currentMusic = this.audioPlayer.getCurrentMusic();
        if (this.currentMusic) {
          this.duration = this.currentMusic.duration;
          this.isFavorite = this.currentMusic.isFavorite;
          // 在播放状态变化时也重新解析歌词
          this.parseMusicLyrics();
        }
      } else if (state === 'paused') {
        this.isPlaying = false;
      } else if (state === 'completed') {
        this.isPlaying = false;
      }
    });

    this.audioPlayer.setOnPositionChangeListener((position: number) => {
      this.currentPosition = position;
      this.updateCurrentLyricIndex();
    });

    // 关键修改:音乐变化时从数据库重新加载歌词并自动匹配
    this.audioPlayer.setOnMusicChangeListener(async (music: MusicItem) => {
      console.info(`[PlayPage] ==== 音乐变化: ${music.title} ====`);
      
      // 重置歌词状态
      this.currentLyricIndex = 0;
      
      try {
        // ① 从数据库重新加载当前音乐的最新数据
        const freshMusic = await this.database.getMusicById(music.id);
        if (!freshMusic) {
          console.warn(`[PlayPage] 未找到音乐: id=${music.id}`);
          return;
        }
        
        console.info(`[PlayPage] 从数据库加载: ${freshMusic.title}`);
        console.info(`[PlayPage] 歌词长度: ${freshMusic.lyrics?.length || 0}`);
        console.info(`[PlayPage] 文件路径: ${freshMusic.filePath}`);
        
        // ② 如果没有歌词,尝试自动匹配
        let finalLyrics = freshMusic.lyrics || '';
        
        if (!finalLyrics || finalLyrics.length === 0) {
          console.info(`[PlayPage] 歌词为空,尝试自动匹配...`);
          
          const lyricPath = await FileUtils.findMatchingLyricFile(freshMusic.filePath);
          if (lyricPath) {
            console.info(`[PlayPage] 找到歌词文件: ${lyricPath}`);
            const lyricsContent = await FileUtils.readLyricFile(lyricPath);
            
            if (lyricsContent && lyricsContent.length > 0) {
              console.info(`[PlayPage] ✅ 自动匹配成功,歌词长度: ${lyricsContent.length}`);
              // 更新到数据库
              await this.database.updateMusicLyrics(freshMusic.id, lyricsContent);
              finalLyrics = lyricsContent;
            } else {
              console.warn(`[PlayPage] 歌词文件为空`);
            }
          } else {
            console.warn(`[PlayPage] 未找到匹配的歌词文件`);
          }
        }
        
        // ③ 更新内存中的歌词
        freshMusic.lyrics = finalLyrics;
        
        // ④ 更新当前音乐信息
        this.currentMusic = freshMusic;
        this.duration = freshMusic.duration;
        this.isFavorite = freshMusic.isFavorite;
        
        // ⑤ 🔑 关键:更新 PlayPage 的 musicList 和 AudioPlayer 的 playlist
        const listIndex = this.musicList.findIndex(m => m.id === freshMusic.id);
        if (listIndex !== -1) {
          this.musicList[listIndex] = freshMusic;
          console.info(`[PlayPage] 已更新 musicList[${listIndex}]`);
        }
        // 同步到 AudioPlayer 的播放列表
        this.audioPlayer.setPlaylist(this.musicList);
        console.info(`[PlayPage] 已同步播放列表到 AudioPlayer`);
        
        // ⑥ 同步到全局状态(必须在 parseMusicLyrics 之前)
        AppStorage.setOrCreate('currentMusicLyrics', finalLyrics);
        console.info(`[PlayPage] 全局状态已同步,歌词长度: ${finalLyrics.length}`);
        
        // ⑦ 解析并显示歌词
        this.parseMusicLyrics();
        
        console.info(`[PlayPage] ✅ 歌词已加载并通知 UI 刷新`);
        console.info(`[PlayPage] 当前显示 ${this.lyrics.length} 行歌词`);
        
      } catch (error) {
        console.error(`[PlayPage] 加载歌词失败: ${JSON.stringify(error)}`);
        // 失败时显示默认歌词
        this.lyrics = [
          { time: 0, text: '加载失败' },
          { time: 3000, text: '请重试' }
        ];
      }
      
      console.info(`[PlayPage] ==== 音乐变化处理完成 ====`);
    });

    this.audioPlayer.setOnPlayModeChangeListener((mode: PlayMode) => {
      this.playMode = mode;
    });

    // 监听实际时长
    this.audioPlayer.setOnDurationChangeListener((duration: number) => {
      this.duration = duration;
      // 更新数据库中的时长
      if (this.currentMusic && this.currentMusic.duration === 0) {
        this.database.updateMusicDuration(this.currentMusic.id, duration);
      }
    });

    // 监听收藏状态变化(从其他页面同步)
    this.audioPlayer.setOnFavoriteChangeListener((musicId: number, isFavorite: boolean) => {
      console.info(`[PlayPage] 收到收藏状态变化: musicId=${musicId}, isFavorite=${isFavorite}`);
      if (this.currentMusic && this.currentMusic.id === musicId) {
        this.isFavorite = isFavorite;
        // 更新 currentMusic
        this.currentMusic.isFavorite = isFavorite;
      }
      // 更新音乐列表中的状态
      const index = this.musicList.findIndex(m => m.id === musicId);
      if (index !== -1) {
        this.musicList[index].isFavorite = isFavorite;
      }
    });
  }

  updateCurrentLyricIndex(): void {
    const oldIndex = this.currentLyricIndex;
    
    for (let i = this.lyrics.length - 1; i >= 0; i--) {
      if (this.currentPosition >= this.lyrics[i].time) {
        this.currentLyricIndex = i;
        break;
      }
    }
    
    // 如果歌词索引变化,自动滚动到当前歌词
    if (oldIndex !== this.currentLyricIndex && this.showLyrics) {
      this.scrollToCurrentLyric();
    }
  }
  
  /**
   * 滚动到当前歌词(居中显示)
   */
  scrollToCurrentLyric(): void {
    try {
      // 每行歌词高度约 50vp(包括 padding 和 space)
      const itemHeight = 50;
      const yOffset = this.currentLyricIndex * itemHeight;
      
      // 滚动到当前歌词,并且居中显示
      this.lyricScroller.scrollTo({
        xOffset: 0,
        yOffset: yOffset,
        animation: {
          duration: 300,
          curve: Curve.EaseInOut
        }
      });
    } catch (error) {
      console.error(`[PlayPage] 歌词滚动失败: ${JSON.stringify(error)}`);
    }
  }

  async playMusic(music: MusicItem): Promise<void> {
    console.info(`[PlayPage] 播放音乐: ${music.title}`);
    try {
      await this.audioPlayer.play(music);
      await this.database.updatePlayCount(music.id);
      this.currentMusic = music;
      this.isPlaying = true;
      this.parseMusicLyrics();
      console.info(`[PlayPage] ✅ 播放已启动`);
    } catch (error) {
      console.error(`[PlayPage] 播放失败: ${JSON.stringify(error)}`);
      this.showTip('播放失败');
    }
  }

  async togglePlayPause(): Promise<void> {
    const now = Date.now();
      
    // 防抖:如果正在执行或距离上次操作不足 300ms,则忽略
    if (this.isToggling || (now - this.lastToggleTime < this.TOGGLE_DEBOUNCE_MS)) {
      console.warn(`[PlayPage] 操作过于频繁,已忽略(距上次 ${now - this.lastToggleTime}ms)`);
      return;
    }
      
    this.isToggling = true;
    this.lastToggleTime = now;
      
    try {
      // ⚠️ 关键:基于 AudioPlayer 的真实状态判断,而不是本地 UI 状态
      const realIsPlaying = this.audioPlayer.getIsPlaying();
        
      console.info(`[PlayPage] 切换播放/暂停:`);
      console.info(`[PlayPage]   UI状态 isPlaying=${this.isPlaying}`);
      console.info(`[PlayPage]   真实状态 realIsPlaying=${realIsPlaying}`);
        
      if (realIsPlaying) {
        // 真实在播放,执行暂停
        console.info(`[PlayPage] → 执行暂停`);
        await this.audioPlayer.pause();
        // ✅ 不立即修改 this.isPlaying,等待回调更新
      } else if (this.currentMusic) {
        // 真实已暂停或停止,执行恢复
        console.info(`[PlayPage] → 执行恢复`);
        await this.audioPlayer.resume();
        // ✅ 不立即修改 this.isPlaying,等待回调更新
      } else if (this.musicList.length > 0) {
        // 没有当前音乐,播放第一首
        console.info(`[PlayPage] → 播放第一首`);
        await this.playMusic(this.musicList[0]);
      } else {
        this.showTip('请先添加音乐');
      }
        
      // 延迟 100ms 后同步状态(给 AudioPlayer 回调时间)
      setTimeout(() => {
        this.isPlaying = this.audioPlayer.getIsPlaying();
        console.info(`[PlayPage] ✅ 状态已同步: isPlaying=${this.isPlaying}`);
      }, 100);
        
    } catch (error) {
      console.error(`[PlayPage] ❌ 切换失败: ${JSON.stringify(error)}`);
      // 发生错误时也同步状态
      this.isPlaying = this.audioPlayer.getIsPlaying();
    } finally {
      // 延迟 300ms 后解锁(防抖时间)
      setTimeout(() => {
        this.isToggling = false;
      }, this.TOGGLE_DEBOUNCE_MS);
    }
  }

  async seekTo(position: number): Promise<void> {
    await this.audioPlayer.seekTo(position);
    this.currentPosition = position;
  }

  async setVolume(value: number): Promise<void> {
    this.volume = value;
    await this.audioPlayer.setVolume(value);
  }

  async toggleFavorite(): Promise<void> {
    if (this.currentMusic) {
      console.info(`[PlayPage] 切换收藏: ${this.currentMusic.title}`);
      const newFavoriteState = await this.database.toggleFavorite(this.currentMusic.id);
      this.isFavorite = newFavoriteState;
      // 通知 AudioPlayer 同步到其他页面
      this.audioPlayer.notifyFavoriteChanged(this.currentMusic.id, newFavoriteState);
      this.showTip(newFavoriteState ? '已添加到收藏' : '已取消收藏');
    }
  }

  /**
   * 处理上一曲点击 - 直接加载歌词
   */
  async handlePlayPrevious(): Promise<void> {
    console.info(`[PlayPage] ===== 点击上一曲 =====`);
    
    // 调用 AudioPlayer 播放上一曲
    await this.audioPlayer.playPrevious();
    
    // 等待 100ms 让 AudioPlayer 完成切歌
    await new Promise<void>((resolve) => setTimeout(resolve, 100));
    
    // 直接从 AudioPlayer 获取当前正在播放的音乐
    const currentMusic = this.audioPlayer.getCurrentMusic();
    if (!currentMusic) {
      console.warn(`[PlayPage] 未获取到当前播放的音乐`);
      return;
    }
    
    console.info(`[PlayPage] 当前播放: ${currentMusic.title}`);
    
    // 从数据库重新加载歌词
    await this.loadAndMatchLyrics(currentMusic.id);
  }

  /**
   * 处理下一曲点击 - 直接加载歌词
   */
  async handlePlayNext(): Promise<void> {
    console.info(`[PlayPage] ===== 点击下一曲 =====`);
    
    // 调用 AudioPlayer 播放下一曲
    await this.audioPlayer.playNext();
    
    // 等待 100ms 让 AudioPlayer 完成切歌
    await new Promise<void>((resolve) => setTimeout(resolve, 100));
    
    // 直接从 AudioPlayer 获取当前正在播放的音乐
    const currentMusic = this.audioPlayer.getCurrentMusic();
    if (!currentMusic) {
      console.warn(`[PlayPage] 未获取到当前播放的音乐`);
      return;
    }
    
    console.info(`[PlayPage] 当前播放: ${currentMusic.title}`);
    
    // 从数据库重新加载歌词
    await this.loadAndMatchLyrics(currentMusic.id);
  }

  /**
   * 加载并匹配歌词 - 通用方法
   */
  async loadAndMatchLyrics(musicId: number): Promise<void> {
    console.info(`[PlayPage] 加载歌词: musicId=${musicId}`);
    
    try {
      // ① 从数据库加载最新数据
      const freshMusic = await this.database.getMusicById(musicId);
      if (!freshMusic) {
        console.warn(`[PlayPage] 未找到音乐: id=${musicId}`);
        return;
      }
      
      console.info(`[PlayPage] 从数据库加载: ${freshMusic.title}`);
      console.info(`[PlayPage] 歌词长度: ${freshMusic.lyrics?.length || 0}`);
      
      // ② 如果没有歌词,尝试自动匹配
      let finalLyrics = freshMusic.lyrics || '';
      
      if (!finalLyrics || finalLyrics.length === 0) {
        console.info(`[PlayPage] 歌词为空,尝试自动匹配...`);
        
        const lyricPath = await FileUtils.findMatchingLyricFile(freshMusic.filePath);
        if (lyricPath) {
          console.info(`[PlayPage] 找到歌词文件: ${lyricPath}`);
          const lyricsContent = await FileUtils.readLyricFile(lyricPath);
          
          if (lyricsContent && lyricsContent.length > 0) {
            console.info(`[PlayPage] ✅ 自动匹配成功,歌词长度: ${lyricsContent.length}`);
            // 更新到数据库
            await this.database.updateMusicLyrics(freshMusic.id, lyricsContent);
            finalLyrics = lyricsContent;
          } else {
            console.warn(`[PlayPage] 歌词文件为空`);
          }
        } else {
          console.warn(`[PlayPage] 未找到匹配的歌词文件`);
        }
      }
      
      // ③ 更新当前音乐信息
      freshMusic.lyrics = finalLyrics;
      this.currentMusic = freshMusic;
      this.duration = freshMusic.duration;
      this.isFavorite = freshMusic.isFavorite;
      this.currentLyricIndex = 0;
      
      // ④ 同步到全局状态
      AppStorage.setOrCreate('currentMusicLyrics', finalLyrics);
      console.info(`[PlayPage] 全局状态已同步,歌词长度: ${finalLyrics.length}`);
      
      // ⑤ 解析并显示歌词
      this.parseMusicLyrics();
      
      console.info(`[PlayPage] ✅ 歌词已加载,显示 ${this.lyrics.length} 行`);
      
    } catch (error) {
      console.error(`[PlayPage] 加载歌词失败: ${JSON.stringify(error)}`);
      this.lyrics = [
        { time: 0, text: '加载失败' },
        { time: 3000, text: '请重试' }
      ];
    }
  }

  showTip(message: string): void {
    this.toastMessage = message;
    this.showToast = true;
    setTimeout(() => {
      this.showToast = false;
    }, 1500);
  }

  getPlayModeIcon(): Resource {
    switch (this.playMode) {
      case PlayMode.SINGLE:
        return $r('app.media.ic_repeat_one');
      case PlayMode.RANDOM:
        return $r('app.media.ic_shuffle');
      case PlayMode.LOOP:
        return $r('app.media.ic_repeat');
      default:
        return $r('app.media.ic_repeat');
    }
  }

  getPlayModeName(): string {
    switch (this.playMode) {
      case PlayMode.SINGLE:
        return '单曲循环';
      case PlayMode.RANDOM:
        return '随机播放';
      case PlayMode.LOOP:
        return '列表循环';
      default:
        return '顺序播放';
    }
  }

  goBack(): void {
    router.back();
  }

  build() {
    Stack({ alignContent: Alignment.Bottom }) {
      Column() {
        // 顶部导航栏
        this.buildHeader()

        if (this.showLyrics) {
          // 歌词模式
          this.buildLyricsView()
        } else {
          // 封面模式
          this.buildCoverView()
        }

        // 进度条
        this.buildProgressBar()

        // 控制按钮
        this.buildControls()

        // 底部功能按钮
        this.buildBottomActions()
      }
      .width('100%')
      .height('100%')
      .linearGradient({
        direction: GradientDirection.Bottom,
        colors: this.isDarkTheme ? [
          ['#1a1a2e', 0.0],   // 深蓝灰
          ['#16213e', 0.3],   // 暗蓝色
          ['#0f3460', 0.7],   // 深蓝色
          ['#c41e3a', 1.0]    // 鲜红色
        ] : [
          ['#667eea', 0],
          ['#764ba2', 0.5],
          ['#f093fb', 1]
        ]
      })

      // 音量滑块
      if (this.showVolumeSlider) {
        this.buildVolumeSlider()
      }

      // 播放列表抽屉
      if (this.showPlaylistDrawer) {
        this.buildPlaylistDrawer()
      }

      // Toast 提示
      if (this.showToast) {
        Text(this.toastMessage)
          .fontSize(14)
          .fontColor('#FFFFFF')
          .backgroundColor(this.isDarkTheme ? 'rgba(255, 65, 88, 0.9)' : 'rgba(0, 0, 0, 0.7)')
          .padding({
            left: 20,
            right: 20,
            top: 12,
            bottom: 12
          })
          .borderRadius(20)
          .position({ x: '50%', y: '40%' })
          .translate({ x: '-50%' })
      }
    }
    .width('100%')
    .height('100%')
  }

  @Builder
  buildHeader(): void {
    Row() {
      // 返回按钮
      Image($r('app.media.ic_back'))
        .width(28)
        .height(28)
        .fillColor('#FFFFFF')
        .onClick(() => {
          this.goBack();
        })

      // 标题区域
      Column() {
        Text(this.currentMusic?.title || '未知歌曲')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
          .maxLines(1)
          .textOverflow({ overflow: TextOverflow.Ellipsis })

        Text(this.currentMusic?.artist || '未知歌手')
          .fontSize(13)
          .fontColor('rgba(255,255,255,0.8)')
          .margin({ top: 2 })
      }
      .layoutWeight(1)
      .alignItems(HorizontalAlign.Center)

      // 分享按钮
      Image($r('app.media.ic_share'))
        .width(24)
        .height(24)
        .fillColor('#FFFFFF')
    }
    .width('100%')
    .height(56)
    .padding({ left: 16, right: 16 })
  }

  @Builder
  buildCoverView(): void {
    Column() {
      // 旋转唱片效果
      Stack() {
        // 唱片底座
        Circle()
          .width(300)
          .height(300)
          .fill('#000000')
          .opacity(0.3)

        // 唱片
        Stack() {
          // 黑胶唱片背景
          Circle()
            .width(280)
            .height(280)
            .fill('#1a1a1a')

          // 唱片纹理
          Circle()
            .width(260)
            .height(260)
            .fill(Color.Transparent)
            .stroke('#333333')
            .strokeWidth(40)

          // 专辑封面
          Image(this.currentMusic?.cover || $r('app.media.ic_album'))
            .width(160)
            .height(160)
            .borderRadius(80)
            .objectFit(ImageFit.Cover)
        }
        .rotate({ angle: this.rotationAngle })
      }
      .margin({ top: 40, bottom: 40 })

      // 歌曲信息
      Column() {
        Text(this.currentMusic?.title || '未知歌曲')
          .fontSize(24)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
          .maxLines(1)
          .textOverflow({ overflow: TextOverflow.Ellipsis })

        Row() {
          Text(this.currentMusic?.artist || '未知歌手')
            .fontSize(16)
            .fontColor('rgba(255,255,255,0.8)')

          Text(' - ')
            .fontSize(16)
            .fontColor('rgba(255,255,255,0.6)')

          Text(this.currentMusic?.album || '未知专辑')
            .fontSize(16)
            .fontColor('rgba(255,255,255,0.8)')
        }
        .margin({ top: 8 })

        // 收藏按钮
        Image(this.isFavorite ? $r('app.media.ic_favorite') : $r('app.media.ic_favorite_border'))
          .width(32)
          .height(32)
          .fillColor(this.isFavorite ? (this.isDarkTheme ? '#FF4158' : '#667EEA') : '#FFFFFF')
          .margin({ top: 20 })
          .onClick(() => {
            this.toggleFavorite();
          })
      }
      .width('100%')
      .alignItems(HorizontalAlign.Center)
    }
    .width('100%')
    .layoutWeight(1)
    .justifyContent(FlexAlign.Center)
    .onClick(() => {
      this.showLyrics = true;
    })
  }

  @Builder
  buildLyricsView(): void {
    Column() {
      // 顶部小封面
      Row() {
        Image(this.currentMusic?.cover || $r('app.media.ic_album'))
          .width(60)
          .height(60)
          .borderRadius(8)
          .objectFit(ImageFit.Cover)
          .rotate({ angle: this.rotationAngle })

        Column() {
          Text(this.currentMusic?.title || '未知歌曲')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')
            .maxLines(1)
            .textOverflow({ overflow: TextOverflow.Ellipsis })

          Text(this.currentMusic?.artist || '未知歌手')
            .fontSize(14)
            .fontColor('rgba(255,255,255,0.8)')
            .margin({ top: 4 })
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Start)
        .margin({ left: 16 })

        // 收藏按钮
        Image(this.isFavorite ? $r('app.media.ic_favorite') : $r('app.media.ic_favorite_border'))
          .width(28)
          .height(28)
          .fillColor(this.isFavorite ? (this.isDarkTheme ? '#FF4158' : '#667EEA') : '#FFFFFF')
          .onClick(() => {
            this.toggleFavorite();
          })
      }
      .width('100%')
      .padding({
        left: 20,
        right: 20,
        top: 10,
        bottom: 20
      })
      .onClick(() => {
        this.showLyrics = false;
      })

      // 歌词滚动区域(使用 Scroll + Column 实现居中)
      Scroll(this.lyricScroller) {
        Column() {
          // 顶部占位空间(让第一行歌词能显示在中间)
          Row()
            .height('40%')
          
          // 歌词列表
          ForEach(this.lyrics, (item: LyricLine, index: number) => {
            Text(item.text)
              .fontSize(this.currentLyricIndex === index ? 22 : 17)
              .fontWeight(this.currentLyricIndex === index ? FontWeight.Bold : FontWeight.Normal)
              .fontColor(this.getLyricColor(index))
              .textAlign(TextAlign.Center)
              .width('100%')
              .padding({ left: 30, right: 30, top: 12, bottom: 12 })
              .transition({ type: TransitionType.All, opacity: 1, scale: { x: 1, y: 1 } })
              .animation({
                duration: 300,
                curve: Curve.EaseInOut
              })
              .onClick(() => {
                // 点击歌词跳转到对应时间
                this.seekTo(item.time);
              })
          }, (item: LyricLine, index: number) => `${index}_${item.time}`)
          
          // 底部占位空间(让最后一行歌词能显示在中间)
          Row()
            .height('40%')
        }
        .width('100%')
      }
      .width('100%')
      .layoutWeight(1)
      .scrollBar(BarState.Off)
      .edgeEffect(EdgeEffect.Spring)
    }
    .width('100%')
    .layoutWeight(1)
  }
  
  /**
   * 获取歌词颜色(渐变效果)
   */
  getLyricColor(index: number): string {
    if (index === this.currentLyricIndex) {
      // 当前歌词:纯白色
      return '#FFFFFF';
    } else if (index === this.currentLyricIndex + 1) {
      // 下一句:略淡
      return 'rgba(255,255,255,0.7)';
    } else if (index === this.currentLyricIndex - 1) {
      // 上一句:略淡
      return 'rgba(255,255,255,0.6)';
    } else {
      // 其他:很淡
      return 'rgba(255,255,255,0.4)';
    }
  }

  @Builder
  buildProgressBar(): void {
    Column() {
      Slider({
        value: this.isDraggingProgress ? this.dragProgressValue : this.currentPosition,  // 拖动时使用临时值
        min: 0,
        max: this.duration > 0 ? this.duration : 1,
        step: 1000,
        style: SliderStyle.OutSet
      })
        .width('90%')
        .height(this.isProgressHovered ? 22 : 18)  // 悬停时22,默认18
        .blockSize({ width: 24, height: 24 })
        .blockColor('#FFFFFF')  // 白色滑块
        .trackColor('rgba(255,255,255,0.3)')  // 半透明白色未播放轨道
        .selectedColor('#FFFFFF')  // 白色已播放轨道
        .trackThickness(this.isProgressHovered ? 8 : 6)  // 悬停时轨道也变粗
        .onTouch((event: TouchEvent) => {
          if (event.type === TouchType.Down) {
            this.isProgressHovered = true;
            this.isDraggingProgress = true;  // 开始拖动
            this.dragProgressValue = this.currentPosition;
          } else if (event.type === TouchType.Up || event.type === TouchType.Cancel) {
            this.isProgressHovered = false;
            this.isDraggingProgress = false;  // 结束拖动
          }
        })
        .onChange((value: number, mode: SliderChangeMode) => {
          if (mode === SliderChangeMode.Moving) {
            // 拖动中,只更新临时值
            this.dragProgressValue = value;
          } else if (mode === SliderChangeMode.End) {
            // 拖动结束,执行跳转
            this.isDraggingProgress = false;
            this.seekTo(value);
          }
        })

      Row() {
        Text(formatTime(this.isDraggingProgress ? this.dragProgressValue : this.currentPosition))  // 拖动时显示临时值
          .fontSize(14)
          .fontColor('rgba(255,255,255,0.8)')  // 白色半透明文字

        Blank()

        Text(formatTime(this.duration))
          .fontSize(14)
          .fontColor('rgba(255,255,255,0.8)')  // 白色半透明文字
      }
      .width('90%')
      .margin({ top: 8 })
    }
    .width('100%')
    .padding({ top: 16 })
  }

  @Builder
  buildControls(): void {
    Row() {
      // 播放模式
      Column() {
        Image(this.getPlayModeIcon())
          .width(28)
          .height(28)
          .fillColor('#FFFFFF')
      }
      .onClick(() => {
        this.playMode = this.audioPlayer.togglePlayMode();
      })

      // 上一首
      Image($r('app.media.ic_previous'))
        .width(40)
        .height(40)
        .fillColor('#FFFFFF')
        .margin({ left: 24 })
        .onClick(() => {
          this.handlePlayPrevious();
        })

      // 播放/暂停
      Stack() {
        Circle()
          .width(72)
          .height(72)
          .fill('#FFFFFF')

        Image(this.isPlaying ? $r('app.media.ic_pause') : $r('app.media.ic_play'))
          .width(36)
          .height(36)
          .fillColor(this.isDarkTheme ? '#FF4158' : '#764ba2')
      }
      .margin({ left: 24, right: 24 })
      .onClick(() => {
        this.togglePlayPause();
      })

      // 下一首
      Image($r('app.media.ic_next'))
        .width(40)
        .height(40)
        .fillColor('#FFFFFF')
        .margin({ right: 24 })
        .onClick(() => {
          this.handlePlayNext();
        })

      // 播放列表
      Image($r('app.media.ic_playlist'))
        .width(28)
        .height(28)
        .fillColor('#FFFFFF')
        .onClick(() => {
          this.showPlaylistDrawer = true;
        })
    }
    .width('100%')
    .justifyContent(FlexAlign.Center)
    .alignItems(VerticalAlign.Center)
    .padding({ top: 24, bottom: 16 })
  }

  @Builder
  buildBottomActions(): void {
    Row() {
      // 收藏
      Column() {
        Image(this.isFavorite ? $r('app.media.ic_favorite') : $r('app.media.ic_favorite_border'))
          .width(24)
          .height(24)
          .fillColor(this.isFavorite ? (this.isDarkTheme ? '#FF4158' : '#667EEA') : '#FFFFFF')
        Text('收藏')
          .fontSize(10)
          .fontColor('#FFFFFF')
          .margin({ top: 4 })
      }
      .onClick(() => {
        this.toggleFavorite();
      })

      // 下载
      Column() {
        Image($r('app.media.ic_download'))
          .width(24)
          .height(24)
          .fillColor('#FFFFFF')
        Text('下载')
          .fontSize(10)
          .fontColor('#FFFFFF')
          .margin({ top: 4 })
      }

      // 评论
      Column() {
        Image($r('app.media.ic_comment'))
          .width(24)
          .height(24)
          .fillColor('#FFFFFF')
        Text('评论')
          .fontSize(10)
          .fontColor('#FFFFFF')
          .margin({ top: 4 })
      }

      // 音量
      Column() {
        Image($r('app.media.ic_volume_high'))
          .width(24)
          .height(24)
          .fillColor('#FFFFFF')
        Text('音量')
          .fontSize(10)
          .fontColor('#FFFFFF')
          .margin({ top: 4 })
      }
      .onClick(() => {
        this.showVolumeSlider = !this.showVolumeSlider;
      })

      // 更多
      Column() {
        Image($r('app.media.ic_more'))
          .width(24)
          .height(24)
          .fillColor('#FFFFFF')
        Text('更多')
          .fontSize(10)
          .fontColor('#FFFFFF')
          .margin({ top: 4 })
      }
    }
    .width('100%')
    .justifyContent(FlexAlign.SpaceAround)
    .padding({ top: 16, bottom: 32 })
  }

  @Builder
  buildVolumeSlider(): void {
    Column() {
      Row()
        .width('100%')
        .layoutWeight(1)
        .backgroundColor('rgba(0,0,0,0.3)')
        .onClick(() => {
          this.showVolumeSlider = false;
        })

      Column() {
        Row() {
          Text('音量')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor('#333333')

          Blank()

          Text(`${Math.round(this.volume * 100)}%`)
            .fontSize(14)
            .fontColor('#666666')
        }
        .width('100%')
        .padding({
          left: 20,
          right: 20,
          top: 20,
          bottom: 16
        })

        Row() {
          Image($r('app.media.ic_volume_low'))
            .width(24)
            .height(24)
            .fillColor('#999999')
            .margin({ right: 12 })

          Slider({
            value: this.volume * 100,
            min: 0,
            max: 100,
            step: 1,
            style: SliderStyle.OutSet
          })
            .layoutWeight(1)
            .blockColor('#764ba2')
            .trackColor('#E0E0E0')
            .selectedColor('#764ba2')
            .onChange((value: number) => {
              this.setVolume(value / 100);
            })

          Image($r('app.media.ic_volume_high'))
            .width(24)
            .height(24)
            .fillColor('#333333')
            .margin({ left: 12 })
        }
        .width('100%')
        .padding({ left: 20, right: 20, bottom: 32 })
      }
      .width('100%')
      .backgroundColor('#FFFFFF')
      .borderRadius({ topLeft: 20, topRight: 20 })
    }
    .width('100%')
    .height('100%')
  }

  @Builder
  buildPlaylistDrawer(): void {
    Column() {
      Row()
        .width('100%')
        .layoutWeight(1)
        .backgroundColor(this.isDarkTheme ? 'rgba(15, 52, 96, 0.85)' : 'rgba(0, 0, 0, 0.5)')
        .onClick(() => {
          this.showPlaylistDrawer = false;
        })

      Column() {
        // 标题栏
        Row() {
          Text('播放列表')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor(this.isDarkTheme ? '#FFFFFF' : '#333333')

          Blank()

          Row() {
            Image(this.getPlayModeIcon())
              .width(18)
              .height(18)
              .fillColor(this.isDarkTheme ? '#AAAAAA' : '#666666')
            Text(this.getPlayModeName())
              .fontSize(12)
              .fontColor(this.isDarkTheme ? '#AAAAAA' : '#666666')
              .margin({ left: 4 })
          }
          .onClick(() => {
            this.playMode = this.audioPlayer.togglePlayMode();
          })

          Image($r('app.media.ic_close'))
            .width(24)
            .height(24)
            .fillColor(this.isDarkTheme ? '#AAAAAA' : '#666666')
            .margin({ left: 16 })
            .onClick(() => {
              this.showPlaylistDrawer = false;
            })
        }
        .width('100%')
        .height(56)
        .padding({ left: 20, right: 20 })

        // 列表
        List({ space: 1 }) {
          ForEach(this.musicList, (item: MusicItem, index: number) => {
            ListItem() {
              Row() {
                Text(`${index + 1}`)
                  .fontSize(14)
                  .fontColor(this.currentMusic?.id === item.id ? 
                    (this.isDarkTheme ? '#FF4158' : '#667EEA') : 
                    (this.isDarkTheme ? '#AAAAAA' : '#999999')
                  )
                  .width(30)

                Column() {
                  Text(item.title)
                    .fontSize(15)
                    .fontColor(this.currentMusic?.id === item.id ? 
                      (this.isDarkTheme ? '#FF4158' : '#667EEA') : 
                      (this.isDarkTheme ? '#FFFFFF' : '#333333')
                    )
                    .fontWeight(this.currentMusic?.id === item.id ? FontWeight.Bold : FontWeight.Normal)
                    .maxLines(1)
                    .textOverflow({ overflow: TextOverflow.Ellipsis })

                  Text(`${item.artist} · ${formatTime(item.duration)}`)
                    .fontSize(12)
                    .fontColor(this.isDarkTheme ? '#AAAAAA' : '#999999')
                    .margin({ top: 2 })
                }
                .layoutWeight(1)
                .alignItems(HorizontalAlign.Start)

                if (this.currentMusic?.id === item.id && this.isPlaying) {
                  Image($r('app.media.ic_playing'))
                    .width(20)
                    .height(20)
                    .fillColor(this.isDarkTheme ? '#FF4158' : '#667EEA')
                }
              }
              .width('100%')
              .height(60)
              .padding({ left: 20, right: 20 })
              .backgroundColor(this.currentMusic?.id === item.id ? 
                (this.isDarkTheme ? 'rgba(255, 65, 88, 0.15)' : 'rgba(102, 126, 234, 0.1)') : 
                'transparent'
              )
              .onClick(() => {
                this.playMusic(item);
              })
            }
          }, (item: MusicItem) => item.id.toString())
        }
        .width('100%')
        .layoutWeight(1)
        .backgroundColor(this.isDarkTheme ? 'rgba(22, 33, 62, 0.3)' : '#FFFFFF')
        .divider({
          strokeWidth: 0.5,
          color: this.isDarkTheme ? 'rgba(255, 255, 255, 0.1)' : '#F0F0F0',
          startMargin: 50,
          endMargin: 20
        })
      }
      .width('100%')
      .height('60%')
      .backgroundColor(this.isDarkTheme ? 'rgba(22, 33, 62, 0.95)' : '#FFFFFF')
      .borderRadius({ topLeft: 20, topRight: 20 })
      .shadow({
        radius: 12,
        color: this.isDarkTheme ? '#40000000' : '#20000000',
        offsetX: 0,
        offsetY: -3
      })
    }
    .width('100%')
    .height('100%')
  }

  aboutToDisappear(): void {
    this.stopRotation();
    // 保存播放历史
    if (this.currentMusic && this.currentPosition > 0) {
      this.database.addPlayHistory(this.currentMusic.id, this.currentPosition);
    }
  }
}
相关推荐
waeng_luo3 小时前
HarmonyOS开发-多线程与异步编程
harmonyos·鸿蒙2025领航者闯关·鸿蒙6实战·#鸿蒙2025领航者闯关
花开彼岸天~4 小时前
鸿蒙平台使用 `video_thumbnail` 插件指南
华为·harmonyos
特立独行的猫a4 小时前
QT开发鸿蒙PC应用:环境搭建及第一个HelloWorld
开发语言·qt·harmonyos·环境搭建·鸿蒙pc
花开彼岸天~4 小时前
Flutter跨平台开发鸿蒙化定位组件使用指南
flutter·华为·harmonyos
sinat_384241095 小时前
HarmonyOS音乐播放器开发实战:从零到一打造完整鸿蒙系统音乐播放器应用 1
华为·harmonyos
yenggd6 小时前
华为批量下发配置命令使用telnetlib模块
网络·python·华为
个案命题7 小时前
鸿蒙ArkUI状态管理新宠:@Once装饰器全方位解析与实战
华为·harmonyos
云_杰7 小时前
取件伙伴性能提升——长列表
性能优化·harmonyos
花开彼岸天~9 小时前
Flutter跨平台开发鸿蒙化日志测试组件使用指南
flutter·elasticsearch·harmonyos