ahutchen commited on
Commit
a1eddda
·
1 Parent(s): f3dc7f8

refactor(search): 重构搜索功能并添加搜索历史页面

Browse files

- 移除 SearchBox 组件中的搜索历史显示逻辑
- 新增 SearchHistoryPage 组件用于展示搜索历史
- 更新 router 添加搜索历史页面路由
- 修改 search store 增加 removeHistoryItem 和 setKeyword 方法
- 调整 SearchResults 组件样式
- 优化 SongItem 组件中播放状态指示器位置

README.md CHANGED
@@ -1,6 +1,6 @@
1
  ---
2
- title: 云服务
3
- emoji: 🐢
4
  colorFrom: purple
5
  colorTo: indigo
6
  sdk: static
@@ -12,24 +12,8 @@ app_file: dist/index.html
12
 
13
  一个基于 Vue3 的渐进式网络应用(PWA),模仿网易云音乐界面风格,支持13个音乐源,提供完整的音乐播放体验。
14
 
15
- ## ✨ 功能特性
16
-
17
- ### 🎯 核心功能
18
- - **多源音乐搜索** - 支持13个主流音乐平台
19
- - **全屏播放器** - 网易云风格界面,支持歌词显示
20
- - **收藏管理** - 收藏喜爱的歌曲
21
- - **播放历史** - 自动记录播放记录
22
- - **智能设置** - 音质、播放模式、主题等个性化设置
23
-
24
- ### 🌟 技术特性
25
- - **PWA 支持** - 可安装到桌面,离线缓存
26
- - **响应式设计** - 适配手机、平板、桌面
27
- - **MediaSession API** - 支持媒体控制(锁屏控制)
28
- - **状态持久化** - 刷新不丢失播放状态
29
- - **网易云主题** - 完美复刻的视觉体验
30
-
31
  ### 📱 支持的音乐源
32
- 1. **网易云音乐** (默认)
33
  2. **QQ音乐**
34
  3. **酷狗音乐**
35
  4. **酷我音乐**
@@ -59,136 +43,9 @@ npm install
59
  npm run dev
60
  ```
61
 
62
- ### 或者直接运行启动脚本
63
- - Windows: 双击 `启动应用.bat`
64
- - macOS/Linux: 运行 `./启动应用.sh`
65
-
66
  ### 构建生产版本
67
  ```bash
68
  npm run build
69
  ```
70
 
71
- ## 📁 项目结构
72
-
73
- ```
74
- vue-music/
75
- ├── public/ # 静态资源
76
- │ ├── icons/ # PWA 图标
77
- │ └── manifest.json # PWA 配置
78
- ├── src/
79
- │ ├── components/ # 组件
80
- │ │ ├── layout/ # 布局组件
81
- │ │ ├── search/ # 搜索相关
82
- │ │ ├── player/ # 播放器相关
83
- │ │ └── common/ # 通用组件
84
- │ ├── views/ # 页面组件
85
- │ ├── stores/ # Pinia 状态管理
86
- │ ├── services/ # API 服务
87
- │ ├── router/ # 路由配置
88
- │ └── styles/ # 全局样式
89
- ├── 需求文档.md # 完整需求说明
90
- └── 启动应用.bat/sh # 快速启动脚本
91
- ```
92
-
93
- ## 🎮 使用说明
94
-
95
- ### 基本操作
96
- 1. **搜索音乐** - 在首页搜索框输入歌曲或歌手名
97
- 2. **切换音源** - 点击搜索框旁的源选择按钮
98
- 3. **播放控制** - 点击歌曲播放,使用底部播放条控制
99
- 4. **全屏播放器** - 点击底部播放条进入全屏播放器
100
- 5. **收藏歌曲** - 点击爱心图标收藏喜欢的歌曲
101
-
102
- ### 高级功能
103
- - **播放模式** - 支持列表循环、随机播放、单曲循环
104
- - **歌词显示** - 全屏播放器支持歌词滚动显示
105
- - **设置调整** - 在设置页面个性化应用体验
106
- - **PWA 安装** - 在浏览器地址栏点击安装按钮
107
-
108
- ## 🛠️ 技术栈
109
-
110
- - **框架**: Vue 3.4 + Composition API
111
- - **状态管理**: Pinia 2.1
112
- - **路由**: Vue Router 4.3
113
- - **构建工具**: Vite 5.3
114
- - **PWA**: Vite PWA Plugin
115
- - **样式**: CSS3 + CSS Variables
116
- - **图标**: FontAwesome 6
117
- - **音乐API**: 自建聚合接口
118
-
119
- ## ⚡ 性能优化
120
-
121
- - **代码分割** - 路由级别的代码分割
122
- - **图片懒加载** - 专辑封面按需加载
123
- - **请求缓存** - API 响应智能缓存
124
- - **Service Worker** - 静态资源离线缓存
125
- - **组件复用** - 高效的组件设计模式
126
-
127
- ## 🔧 开发特性
128
-
129
- ### 代码质量
130
- - **组件化架构** - 高内聚低耦合的组件设计
131
- - **TypeScript 支持** - 可选的类型检查
132
- - **ESLint 配置** - 代码风格统一
133
- - **响应式设计** - 移动优先的设计理念
134
-
135
- ### 开发体验
136
- - **热重载** - 代码修改实时预览
137
- - **自动导入** - 组件和工具函数自动导入
138
- - **开发工具** - Vue DevTools 完美支持
139
- - **错误处理** - 完善的错误边界处理
140
-
141
- ## 📋 功能清单
142
-
143
- ### ✅ 已完成功能
144
- - [x] 项目架构搭建
145
- - [x] PWA 配置
146
- - [x] 音乐API集成 (13个源)
147
- - [x] 状态管理 (Pinia)
148
- - [x] 首页搜索功能
149
- - [x] 搜索结果展示
150
- - [x] 全屏播放器
151
- - [x] 播放控制
152
- - [x] 进度条
153
- - [x] 歌词显示
154
- - [x] 收藏功能
155
- - [x] 播放历史
156
- - [x] 设置页面
157
- - [x] 响应式布局
158
- - [x] 通用组件库
159
-
160
- ### 🔄 可扩展功能
161
- - [ ] 用户登录系统
162
- - [ ] 社交分享功能
163
- - [ ] 音乐下载功能
164
- - [ ] 播放列表导入导出
165
- - [ ] 桌面歌词显示
166
- - [ ] 均衡器设置
167
- - [ ] 定时关闭功能
168
- - [ ] 音乐推荐算法
169
-
170
- ## 🤝 贡献指南
171
-
172
- 欢迎提交 Issue 和 Pull Request!
173
-
174
- 1. Fork 项目
175
- 2. 创建特性分支 (`git checkout -b feature/AmazingFeature`)
176
- 3. 提交更改 (`git commit -m 'Add some AmazingFeature'`)
177
- 4. 推送到分支 (`git push origin feature/AmazingFeature`)
178
- 5. 打开 Pull Request
179
-
180
- ## 📄 许可证
181
-
182
- 本项目基于 MIT 许可证开源 - 查看 [LICENSE](LICENSE) 文件了解详情。
183
-
184
- ## 🙏 致谢
185
-
186
- - [Vue.js](https://vuejs.org/) - 渐进式 JavaScript 框架
187
- - [Vite](https://vitejs.dev/) - 下一代前端构建工具
188
- - [Pinia](https://pinia.vuejs.org/) - Vue 状态管理库
189
- - [FontAwesome](https://fontawesome.com/) - 图标库
190
- - [网易云音乐](https://music.163.com/) - UI 设计参考
191
-
192
- ---
193
-
194
  **🎵 享受音乐,享受编程!**
 
1
  ---
2
+ title: 云音乐
3
+ emoji: 🎵
4
  colorFrom: purple
5
  colorTo: indigo
6
  sdk: static
 
12
 
13
  一个基于 Vue3 的渐进式网络应用(PWA),模仿网易云音乐界面风格,支持13个音乐源,提供完整的音乐播放体验。
14
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  ### 📱 支持的音乐源
16
+ 1. **网易云音乐**
17
  2. **QQ音乐**
18
  3. **酷狗音乐**
19
  4. **酷我音乐**
 
43
  npm run dev
44
  ```
45
 
 
 
 
 
46
  ### 构建生产版本
47
  ```bash
48
  npm run build
49
  ```
50
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  **🎵 享受音乐,享受编程!**
src/App.vue CHANGED
@@ -1,5 +1,8 @@
1
  <template>
2
  <div id="app">
 
 
 
3
  <!-- 主内容区域 -->
4
  <main class="main-content">
5
  <router-view v-slot="{ Component, route }">
@@ -65,6 +68,7 @@ import { useFavoritesStore } from '@/stores/favorites'
65
  import { useHistoryStore } from '@/stores/history'
66
  import { useSettingsStore } from '@/stores/settings'
67
  import { musicApi, utils } from '@/services/musicApi'
 
68
  import AppTabBar from '@/components/layout/AppTabBar.vue'
69
  import MiniPlayer from '@/components/layout/MiniPlayer.vue'
70
  import FullPlayerPage from '@/views/FullPlayerPage.vue'
@@ -366,7 +370,14 @@ onMounted(() => {
366
  loadAndPlaySong(event.detail.song)
367
  }
368
  }
 
 
 
 
 
 
369
  window.addEventListener('loadAndPlaySong', handleLoadAndPlaySong)
 
370
 
371
  // 恢复逻辑交由watch处理,避免重复设置
372
  console.log('App mounted, 恢复状态:', {
@@ -391,6 +402,24 @@ onMounted(() => {
391
  background: var(--bg-primary);
392
  }
393
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
394
  /* iOS PWA 模式适配 */
395
  @supports (-webkit-touch-callout: none) {
396
  @media all and (display-mode: standalone) {
@@ -402,5 +431,16 @@ onMounted(() => {
402
  padding-bottom: calc(var(--tabbar-height) + 20px);
403
  }
404
  }
 
 
 
 
 
 
 
 
 
 
 
405
  }
406
  </style>
 
1
  <template>
2
  <div id="app">
3
+ <!-- PC端侧边栏导航 -->
4
+ <DesktopSidebar />
5
+
6
  <!-- 主内容区域 -->
7
  <main class="main-content">
8
  <router-view v-slot="{ Component, route }">
 
68
  import { useHistoryStore } from '@/stores/history'
69
  import { useSettingsStore } from '@/stores/settings'
70
  import { musicApi, utils } from '@/services/musicApi'
71
+ import DesktopSidebar from '@/components/layout/DesktopSidebar.vue'
72
  import AppTabBar from '@/components/layout/AppTabBar.vue'
73
  import MiniPlayer from '@/components/layout/MiniPlayer.vue'
74
  import FullPlayerPage from '@/views/FullPlayerPage.vue'
 
370
  loadAndPlaySong(event.detail.song)
371
  }
372
  }
373
+
374
+ // 监听来自侧边栏的播放控制事件
375
+ const handleSidebarTogglePlay = () => {
376
+ togglePlay()
377
+ }
378
+
379
  window.addEventListener('loadAndPlaySong', handleLoadAndPlaySong)
380
+ window.addEventListener('sidebarTogglePlay', handleSidebarTogglePlay)
381
 
382
  // 恢复逻辑交由watch处理,避免重复设置
383
  console.log('App mounted, 恢复状态:', {
 
402
  background: var(--bg-primary);
403
  }
404
 
405
+ /* PC端布局适配 */
406
+ @media (min-width: 1024px) {
407
+ #app {
408
+ display: flex;
409
+ height: 100vh;
410
+ }
411
+
412
+ .main-content {
413
+ flex: 1;
414
+ margin-left: 280px; /* 侧边栏宽度 */
415
+ padding-bottom: var(--mini-player-height);
416
+ }
417
+
418
+ .main-content:not(.has-mini-player) {
419
+ padding-bottom: 0;
420
+ }
421
+ }
422
+
423
  /* iOS PWA 模式适配 */
424
  @supports (-webkit-touch-callout: none) {
425
  @media all and (display-mode: standalone) {
 
431
  padding-bottom: calc(var(--tabbar-height) + 20px);
432
  }
433
  }
434
+
435
+ /* PC端PWA模式 */
436
+ @media (min-width: 1024px) and (display-mode: standalone) {
437
+ .main-content {
438
+ padding-bottom: var(--mini-player-height);
439
+ }
440
+
441
+ .main-content:not(.has-mini-player) {
442
+ padding-bottom: 0;
443
+ }
444
+ }
445
  }
446
  </style>
src/components/layout/AppTabBar.vue CHANGED
@@ -155,4 +155,11 @@ const handleTabClick = (tab) => {
155
  border-top: 1px solid var(--border-strong);
156
  }
157
  }
 
 
 
 
 
 
 
158
  </style>
 
155
  border-top: 1px solid var(--border-strong);
156
  }
157
  }
158
+
159
+ /* PC端隐藏底部标签栏 */
160
+ @media (min-width: 1024px) {
161
+ .app-tabbar {
162
+ display: none;
163
+ }
164
+ }
165
  </style>
src/components/layout/DesktopSidebar.vue ADDED
@@ -0,0 +1,751 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <template>
2
+ <div class="desktop-sidebar hidden-mobile">
3
+ <!-- 应用Logo和标题 -->
4
+ <div class="sidebar-header">
5
+ <div class="app-logo">
6
+ <i class="fas fa-music"></i>
7
+ </div>
8
+ <div class="app-title">云音乐</div>
9
+ </div>
10
+
11
+ <!-- 导航菜单 -->
12
+ <nav class="sidebar-nav">
13
+ <router-link
14
+ v-for="item in navItems"
15
+ :key="item.name"
16
+ :to="item.path"
17
+ class="nav-item"
18
+ :class="{ active: currentRoute === item.name }"
19
+ @click="handleNavClick(item)"
20
+ >
21
+ <i :class="item.icon" class="nav-icon"></i>
22
+ <span class="nav-label">{{ item.label }}</span>
23
+ <div class="nav-indicator" v-if="currentRoute === item.name"></div>
24
+ </router-link>
25
+ </nav>
26
+
27
+ <!-- 播放控制区域 -->
28
+ <div class="sidebar-player" v-if="currentSong">
29
+ <!-- 当前播放歌曲信息 -->
30
+ <div class="current-song" @click="openFullPlayer">
31
+ <div class="song-cover">
32
+ <img
33
+ :src="currentCoverUrl || defaultCover"
34
+ :alt="currentSong.name"
35
+ class="cover-image"
36
+ @error="handleImageError"
37
+ />
38
+ <div class="play-overlay" @click.stop="togglePlay">
39
+ <i :class="isPlaying ? 'fas fa-pause' : 'fas fa-play'"></i>
40
+ </div>
41
+ </div>
42
+ <div class="song-details">
43
+ <div class="song-name" :title="currentSong.name">{{ currentSong.name }}</div>
44
+ <div class="song-artist" :title="formattedArtist">{{ formattedArtist }}</div>
45
+ </div>
46
+ </div>
47
+
48
+ <!-- 播放控制按钮 -->
49
+ <div class="player-controls">
50
+ <button class="control-btn" @click="playPrevious" title="上一首">
51
+ <i class="fas fa-step-backward"></i>
52
+ </button>
53
+ <button class="control-btn play-btn" @click="togglePlay" :title="isPlaying ? '暂停' : '播放'">
54
+ <i :class="isPlaying ? 'fas fa-pause' : 'fas fa-play'"></i>
55
+ </button>
56
+ <button class="control-btn" @click="playNext" title="下一首">
57
+ <i class="fas fa-step-forward"></i>
58
+ </button>
59
+ <button class="control-btn" @click="openFullPlayer" title="打开播放器">
60
+ <i class="fas fa-expand"></i>
61
+ </button>
62
+ </div>
63
+
64
+ <!-- 进度条 -->
65
+ <div class="progress-container">
66
+ <div class="progress-background" @click="handleProgressClick" ref="progressRef">
67
+ <div
68
+ class="progress-fill"
69
+ :style="{ width: `${progress}%` }"
70
+ ></div>
71
+ </div>
72
+ </div>
73
+
74
+ <!-- 音量控制 -->
75
+ <div class="volume-control">
76
+ <button class="volume-btn" @click="toggleMute">
77
+ <i :class="volumeIcon"></i>
78
+ </button>
79
+ <input
80
+ type="range"
81
+ min="0"
82
+ max="100"
83
+ :value="volume"
84
+ @input="handleVolumeChange"
85
+ class="volume-slider"
86
+ />
87
+ </div>
88
+ </div>
89
+
90
+ <!-- 播放统计 -->
91
+ <div class="sidebar-stats" v-if="!currentSong">
92
+ <div class="stats-item">
93
+ <i class="fas fa-music"></i>
94
+ <div class="stats-info">
95
+ <div class="stats-label">暂无播放</div>
96
+ <div class="stats-value">选择音乐开始播放</div>
97
+ </div>
98
+ </div>
99
+ </div>
100
+
101
+ <!-- 快捷操作 -->
102
+ <div class="sidebar-actions">
103
+ <button class="action-btn" @click="toggleTheme" :title="isDarkTheme ? '切换到亮色主题' : '切换到暗色主题'">
104
+ <i :class="isDarkTheme ? 'fas fa-sun' : 'fas fa-moon'"></i>
105
+ </button>
106
+ <button class="action-btn" @click="openSettings" title="设置">
107
+ <i class="fas fa-cog"></i>
108
+ </button>
109
+ </div>
110
+ </div>
111
+ </template>
112
+
113
+ <script setup>
114
+ import { computed, ref, watch } from 'vue'
115
+ import { useRoute, useRouter } from 'vue-router'
116
+ import { usePlayerStore } from '@/stores/player'
117
+ import { usePlayQueueStore } from '@/stores/playqueue'
118
+ import { useSettingsStore } from '@/stores/settings'
119
+ import { utils } from '@/services/musicApi'
120
+
121
+ const route = useRoute()
122
+ const router = useRouter()
123
+ const playerStore = usePlayerStore()
124
+ const playQueueStore = usePlayQueueStore()
125
+ const settingsStore = useSettingsStore()
126
+
127
+ // 播放器相关状态
128
+ const currentCoverUrl = ref('')
129
+ const isMuted = ref(false)
130
+ const lastVolume = ref(80)
131
+ const progressRef = ref(null)
132
+
133
+ // 导航项配置
134
+ const navItems = [
135
+ {
136
+ name: 'Home',
137
+ path: '/home',
138
+ label: '首页',
139
+ icon: 'fas fa-home'
140
+ },
141
+ {
142
+ name: 'Favorites',
143
+ path: '/favorites',
144
+ label: '我喜欢',
145
+ icon: 'fas fa-heart'
146
+ },
147
+ {
148
+ name: 'Playlists',
149
+ path: '/playlists',
150
+ label: '歌单',
151
+ icon: 'fas fa-music'
152
+ },
153
+ {
154
+ name: 'PlayQueue',
155
+ path: '/play-queue',
156
+ label: '播放列表',
157
+ icon: 'fas fa-list'
158
+ },
159
+ {
160
+ name: 'History',
161
+ path: '/history',
162
+ label: '播放历史',
163
+ icon: 'fas fa-history'
164
+ }
165
+ ]
166
+
167
+ // 计算属性
168
+ const currentRoute = computed(() => route.name)
169
+ const currentSong = computed(() => playerStore.currentSong)
170
+ const isPlaying = computed(() => playerStore.isPlaying)
171
+ const progress = computed(() => playerStore.progress)
172
+ const volume = computed(() => playerStore.volume || 80)
173
+ const isDarkTheme = computed(() => settingsStore.settings.theme === 'dark')
174
+
175
+ const formattedArtist = computed(() => {
176
+ if (!currentSong.value?.artist) return ''
177
+ return utils.formatArtist(currentSong.value.artist)
178
+ })
179
+
180
+ // 默认封面
181
+ const defaultCover = computed(() => {
182
+ return 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjQiIGhlaWdodD0iNjQiIHZpZXdCb3g9IjAgMCA2NCA2NCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjY0IiBoZWlnaHQ9IjY0IiBmaWxsPSJyZ2JhKDI1NSwyNTUsMjU1LDAuMSkiIHJ4PSI4Ii8+CjxwYXRoIGQ9Ik0zMiAyMEw0MCAzMkgzNlY0NEgyOFYzMkgyNEwzMiAyMFoiIGZpbGw9InJnYmEoMjU1LDI1NSwyNTUsMC4zKSIvPgo8L3N2Zz4K'
183
+ })
184
+
185
+ // 音量图标
186
+ const volumeIcon = computed(() => {
187
+ if (isMuted.value || volume.value === 0) {
188
+ return 'fas fa-volume-mute'
189
+ } else if (volume.value < 30) {
190
+ return 'fas fa-volume-off'
191
+ } else if (volume.value < 70) {
192
+ return 'fas fa-volume-down'
193
+ } else {
194
+ return 'fas fa-volume-up'
195
+ }
196
+ })
197
+
198
+ // 方法
199
+ const handleNavClick = (item) => {
200
+ console.log('导航到:', item.label)
201
+ }
202
+
203
+ // 播放器控制方法
204
+ const togglePlay = () => {
205
+ // 通过全局事件触发App.vue中的播放控制
206
+ window.dispatchEvent(new CustomEvent('sidebarTogglePlay'))
207
+ }
208
+
209
+ const playPrevious = () => {
210
+ const prevSong = playQueueStore.playPrevious()
211
+ if (prevSong) {
212
+ playerStore.playSong(prevSong)
213
+ }
214
+ }
215
+
216
+ const playNext = () => {
217
+ const nextSong = playQueueStore.playNext()
218
+ if (nextSong) {
219
+ playerStore.playSong(nextSong)
220
+ }
221
+ }
222
+
223
+ const openFullPlayer = () => {
224
+ if (currentSong.value) {
225
+ router.push('/player')
226
+ }
227
+ }
228
+
229
+ // 进度条控制
230
+ const handleProgressClick = (event) => {
231
+ if (!progressRef.value || !playerStore.duration) return
232
+
233
+ const rect = progressRef.value.getBoundingClientRect()
234
+ const percent = (event.clientX - rect.left) / rect.width
235
+ const newTime = percent * playerStore.duration
236
+
237
+ playerStore.seekTo(newTime)
238
+ }
239
+
240
+ // 音量控制
241
+ const toggleMute = () => {
242
+ if (isMuted.value) {
243
+ playerStore.setVolume(lastVolume.value)
244
+ isMuted.value = false
245
+ } else {
246
+ lastVolume.value = volume.value
247
+ playerStore.setVolume(0)
248
+ isMuted.value = true
249
+ }
250
+ }
251
+
252
+ const handleVolumeChange = (event) => {
253
+ const newVolume = parseInt(event.target.value)
254
+ playerStore.setVolume(newVolume)
255
+ isMuted.value = newVolume === 0
256
+ }
257
+
258
+ // 图片加载错误处理
259
+ const handleImageError = () => {
260
+ currentCoverUrl.value = defaultCover.value
261
+ }
262
+
263
+ // 加载专辑封面
264
+ const loadAlbumCover = async () => {
265
+ if (!currentSong.value) {
266
+ currentCoverUrl.value = defaultCover.value
267
+ return
268
+ }
269
+
270
+ try {
271
+ const coverUrlResult = await playerStore.getAlbumCover(currentSong.value, 300)
272
+ if (coverUrlResult) {
273
+ currentCoverUrl.value = coverUrlResult
274
+ } else {
275
+ currentCoverUrl.value = defaultCover.value
276
+ }
277
+ } catch (error) {
278
+ console.error('加载侧边栏封面失败:', error)
279
+ currentCoverUrl.value = defaultCover.value
280
+ }
281
+ }
282
+
283
+ const toggleTheme = () => {
284
+ const newTheme = isDarkTheme.value ? 'light' : 'dark'
285
+ settingsStore.updateSetting('theme', newTheme)
286
+ settingsStore.applyTheme(newTheme)
287
+ }
288
+
289
+ const openSettings = () => {
290
+ router.push('/settings')
291
+ }
292
+
293
+ // 监听当前歌曲变化
294
+ watch(currentSong, (newSong) => {
295
+ if (newSong) {
296
+ loadAlbumCover()
297
+ } else {
298
+ currentCoverUrl.value = defaultCover.value
299
+ }
300
+ }, { immediate: true })
301
+ </script>
302
+
303
+ <style scoped>
304
+ .desktop-sidebar {
305
+ width: 280px;
306
+ height: 100vh;
307
+ background: var(--bg-card);
308
+ border-right: 1px solid var(--border-light);
309
+ display: flex;
310
+ flex-direction: column;
311
+ position: fixed;
312
+ left: 0;
313
+ top: 0;
314
+ z-index: 100;
315
+ backdrop-filter: blur(20px);
316
+ }
317
+
318
+ /* 应用头部 */
319
+ .sidebar-header {
320
+ display: flex;
321
+ align-items: center;
322
+ gap: 12px;
323
+ padding: 24px 20px;
324
+ border-bottom: 1px solid var(--border-lighter);
325
+ }
326
+
327
+ .app-logo {
328
+ width: 40px;
329
+ height: 40px;
330
+ background: linear-gradient(135deg, var(--primary-color), var(--primary-color-hover));
331
+ border-radius: 12px;
332
+ display: flex;
333
+ align-items: center;
334
+ justify-content: center;
335
+ color: white;
336
+ font-size: 18px;
337
+ box-shadow: 0 4px 12px var(--glow-color);
338
+ }
339
+
340
+ .app-title {
341
+ font-size: 20px;
342
+ font-weight: 700;
343
+ color: var(--text-primary);
344
+ }
345
+
346
+ /* 导航菜单 */
347
+ .sidebar-nav {
348
+ flex: 1;
349
+ padding: 16px 0;
350
+ overflow-y: auto;
351
+ }
352
+
353
+ .nav-item {
354
+ display: flex;
355
+ align-items: center;
356
+ gap: 12px;
357
+ padding: 16px 20px;
358
+ text-decoration: none;
359
+ color: var(--text-secondary);
360
+ transition: all var(--transition-fast);
361
+ position: relative;
362
+ margin: 0 12px;
363
+ border-radius: 12px;
364
+ }
365
+
366
+ .nav-item:hover {
367
+ background: var(--overlay-lighter);
368
+ color: var(--text-primary);
369
+ transform: translateX(4px);
370
+ }
371
+
372
+ .nav-item.active {
373
+ background: linear-gradient(135deg, var(--primary-color), var(--primary-color-hover));
374
+ color: white;
375
+ box-shadow: 0 4px 12px var(--glow-color);
376
+ }
377
+
378
+ .nav-item.active .nav-icon {
379
+ color: white;
380
+ }
381
+
382
+ .nav-icon {
383
+ font-size: 18px;
384
+ width: 20px;
385
+ text-align: center;
386
+ transition: var(--transition-fast);
387
+ }
388
+
389
+ .nav-label {
390
+ font-size: 16px;
391
+ font-weight: 500;
392
+ }
393
+
394
+ .nav-indicator {
395
+ position: absolute;
396
+ right: 0;
397
+ top: 50%;
398
+ transform: translateY(-50%);
399
+ width: 4px;
400
+ height: 20px;
401
+ background: white;
402
+ border-radius: 2px;
403
+ opacity: 0.8;
404
+ }
405
+
406
+ /* 播放统计 */
407
+ .sidebar-stats {
408
+ padding: 16px 20px;
409
+ border-top: 1px solid var(--border-lighter);
410
+ border-bottom: 1px solid var(--border-lighter);
411
+ }
412
+
413
+ .stats-item {
414
+ display: flex;
415
+ align-items: center;
416
+ gap: 12px;
417
+ padding: 12px 0;
418
+ }
419
+
420
+ .stats-item i {
421
+ font-size: 16px;
422
+ color: var(--primary-color);
423
+ width: 20px;
424
+ text-align: center;
425
+ }
426
+
427
+ .stats-info {
428
+ flex: 1;
429
+ min-width: 0;
430
+ }
431
+
432
+ .stats-label {
433
+ font-size: 12px;
434
+ color: var(--text-tertiary);
435
+ margin-bottom: 2px;
436
+ }
437
+
438
+ .stats-value {
439
+ font-size: 14px;
440
+ font-weight: 600;
441
+ color: var(--text-primary);
442
+ white-space: nowrap;
443
+ overflow: hidden;
444
+ text-overflow: ellipsis;
445
+ }
446
+
447
+ /* 快捷操作 */
448
+ .sidebar-actions {
449
+ display: flex;
450
+ gap: 8px;
451
+ padding: 16px 20px;
452
+ }
453
+
454
+ .action-btn {
455
+ flex: 1;
456
+ height: 40px;
457
+ border: none;
458
+ background: var(--overlay-lighter);
459
+ color: var(--text-secondary);
460
+ border-radius: 10px;
461
+ cursor: pointer;
462
+ display: flex;
463
+ align-items: center;
464
+ justify-content: center;
465
+ font-size: 16px;
466
+ transition: var(--transition-fast);
467
+ }
468
+
469
+ .action-btn:hover {
470
+ background: var(--overlay-light);
471
+ color: var(--text-primary);
472
+ transform: scale(1.05);
473
+ }
474
+
475
+ /* 滚动条样式 */
476
+ .sidebar-nav::-webkit-scrollbar {
477
+ width: 4px;
478
+ }
479
+
480
+ .sidebar-nav::-webkit-scrollbar-track {
481
+ background: transparent;
482
+ }
483
+
484
+ .sidebar-nav::-webkit-scrollbar-thumb {
485
+ background: rgba(255, 255, 255, 0.2);
486
+ border-radius: 2px;
487
+ }
488
+
489
+ .sidebar-nav::-webkit-scrollbar-thumb:hover {
490
+ background: rgba(255, 255, 255, 0.4);
491
+ }
492
+
493
+ /* 黑色主题适配 */
494
+ [data-theme="dark"] .sidebar-nav::-webkit-scrollbar-thumb {
495
+ background: rgba(255, 255, 255, 0.1);
496
+ }
497
+
498
+ [data-theme="dark"] .sidebar-nav::-webkit-scrollbar-thumb:hover {
499
+ background: rgba(255, 255, 255, 0.2);
500
+ }
501
+
502
+ /* 侧边栏播放器 */
503
+ .sidebar-player {
504
+ padding: 20px;
505
+ border-top: 1px solid var(--border-lighter);
506
+ border-bottom: 1px solid var(--border-lighter);
507
+ background: var(--overlay-lighter);
508
+ backdrop-filter: blur(10px);
509
+ }
510
+
511
+ .current-song {
512
+ display: flex;
513
+ align-items: center;
514
+ gap: 12px;
515
+ margin-bottom: 16px;
516
+ cursor: pointer;
517
+ transition: var(--transition-fast);
518
+ border-radius: 8px;
519
+ padding: 4px;
520
+ }
521
+
522
+ .current-song:hover {
523
+ background: var(--overlay-lighter);
524
+ transform: scale(1.02);
525
+ }
526
+
527
+ .song-cover {
528
+ position: relative;
529
+ width: 48px;
530
+ height: 48px;
531
+ border-radius: 8px;
532
+ overflow: hidden;
533
+ flex-shrink: 0;
534
+ }
535
+
536
+ .cover-image {
537
+ width: 100%;
538
+ height: 100%;
539
+ object-fit: cover;
540
+ }
541
+
542
+ .play-overlay {
543
+ position: absolute;
544
+ top: 0;
545
+ left: 0;
546
+ right: 0;
547
+ bottom: 0;
548
+ background: rgba(0, 0, 0, 0.5);
549
+ display: flex;
550
+ align-items: center;
551
+ justify-content: center;
552
+ opacity: 0;
553
+ transition: var(--transition-fast);
554
+ cursor: pointer;
555
+ }
556
+
557
+ .song-cover:hover .play-overlay {
558
+ opacity: 1;
559
+ }
560
+
561
+ .play-overlay i {
562
+ color: white;
563
+ font-size: 14px;
564
+ }
565
+
566
+ .song-details {
567
+ flex: 1;
568
+ min-width: 0;
569
+ }
570
+
571
+ .song-name {
572
+ font-size: 14px;
573
+ font-weight: 600;
574
+ color: var(--text-primary);
575
+ margin-bottom: 2px;
576
+ white-space: nowrap;
577
+ overflow: hidden;
578
+ text-overflow: ellipsis;
579
+ }
580
+
581
+ .song-artist {
582
+ font-size: 12px;
583
+ color: var(--text-secondary);
584
+ white-space: nowrap;
585
+ overflow: hidden;
586
+ text-overflow: ellipsis;
587
+ }
588
+
589
+ /* 播放控制按钮 */
590
+ .player-controls {
591
+ display: flex;
592
+ align-items: center;
593
+ justify-content: center;
594
+ gap: 8px;
595
+ margin-bottom: 16px;
596
+ }
597
+
598
+ .control-btn {
599
+ width: 32px;
600
+ height: 32px;
601
+ border-radius: 50%;
602
+ background: var(--overlay-light);
603
+ border: 1px solid var(--border-light);
604
+ color: var(--text-secondary);
605
+ cursor: pointer;
606
+ display: flex;
607
+ align-items: center;
608
+ justify-content: center;
609
+ font-size: 12px;
610
+ transition: var(--transition-fast);
611
+ }
612
+
613
+ .control-btn:hover {
614
+ background: var(--overlay-light);
615
+ color: var(--text-primary);
616
+ transform: scale(1.1);
617
+ }
618
+
619
+ .control-btn.play-btn {
620
+ width: 36px;
621
+ height: 36px;
622
+ font-size: 14px;
623
+ background: var(--primary-color);
624
+ color: white;
625
+ border-color: var(--primary-color);
626
+ }
627
+
628
+ .control-btn.play-btn:hover {
629
+ background: var(--primary-color-hover);
630
+ border-color: var(--primary-color-hover);
631
+ box-shadow: 0 0 12px var(--glow-color);
632
+ }
633
+
634
+ /* 进度条 */
635
+ .progress-container {
636
+ margin-bottom: 16px;
637
+ }
638
+
639
+ .progress-background {
640
+ height: 4px;
641
+ background: var(--border-light);
642
+ border-radius: 2px;
643
+ cursor: pointer;
644
+ position: relative;
645
+ transition: var(--transition-fast);
646
+ }
647
+
648
+ .progress-background:hover {
649
+ height: 6px;
650
+ transform: scaleY(1.2);
651
+ }
652
+
653
+ .progress-fill {
654
+ height: 100%;
655
+ background: var(--primary-color);
656
+ border-radius: 2px;
657
+ transition: width 0.1s ease;
658
+ position: relative;
659
+ }
660
+
661
+ .progress-fill::after {
662
+ content: '';
663
+ position: absolute;
664
+ right: -2px;
665
+ top: 50%;
666
+ transform: translateY(-50%);
667
+ width: 8px;
668
+ height: 8px;
669
+ background: var(--primary-color);
670
+ border-radius: 50%;
671
+ opacity: 0;
672
+ transition: var(--transition-fast);
673
+ }
674
+
675
+ .progress-background:hover .progress-fill::after {
676
+ opacity: 1;
677
+ }
678
+
679
+ /* 音量控制 */
680
+ .volume-control {
681
+ display: flex;
682
+ align-items: center;
683
+ gap: 12px;
684
+ }
685
+
686
+ .volume-btn {
687
+ width: 28px;
688
+ height: 28px;
689
+ border-radius: 50%;
690
+ background: var(--overlay-lighter);
691
+ border: none;
692
+ color: var(--text-secondary);
693
+ cursor: pointer;
694
+ display: flex;
695
+ align-items: center;
696
+ justify-content: center;
697
+ font-size: 10px;
698
+ transition: var(--transition-fast);
699
+ flex-shrink: 0;
700
+ }
701
+
702
+ .volume-btn:hover {
703
+ background: var(--overlay-light);
704
+ color: var(--text-primary);
705
+ }
706
+
707
+ .volume-slider {
708
+ flex: 1;
709
+ height: 3px;
710
+ background: var(--border-light);
711
+ border-radius: 2px;
712
+ outline: none;
713
+ appearance: none;
714
+ cursor: pointer;
715
+ transition: var(--transition-fast);
716
+ }
717
+
718
+ .volume-slider::-webkit-slider-thumb {
719
+ appearance: none;
720
+ width: 12px;
721
+ height: 12px;
722
+ border-radius: 50%;
723
+ background: var(--primary-color);
724
+ cursor: pointer;
725
+ border: 2px solid white;
726
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
727
+ transition: var(--transition-fast);
728
+ }
729
+
730
+ .volume-slider::-webkit-slider-thumb:hover {
731
+ transform: scale(1.2);
732
+ }
733
+
734
+ .volume-slider::-moz-range-thumb {
735
+ width: 12px;
736
+ height: 12px;
737
+ border-radius: 50%;
738
+ background: var(--primary-color);
739
+ cursor: pointer;
740
+ border: 2px solid white;
741
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
742
+ transition: var(--transition-fast);
743
+ }
744
+
745
+ /* 响应式 - 只在PC端显示 */
746
+ @media (max-width: 1023px) {
747
+ .desktop-sidebar {
748
+ display: none;
749
+ }
750
+ }
751
+ </style>
src/components/layout/MiniPlayer.vue CHANGED
@@ -28,13 +28,45 @@
28
 
29
  <!-- 播放控制 -->
30
  <div class="play-controls">
 
31
  <button
32
- class="control-btn"
 
 
 
 
 
 
 
33
  @click.stop="togglePlay"
34
  :disabled="!audioSrc"
35
  >
36
  <i :class="isPlaying ? 'fas fa-pause' : 'fas fa-play'"></i>
37
  </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  </div>
39
 
40
  <!-- 进度条 -->
@@ -62,6 +94,10 @@ const router = useRouter()
62
  const playerStore = usePlayerStore()
63
  const coverUrl = ref('')
64
 
 
 
 
 
65
  // 触摸处理
66
  const touchStartY = ref(0)
67
  const touchStartX = ref(0)
@@ -72,6 +108,20 @@ const currentSong = computed(() => playerStore.currentSong)
72
  const isPlaying = computed(() => playerStore.isPlaying)
73
  const progress = computed(() => playerStore.progress)
74
  const audioSrc = computed(() => playerStore.audioSrc)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
 
76
  // 格式化歌手名
77
  const formatArtist = (artist) => {
@@ -93,6 +143,26 @@ const openFullPlayer = () => {
93
  }
94
  }
95
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  // 加载专辑封面
97
  const loadCover = async () => {
98
  if (!currentSong.value) {
@@ -261,6 +331,9 @@ onMounted(() => {
261
 
262
  .play-controls {
263
  flex-shrink: 0;
 
 
 
264
  }
265
 
266
  .control-btn {
@@ -280,6 +353,105 @@ onMounted(() => {
280
  backdrop-filter: blur(10px);
281
  }
282
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
283
  /* 播放状态时的视觉反馈 - 立即显示 */
284
  .mini-player .control-btn {
285
  transition: all var(--transition-fast);
 
28
 
29
  <!-- 播放控制 -->
30
  <div class="play-controls">
31
+ <!-- PC端增加上一首按钮 -->
32
  <button
33
+ class="control-btn prev-btn hidden-mobile"
34
+ @click.stop="$emit('playPrevious')"
35
+ >
36
+ <i class="fas fa-step-backward"></i>
37
+ </button>
38
+
39
+ <button
40
+ class="control-btn play-btn"
41
  @click.stop="togglePlay"
42
  :disabled="!audioSrc"
43
  >
44
  <i :class="isPlaying ? 'fas fa-pause' : 'fas fa-play'"></i>
45
  </button>
46
+
47
+ <!-- PC端增加下一首按钮 -->
48
+ <button
49
+ class="control-btn next-btn hidden-mobile"
50
+ @click.stop="$emit('playNext')"
51
+ >
52
+ <i class="fas fa-step-forward"></i>
53
+ </button>
54
+ </div>
55
+
56
+ <!-- PC端音量控制 -->
57
+ <div class="volume-control hidden-mobile">
58
+ <button class="volume-btn" @click.stop="toggleMute">
59
+ <i :class="volumeIcon"></i>
60
+ </button>
61
+ <input
62
+ type="range"
63
+ min="0"
64
+ max="100"
65
+ :value="volume"
66
+ @input="handleVolumeChange"
67
+ @click.stop
68
+ class="volume-slider"
69
+ />
70
  </div>
71
 
72
  <!-- 进度条 -->
 
94
  const playerStore = usePlayerStore()
95
  const coverUrl = ref('')
96
 
97
+ // 音量控制
98
+ const isMuted = ref(false)
99
+ const lastVolume = ref(80)
100
+
101
  // 触摸处理
102
  const touchStartY = ref(0)
103
  const touchStartX = ref(0)
 
108
  const isPlaying = computed(() => playerStore.isPlaying)
109
  const progress = computed(() => playerStore.progress)
110
  const audioSrc = computed(() => playerStore.audioSrc)
111
+ const volume = computed(() => playerStore.volume || 80)
112
+
113
+ // 音量图标
114
+ const volumeIcon = computed(() => {
115
+ if (isMuted.value || volume.value === 0) {
116
+ return 'fas fa-volume-mute'
117
+ } else if (volume.value < 30) {
118
+ return 'fas fa-volume-off'
119
+ } else if (volume.value < 70) {
120
+ return 'fas fa-volume-down'
121
+ } else {
122
+ return 'fas fa-volume-up'
123
+ }
124
+ })
125
 
126
  // 格式化歌手名
127
  const formatArtist = (artist) => {
 
143
  }
144
  }
145
 
146
+ // 音量控制方法
147
+ const toggleMute = () => {
148
+ if (isMuted.value) {
149
+ // 取消静音
150
+ playerStore.setVolume(lastVolume.value)
151
+ isMuted.value = false
152
+ } else {
153
+ // 静音
154
+ lastVolume.value = volume.value
155
+ playerStore.setVolume(0)
156
+ isMuted.value = true
157
+ }
158
+ }
159
+
160
+ const handleVolumeChange = (event) => {
161
+ const newVolume = parseInt(event.target.value)
162
+ playerStore.setVolume(newVolume)
163
+ isMuted.value = newVolume === 0
164
+ }
165
+
166
  // 加载专辑封面
167
  const loadCover = async () => {
168
  if (!currentSong.value) {
 
331
 
332
  .play-controls {
333
  flex-shrink: 0;
334
+ display: flex;
335
+ align-items: center;
336
+ gap: 8px;
337
  }
338
 
339
  .control-btn {
 
353
  backdrop-filter: blur(10px);
354
  }
355
 
356
+ /* PC端播放控制按钮调整 */
357
+ @media (min-width: 1024px) {
358
+ .mini-player-content {
359
+ padding: 0 20px;
360
+ }
361
+
362
+ .play-controls {
363
+ gap: 4px;
364
+ }
365
+
366
+ .control-btn.prev-btn,
367
+ .control-btn.next-btn {
368
+ width: 32px;
369
+ height: 32px;
370
+ font-size: 12px;
371
+ background: var(--overlay-lighter);
372
+ }
373
+
374
+ .control-btn.play-btn {
375
+ width: 40px;
376
+ height: 40px;
377
+ font-size: 16px;
378
+ }
379
+ }
380
+
381
+ /* PC端音量控制 */
382
+ .volume-control {
383
+ display: flex;
384
+ align-items: center;
385
+ gap: 8px;
386
+ flex-shrink: 0;
387
+ }
388
+
389
+ .volume-btn {
390
+ width: 32px;
391
+ height: 32px;
392
+ border-radius: 50%;
393
+ background: var(--overlay-lighter);
394
+ border: none;
395
+ color: var(--text-secondary);
396
+ cursor: pointer;
397
+ display: flex;
398
+ align-items: center;
399
+ justify-content: center;
400
+ font-size: 12px;
401
+ transition: var(--transition-fast);
402
+ }
403
+
404
+ .volume-btn:hover {
405
+ background: var(--overlay-light);
406
+ color: var(--text-primary);
407
+ }
408
+
409
+ .volume-slider {
410
+ width: 80px;
411
+ height: 4px;
412
+ background: var(--border-light);
413
+ border-radius: 2px;
414
+ outline: none;
415
+ appearance: none;
416
+ cursor: pointer;
417
+ transition: var(--transition-fast);
418
+ }
419
+
420
+ .volume-slider::-webkit-slider-thumb {
421
+ appearance: none;
422
+ width: 12px;
423
+ height: 12px;
424
+ border-radius: 50%;
425
+ background: var(--primary-color);
426
+ cursor: pointer;
427
+ border: 2px solid white;
428
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
429
+ transition: var(--transition-fast);
430
+ }
431
+
432
+ .volume-slider::-webkit-slider-thumb:hover {
433
+ transform: scale(1.2);
434
+ box-shadow: 0 0 8px var(--glow-color);
435
+ }
436
+
437
+ .volume-slider::-moz-range-thumb {
438
+ width: 12px;
439
+ height: 12px;
440
+ border-radius: 50%;
441
+ background: var(--primary-color);
442
+ cursor: pointer;
443
+ border: 2px solid white;
444
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
445
+ transition: var(--transition-fast);
446
+ }
447
+
448
+ /* PC端迷你播放器整体布局调整 */
449
+ @media (min-width: 1024px) {
450
+ .mini-player {
451
+ display: none; /* PC端隐藏迷你播放器,使用侧边栏播放器 */
452
+ }
453
+ }
454
+
455
  /* 播放状态时的视觉反馈 - 立即显示 */
456
  .mini-player .control-btn {
457
  transition: all var(--transition-fast);
src/components/player/DesktopFullPlayer.vue ADDED
@@ -0,0 +1,1057 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <template>
2
+ <div class="desktop-full-player">
3
+ <!-- 悬浮控制按钮 -->
4
+ <button class="floating-back-btn" @click="handleBack">
5
+ <i class="fas fa-arrow-left"></i>
6
+ </button>
7
+
8
+ <!-- 主内容区域:左右分栏 -->
9
+ <main class="player-main">
10
+ <!-- 左侧:音乐播放区域 -->
11
+ <section class="left-panel">
12
+ <!-- 更多操作按钮 -->
13
+ <button class="panel-more-btn" @click="showMoreActions = true" v-if="currentSong">
14
+ <i class="fas fa-ellipsis-h"></i>
15
+ </button>
16
+
17
+ <!-- 专辑封面 -->
18
+ <div class="album-cover-section">
19
+ <div class="album-cover-wrapper">
20
+ <img
21
+ :src="currentCover"
22
+ :alt="currentSong?.name"
23
+ class="album-cover"
24
+ :class="{ rotating: isPlaying }"
25
+ @error="handleImageError"
26
+ />
27
+ <!-- CD中心点 -->
28
+ <div class="cd-center">
29
+ <div class="center-dot"></div>
30
+ </div>
31
+ </div>
32
+ </div>
33
+
34
+ <!-- 歌曲信息 -->
35
+ <div class="song-info-section">
36
+ <div class="song-title-row">
37
+ <h2 class="song-title">{{ currentSong?.name || '暂无播放' }}</h2>
38
+ <button
39
+ class="action-button favorite-btn"
40
+ :class="{ active: isFavorite }"
41
+ @click="toggleFavorite"
42
+ v-if="currentSong"
43
+ >
44
+ <i :class="isFavorite ? 'fas fa-heart' : 'far fa-heart'"></i>
45
+ </button>
46
+ </div>
47
+ <p class="artist-name">{{ formattedArtist }}</p>
48
+ <p class="album-name" v-if="currentSong?.album">{{ currentSong.album }}</p>
49
+ </div>
50
+
51
+ <!-- 播放进度 -->
52
+ <div class="progress-section">
53
+ <ProgressBar @seek="handleSeek"/>
54
+ </div>
55
+
56
+ <!-- 播放控制 -->
57
+ <div class="controls-section">
58
+ <PlayControls
59
+ :loading="loading"
60
+ @togglePlay="handleTogglePlay"
61
+ @previous="handlePrevious"
62
+ @next="handleNext"
63
+ @togglePlayMode="handleTogglePlayMode"
64
+ @showPlaylist="handleShowPlaylist"
65
+ />
66
+ </div>
67
+ </section>
68
+
69
+ <!-- 右侧:实时歌词区域 -->
70
+ <section class="right-panel">
71
+ <div class="lyrics-content">
72
+ <div class="lyrics-scroll-container">
73
+ <LyricsView
74
+ :lyrics="currentLyrics"
75
+ :currentTime="currentTime"
76
+ @seekTo="handleSeekTo"
77
+ />
78
+ </div>
79
+ </div>
80
+ </section>
81
+ </main>
82
+
83
+ <!-- 音质选择面板 -->
84
+ <div class="quality-selector" v-if="showQualitySelector" @click.self="showQualitySelector = false">
85
+ <div class="quality-panel" @click.stop>
86
+ <div class="panel-header">
87
+ <h3>音质选择</h3>
88
+ <button class="close-btn" @click.stop="showQualitySelector = false">
89
+ <i class="fas fa-times"></i>
90
+ </button>
91
+ </div>
92
+
93
+ <div class="quality-list">
94
+ <div
95
+ v-for="quality in availableQualities"
96
+ :key="quality.value"
97
+ class="quality-item"
98
+ :class="{
99
+ 'active': quality.value === currentQuality,
100
+ 'loading': qualityLoading && selectedQuality === quality.value
101
+ }"
102
+ @click.stop="selectQuality(quality.value)"
103
+ >
104
+ <div class="quality-main">
105
+ <div class="quality-name">{{ quality.name }}</div>
106
+ <div class="quality-desc">{{ quality.description }}</div>
107
+ </div>
108
+
109
+ <div class="quality-actions">
110
+ <div class="quality-size" v-if="quality.size">{{ quality.size }}</div>
111
+ <i v-if="quality.value === currentQuality" class="fas fa-check quality-check"></i>
112
+ <i v-else-if="qualityLoading && selectedQuality === quality.value" class="fas fa-spinner fa-spin"></i>
113
+ </div>
114
+ </div>
115
+ </div>
116
+
117
+ <div class="quality-note">
118
+ <i class="fas fa-info-circle"></i>
119
+ <span>更高音质需要更多流量,请在WiFi环境下使用</span>
120
+ </div>
121
+ </div>
122
+ </div>
123
+
124
+ <!-- 播放列表面板 -->
125
+ <PlaylistPanel
126
+ v-if="showPlaylist"
127
+ @close="showPlaylist = false"
128
+ @play="handlePlayFromList"
129
+ @remove="handleRemoveFromList"
130
+ />
131
+
132
+ <!-- 更多操作面板 -->
133
+ <MoreActionsPanel
134
+ v-if="showMoreActions"
135
+ :song="currentSong"
136
+ @close="showMoreActions = false"
137
+ @action="handleMoreAction"
138
+ />
139
+ </div>
140
+ </template>
141
+
142
+ <script setup>
143
+ import {ref, computed, onMounted, onUnmounted, watch} from 'vue'
144
+ import {useRouter} from 'vue-router'
145
+ import {usePlayerStore} from '@/stores/player'
146
+ import {usePlayQueueStore} from '@/stores/playqueue'
147
+ import {useFavoritesStore} from '@/stores/favorites'
148
+ import {useHistoryStore} from '@/stores/history'
149
+ import {useToastStore} from '@/stores/toast'
150
+ import {useSettingsStore} from '@/stores/settings'
151
+ import {musicApi, utils} from '@/services/musicApi'
152
+ import ProgressBar from '@/components/player/ProgressBar.vue'
153
+ import PlayControls from '@/components/player/PlayControls.vue'
154
+ import LyricsView from '@/components/player/LyricsView.vue'
155
+ import PlaylistPanel from '@/components/player/PlaylistPanel.vue'
156
+ import MoreActionsPanel from '@/components/player/MoreActionsPanel.vue'
157
+
158
+ // 定义事件发送器
159
+ const emit = defineEmits(['close'])
160
+
161
+ const router = useRouter()
162
+ const playerStore = usePlayerStore()
163
+ const playQueueStore = usePlayQueueStore()
164
+ const favoritesStore = useFavoritesStore()
165
+ const historyStore = useHistoryStore()
166
+ const toastStore = useToastStore()
167
+ const settingsStore = useSettingsStore()
168
+
169
+ // 响应式数据
170
+ const loading = ref(false)
171
+ const showPlaylist = ref(false)
172
+ const showMoreActions = ref(false)
173
+ const showQualitySelector = ref(false)
174
+ const currentLyrics = ref([])
175
+ const qualityLoading = ref(false)
176
+ const selectedQuality = ref(null)
177
+ const currentCoverUrl = ref('')
178
+
179
+ // 音质配置
180
+ const availableQualities = ref([
181
+ {
182
+ value: 128,
183
+ name: '标准',
184
+ description: '128K 省流模式',
185
+ size: '约3MB/首'
186
+ },
187
+ {
188
+ value: 192,
189
+ name: '较高',
190
+ description: '192K 均衡模式',
191
+ size: '约4MB/首'
192
+ },
193
+ {
194
+ value: 320,
195
+ name: 'HQ',
196
+ description: '320K 高品质',
197
+ size: '约7MB/首'
198
+ },
199
+ {
200
+ value: 740,
201
+ name: '无损',
202
+ description: 'FLAC 原音质',
203
+ size: '约30MB/首'
204
+ },
205
+ {
206
+ value: 999,
207
+ name: 'Hi-Res',
208
+ description: '高解析度音频',
209
+ size: '约60MB/首'
210
+ }
211
+ ])
212
+
213
+ // 计算属性
214
+ const currentSong = computed(() => playerStore.currentSong)
215
+ const isPlaying = computed(() => playerStore.isPlaying)
216
+ const currentTime = computed(() => playerStore.currentTime)
217
+ const currentQuality = computed(() => settingsStore.settings.defaultQuality || 320)
218
+
219
+ const isFavorite = computed(() => {
220
+ return currentSong.value ? favoritesStore.isFavorite(currentSong.value) : false
221
+ })
222
+
223
+ const formattedArtist = computed(() => {
224
+ if (!currentSong.value?.artist) return ''
225
+ return utils.formatArtist(currentSong.value.artist)
226
+ })
227
+
228
+ const currentCover = computed(() => {
229
+ // 如果有设置的封面URL,优先使用
230
+ if (currentCoverUrl.value) {
231
+ return currentCoverUrl.value
232
+ }
233
+
234
+ if (!currentSong.value) return defaultCover.value
235
+
236
+ // 优先使用缓存的封面
237
+ const cachedCover = playerStore.getCachedCover(currentSong.value, 300)
238
+ if (cachedCover) {
239
+ return cachedCover
240
+ }
241
+
242
+ return defaultCover.value
243
+ })
244
+
245
+ const defaultCover = computed(() => {
246
+ return 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjgwIiBoZWlnaHQ9IjI4MCIgdmlld0JveD0iMCAwIDI4MCAyODAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIyODAiIGhlaWdodD0iMjgwIiBmaWxsPSJyZ2JhKDIwMCwyMDAsMjAwLDAuMykiIHJ4PSI0MCIvPgo8Y2lyY2xlIGN4PSIxNDAiIGN5PSIxNDAiIHI9IjgwIiBmaWxsPSJyZ2JhKDI1NSwyNTUsMjU1LDAuMSkiLz4KPHN2ZyB4PSIxMTAiIHk9IjExMCIgd2lkdGg9IjYwIiBoZWlnaHQ9IjYwIiB2aWV3Qm94PSIwIDAgMjQgMjQiIGZpbGw9InJnYmEoMTYwLDE2MCwxNjAsMC42KSI+CjxwYXRoIGQ9Ik0xMiAzdjEwLjU1Yy0uNTktLjM0LTEuMjctLjU1LTItLjU1QzcuNzkgMTMgNiAxNC43OSA2IDE3czEuNzkgNCA0IDRDMS45NCAwIDMuNS0xLjI5IDMuOTEtM0gxNFYzeiIvPgo8L3N2Zz4KPC9zdmc+'
247
+ })
248
+
249
+ // 异步加载专辑封面
250
+ const loadAlbumCover = async () => {
251
+ if (!currentSong.value) {
252
+ currentCoverUrl.value = defaultCover.value
253
+ return
254
+ }
255
+
256
+ try {
257
+ // 先使用缓存的封面
258
+ const cachedCover = playerStore.getCachedCover(currentSong.value, 300)
259
+ if (cachedCover) {
260
+ currentCoverUrl.value = cachedCover
261
+ return
262
+ }
263
+
264
+ const coverUrlResult = await playerStore.getAlbumCover(currentSong.value, 300)
265
+ if (coverUrlResult) {
266
+ currentCoverUrl.value = coverUrlResult
267
+ } else {
268
+ currentCoverUrl.value = defaultCover.value
269
+ }
270
+ } catch (error) {
271
+ console.error('加载专辑封面失败:', error)
272
+ currentCoverUrl.value = defaultCover.value
273
+ }
274
+ }
275
+
276
+ // 图片加载错误处理
277
+ const handleImageError = () => {
278
+ currentCoverUrl.value = defaultCover.value
279
+ }
280
+
281
+ // 方法
282
+ const handleBack = () => {
283
+ emit('close')
284
+ }
285
+
286
+ const handleSeek = (time) => {
287
+ playerStore.seekTo(time)
288
+ }
289
+
290
+ const handleSeekTo = (time) => {
291
+ playerStore.seekTo(time)
292
+ }
293
+
294
+ const handleTogglePlay = async () => {
295
+ const audioElement = playerStore.audioElement || document.querySelector('audio')
296
+
297
+ if (!audioElement || !currentSong.value) {
298
+ return
299
+ }
300
+
301
+ try {
302
+ if (isPlaying.value) {
303
+ audioElement.pause()
304
+ playerStore.setPlayingState(false)
305
+ } else {
306
+ if (!playerStore.audioSrc || !audioElement.src || audioElement.src === location.href) {
307
+ window.dispatchEvent(new CustomEvent('loadAndPlaySong', {
308
+ detail: {song: currentSong.value}
309
+ }))
310
+ return
311
+ }
312
+
313
+ if (audioElement.src !== playerStore.audioSrc) {
314
+ audioElement.src = playerStore.audioSrc
315
+ audioElement.load()
316
+ }
317
+
318
+ await audioElement.play()
319
+ playerStore.setPlayingState(true)
320
+ }
321
+ } catch (error) {
322
+ console.error('播放失败:', error)
323
+ }
324
+ }
325
+
326
+ const handlePrevious = () => {
327
+ const result = playQueueStore.playPrevious()
328
+ if (result) {
329
+ playerStore.playSong(result)
330
+ }
331
+ }
332
+
333
+ const handleNext = () => {
334
+ const result = playQueueStore.playNext()
335
+ if (result) {
336
+ playerStore.playSong(result)
337
+ }
338
+ }
339
+
340
+ const handleTogglePlayMode = () => {
341
+ playQueueStore.togglePlayMode()
342
+ }
343
+
344
+ const handleShowPlaylist = () => {
345
+ showPlaylist.value = true
346
+ }
347
+
348
+ const handlePlayFromList = async (song, index) => {
349
+ try {
350
+ const selectedSong = playQueueStore.playAtIndex(index)
351
+ if (selectedSong) {
352
+ await playerStore.playSong(selectedSong, true)
353
+ historyStore.addToHistory(selectedSong)
354
+ toastStore.success(`开始播放 "${selectedSong.name}"`)
355
+ }
356
+ showPlaylist.value = false
357
+ } catch (error) {
358
+ console.error('播放失败:', error)
359
+ toastStore.error('播放失败,请重试')
360
+ }
361
+ }
362
+
363
+ const handleRemoveFromList = (index) => {
364
+ try {
365
+ const result = playQueueStore.removeFromQueue(index)
366
+ if (result.success) {
367
+ toastStore.success('已从播放列表移除')
368
+ } else {
369
+ toastStore.error(result.message)
370
+ }
371
+ } catch (error) {
372
+ console.error('移除失败:', error)
373
+ toastStore.error('操作失败,请重试')
374
+ }
375
+ }
376
+
377
+ const toggleFavorite = async () => {
378
+ if (!currentSong.value) return
379
+
380
+ try {
381
+ const result = await favoritesStore.toggleFavorite(currentSong.value)
382
+ const message = result ? '已添加到我喜欢的音乐' : '已从我喜欢的音乐中移除'
383
+ toastStore.success(message)
384
+ } catch (error) {
385
+ console.error('收藏操作失败:', error)
386
+ toastStore.error('操作失败,请重试')
387
+ }
388
+ }
389
+
390
+
391
+ const handleMoreAction = (action) => {
392
+ switch (action) {
393
+ case 'addToPlaylist':
394
+ // 实现添加到播放列表
395
+ break
396
+ case 'viewAlbum':
397
+ // 实现查看专辑
398
+ break
399
+ case 'viewArtist':
400
+ // 实现查看歌手
401
+ break
402
+ case 'selectQuality':
403
+ showQualitySelector.value = true
404
+ break
405
+ case 'report':
406
+ // 实现举报
407
+ break
408
+ }
409
+ showMoreActions.value = false
410
+ }
411
+
412
+ const selectQuality = async (quality) => {
413
+ if (quality === currentQuality.value || qualityLoading.value) return
414
+
415
+ selectedQuality.value = quality
416
+ qualityLoading.value = true
417
+
418
+ try {
419
+ await settingsStore.updateSetting('defaultQuality', quality)
420
+
421
+ if (currentSong.value && isPlaying.value) {
422
+ const currentTime = playerStore.currentTime
423
+ await playerStore.changeQuality(quality)
424
+
425
+ if (currentTime > 0) {
426
+ setTimeout(() => {
427
+ playerStore.seekTo(currentTime)
428
+ }, 500)
429
+ }
430
+ }
431
+
432
+ showQualitySelector.value = false
433
+
434
+ } catch (error) {
435
+ console.error('切换音质失败:', error)
436
+ } finally {
437
+ qualityLoading.value = false
438
+ selectedQuality.value = null
439
+ }
440
+ }
441
+
442
+
443
+ // 获取当前时间对应的歌词
444
+ const getCurrentLyric = () => {
445
+ if (!currentLyrics.value || currentLyrics.value.length === 0) {
446
+ return ''
447
+ }
448
+
449
+ const currentTimeSeconds = currentTime.value
450
+ let currentLyric = ''
451
+
452
+ for (let i = 0; i < currentLyrics.value.length; i++) {
453
+ const lyric = currentLyrics.value[i]
454
+ if (lyric.time <= currentTimeSeconds) {
455
+ currentLyric = lyric.text
456
+ } else {
457
+ break
458
+ }
459
+ }
460
+
461
+ return currentLyric
462
+ }
463
+
464
+ // 加载歌词
465
+ const loadLyrics = async (song) => {
466
+ if (!song) {
467
+ currentLyrics.value = []
468
+ return
469
+ }
470
+
471
+ const cachedLyrics = playerStore.getCachedLyrics(song)
472
+ if (cachedLyrics) {
473
+ const parsedLyrics = musicApi.parseLyrics(cachedLyrics.lyric)
474
+ currentLyrics.value = parsedLyrics.lyrics
475
+ return
476
+ }
477
+
478
+ try {
479
+ loading.value = true
480
+ const lyricsData = await playerStore.getLyricsWithDedup(song)
481
+
482
+ const parsedLyrics = musicApi.parseLyrics(lyricsData.lyric)
483
+ currentLyrics.value = parsedLyrics.lyrics
484
+ } catch (error) {
485
+ console.error('加载歌词失败:', error)
486
+ currentLyrics.value = []
487
+ } finally {
488
+ loading.value = false
489
+ }
490
+ }
491
+
492
+ // 延时加载歌词的定时器
493
+ let lyricsTimer = null
494
+
495
+ // 监听歌曲变化
496
+ watch(currentSong, (newSong, oldSong) => {
497
+ if (newSong && newSong !== oldSong) {
498
+ if (lyricsTimer) {
499
+ clearTimeout(lyricsTimer)
500
+ lyricsTimer = null
501
+ }
502
+
503
+ loadAlbumCover()
504
+
505
+ lyricsTimer = setTimeout(() => {
506
+ loadLyrics(newSong)
507
+ lyricsTimer = null
508
+ }, 200)
509
+ }
510
+ }, {immediate: true})
511
+
512
+ // 生命周期
513
+ onMounted(() => {
514
+ if (currentSong.value) {
515
+ loadAlbumCover()
516
+ loadLyrics(currentSong.value)
517
+ }
518
+ })
519
+
520
+ onUnmounted(() => {
521
+ if (lyricsTimer) {
522
+ clearTimeout(lyricsTimer)
523
+ lyricsTimer = null
524
+ }
525
+ })
526
+ </script>
527
+
528
+ <style scoped>
529
+ .desktop-full-player {
530
+ position: fixed;
531
+ top: 0;
532
+ left: 280px; /* 避开侧边栏 */
533
+ right: 0;
534
+ bottom: 0;
535
+ z-index: 1000;
536
+ background: var(--bg-primary);
537
+ display: flex;
538
+ flex-direction: column;
539
+ overflow: hidden;
540
+ }
541
+
542
+ /* 悬浮控制按钮 */
543
+ .floating-back-btn {
544
+ position: absolute;
545
+ top: 20px;
546
+ left: 20px;
547
+ width: 44px;
548
+ height: 44px;
549
+ border: none;
550
+ background: rgba(255, 255, 255, 0.15);
551
+ color: var(--text-primary);
552
+ border-radius: 50%;
553
+ display: flex;
554
+ align-items: center;
555
+ justify-content: center;
556
+ cursor: pointer;
557
+ backdrop-filter: blur(20px);
558
+ transition: var(--transition-fast);
559
+ z-index: 20;
560
+ font-size: 16px;
561
+ border: 1px solid rgba(255, 255, 255, 0.1);
562
+ }
563
+
564
+ .floating-back-btn:hover {
565
+ background: rgba(255, 255, 255, 0.25);
566
+ transform: scale(1.05);
567
+ border-color: rgba(255, 255, 255, 0.2);
568
+ }
569
+
570
+ /* 主内容区域:左右分栏 */
571
+ .player-main {
572
+ display: grid;
573
+ grid-template-columns: 400px 1fr;
574
+ overflow: hidden;
575
+ height: 100%;
576
+ flex: 1;
577
+ }
578
+
579
+ /* 左侧音乐播放区域 */
580
+ .left-panel {
581
+ display: flex;
582
+ flex-direction: column;
583
+ padding: 20px;
584
+ background: var(--bg-card);
585
+ border-right: 1px solid var(--border-light);
586
+ overflow: hidden;
587
+ height: 100%;
588
+ position: relative;
589
+ }
590
+
591
+ .panel-more-btn {
592
+ position: absolute;
593
+ top: 16px;
594
+ right: 16px;
595
+ width: 32px;
596
+ height: 32px;
597
+ border: none;
598
+ background: rgba(255, 255, 255, 0.1);
599
+ color: var(--text-secondary);
600
+ border-radius: 50%;
601
+ display: flex;
602
+ align-items: center;
603
+ justify-content: center;
604
+ cursor: pointer;
605
+ transition: var(--transition-fast);
606
+ z-index: 10;
607
+ font-size: 14px;
608
+ }
609
+
610
+ .panel-more-btn:hover {
611
+ background: rgba(255, 255, 255, 0.2);
612
+ color: var(--text-primary);
613
+ transform: scale(1.05);
614
+ }
615
+
616
+ .album-cover-section {
617
+ flex: 1;
618
+ display: flex;
619
+ align-items: center;
620
+ justify-content: center;
621
+ text-align: center;
622
+ }
623
+
624
+ .album-cover-wrapper {
625
+ position: relative;
626
+ display: inline-block;
627
+ width: 180px;
628
+ height: 180px;
629
+ border-radius: 50%;
630
+ overflow: hidden;
631
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
632
+ background: var(--overlay-light);
633
+ border: 3px solid var(--border-light);
634
+ }
635
+
636
+ .album-cover {
637
+ width: 100%;
638
+ height: 100%;
639
+ object-fit: cover;
640
+ border-radius: 50%;
641
+ transition: var(--transition-slow);
642
+ }
643
+
644
+ .album-cover.rotating {
645
+ animation: cd-rotate 10s linear infinite;
646
+ }
647
+
648
+ @keyframes cd-rotate {
649
+ from { transform: rotate(0deg); }
650
+ to { transform: rotate(360deg); }
651
+ }
652
+
653
+ .album-cover:hover {
654
+ transform: scale(1.02);
655
+ }
656
+
657
+ .cd-center {
658
+ position: absolute;
659
+ top: 50%;
660
+ left: 50%;
661
+ transform: translate(-50%, -50%);
662
+ width: 36px;
663
+ height: 36px;
664
+ border-radius: 50%;
665
+ background: rgba(0, 0, 0, 0.7);
666
+ backdrop-filter: blur(8px);
667
+ display: flex;
668
+ align-items: center;
669
+ justify-content: center;
670
+ z-index: 2;
671
+ border: 2px solid var(--border-light);
672
+ }
673
+
674
+ .center-dot {
675
+ width: 12px;
676
+ height: 12px;
677
+ border-radius: 50%;
678
+ background: rgba(255, 255, 255, 0.9);
679
+ }
680
+
681
+ .song-info-section {
682
+ text-align: center;
683
+ flex-shrink: 0;
684
+ max-width: 100%;
685
+ margin-bottom: 16px;
686
+ }
687
+
688
+ .song-title-row {
689
+ display: flex;
690
+ align-items: center;
691
+ justify-content: center;
692
+ gap: 8px;
693
+ margin-bottom: 4px;
694
+ position: relative;
695
+ }
696
+
697
+ .song-title {
698
+ font-size: 16px;
699
+ font-weight: 600;
700
+ color: var(--text-primary);
701
+ margin: 0;
702
+ line-height: 1.3;
703
+ word-wrap: break-word;
704
+ overflow: hidden;
705
+ text-overflow: ellipsis;
706
+ white-space: nowrap;
707
+ text-align: center;
708
+ flex: 1;
709
+ }
710
+
711
+ .song-title-row .action-button {
712
+ position: absolute;
713
+ right: -8px;
714
+ flex-shrink: 0;
715
+ }
716
+
717
+ .artist-name {
718
+ font-size: 13px;
719
+ color: var(--text-secondary);
720
+ margin: 0 0 3px;
721
+ font-weight: 500;
722
+ overflow: hidden;
723
+ text-overflow: ellipsis;
724
+ white-space: nowrap;
725
+ }
726
+
727
+ .album-name {
728
+ font-size: 11px;
729
+ color: var(--text-tertiary);
730
+ margin: 0 0 12px;
731
+ overflow: hidden;
732
+ text-overflow: ellipsis;
733
+ white-space: nowrap;
734
+ }
735
+
736
+
737
+ .action-button {
738
+ width: 32px;
739
+ height: 32px;
740
+ border: none;
741
+ background: rgba(255, 255, 255, 0.1);
742
+ color: var(--text-primary);
743
+ border-radius: 50%;
744
+ font-size: 13px;
745
+ cursor: pointer;
746
+ transition: var(--transition-fast);
747
+ display: flex;
748
+ align-items: center;
749
+ justify-content: center;
750
+ }
751
+
752
+ .action-button:hover {
753
+ background: rgba(255, 255, 255, 0.2);
754
+ transform: scale(1.05);
755
+ }
756
+
757
+ .favorite-btn.active {
758
+ background: rgba(255, 107, 107, 0.2);
759
+ color: var(--accent-red);
760
+ }
761
+
762
+ .favorite-btn.active:hover {
763
+ background: rgba(255, 107, 107, 0.3);
764
+ }
765
+
766
+ .progress-section {
767
+ flex-shrink: 0;
768
+ width: 100%;
769
+ margin-bottom: 6px;
770
+ }
771
+
772
+ .controls-section {
773
+ flex-shrink: 0;
774
+ }
775
+
776
+
777
+ /* 右侧歌词区域 */
778
+ .right-panel {
779
+ display: flex;
780
+ flex-direction: column;
781
+ background: var(--bg-primary);
782
+ overflow: hidden;
783
+ }
784
+
785
+
786
+ .lyrics-content {
787
+ flex: 1;
788
+ overflow: hidden;
789
+ padding: 0;
790
+ background: var(--bg-primary);
791
+ border: 1px solid var(--border-light);
792
+ }
793
+
794
+ .lyrics-scroll-container {
795
+ height: 100%;
796
+ overflow-y: auto;
797
+ padding: 20px;
798
+ background: var(--bg-card);
799
+ border-radius: 12px;
800
+ position: relative;
801
+ }
802
+
803
+ .lyrics-scroll-container::-webkit-scrollbar {
804
+ width: 6px;
805
+ }
806
+
807
+ .lyrics-scroll-container::-webkit-scrollbar-track {
808
+ background: transparent;
809
+ border-radius: 3px;
810
+ }
811
+
812
+ .lyrics-scroll-container::-webkit-scrollbar-thumb {
813
+ background: rgba(255, 107, 107, 0.3);
814
+ border-radius: 3px;
815
+ transition: var(--transition-fast);
816
+ }
817
+
818
+ .lyrics-scroll-container::-webkit-scrollbar-thumb:hover {
819
+ background: rgba(255, 107, 107, 0.5);
820
+ }
821
+
822
+ /* 深度选择器优化歌词组件内部样式 */
823
+ .lyrics-scroll-container :deep(.lyrics-view) {
824
+ padding: 0;
825
+ height: 100%;
826
+ display: flex;
827
+ flex-direction: column;
828
+ }
829
+
830
+ .lyrics-scroll-container :deep(.lyrics-list) {
831
+ flex: 1;
832
+ display: flex;
833
+ flex-direction: column;
834
+ justify-content: flex-start;
835
+ min-height: 100%;
836
+ gap: 8px;
837
+ }
838
+
839
+ .lyrics-scroll-container :deep(.lyric-line) {
840
+ padding: 12px 16px;
841
+ font-size: 16px;
842
+ line-height: 1.8;
843
+ color: var(--text-secondary);
844
+ text-align: center;
845
+ transition: all 0.3s ease;
846
+ cursor: pointer;
847
+ border-radius: 8px;
848
+ margin: 0;
849
+ word-wrap: break-word;
850
+ white-space: normal;
851
+ }
852
+
853
+ .lyrics-scroll-container :deep(.lyric-line:hover) {
854
+ color: var(--text-primary);
855
+ background: rgba(255, 107, 107, 0.05);
856
+ }
857
+
858
+ .lyrics-scroll-container :deep(.lyric-line.active) {
859
+ color: var(--accent-red);
860
+ font-weight: 600;
861
+ font-size: 18px;
862
+ background: rgba(255, 107, 107, 0.1);
863
+ text-shadow: none;
864
+ box-shadow: 0 2px 8px rgba(255, 107, 107, 0.2);
865
+ transform: scale(1.02);
866
+ }
867
+
868
+ .lyrics-scroll-container :deep(.lyric-line.next) {
869
+ color: var(--text-primary);
870
+ font-weight: 500;
871
+ background: rgba(255, 255, 255, 0.02);
872
+ }
873
+
874
+ .lyrics-scroll-container :deep(.empty-lyrics) {
875
+ display: flex;
876
+ align-items: center;
877
+ justify-content: center;
878
+ height: 100%;
879
+ color: var(--text-tertiary);
880
+ font-size: 16px;
881
+ font-style: italic;
882
+ }
883
+
884
+ /* 歌词淡入淡出效果 */
885
+ .lyrics-scroll-container :deep(.lyric-line) {
886
+ opacity: 0.7;
887
+ }
888
+
889
+ .lyrics-scroll-container :deep(.lyric-line.active),
890
+ .lyrics-scroll-container :deep(.lyric-line.next) {
891
+ opacity: 1;
892
+ }
893
+
894
+ /* 音质选择面板样式复用原有样式 */
895
+ .quality-selector {
896
+ position: fixed;
897
+ top: 0;
898
+ left: 0;
899
+ right: 0;
900
+ bottom: 0;
901
+ background: rgba(0, 0, 0, 0.8);
902
+ backdrop-filter: blur(20px);
903
+ z-index: 2000;
904
+ display: flex;
905
+ align-items: center;
906
+ justify-content: center;
907
+ animation: fadeIn 0.3s ease-out;
908
+ }
909
+
910
+ .quality-panel {
911
+ background: var(--bg-card);
912
+ border-radius: 16px;
913
+ padding: 0;
914
+ width: 100%;
915
+ max-width: 480px;
916
+ max-height: 70vh;
917
+ overflow-y: auto;
918
+ animation: slideUpPanel 0.3s ease-out;
919
+ }
920
+
921
+ .panel-header {
922
+ display: flex;
923
+ align-items: center;
924
+ justify-content: space-between;
925
+ padding: 24px;
926
+ border-bottom: 1px solid var(--border-light);
927
+ }
928
+
929
+ .panel-header h3 {
930
+ font-size: 18px;
931
+ font-weight: 600;
932
+ color: var(--text-primary);
933
+ margin: 0;
934
+ }
935
+
936
+ .close-btn {
937
+ width: 32px;
938
+ height: 32px;
939
+ border: none;
940
+ background: rgba(255, 255, 255, 0.1);
941
+ color: var(--text-secondary);
942
+ border-radius: 50%;
943
+ display: flex;
944
+ align-items: center;
945
+ justify-content: center;
946
+ cursor: pointer;
947
+ transition: var(--transition-fast);
948
+ }
949
+
950
+ .close-btn:hover {
951
+ background: rgba(255, 255, 255, 0.2);
952
+ color: var(--text-primary);
953
+ }
954
+
955
+ .quality-list {
956
+ padding: 12px 0;
957
+ }
958
+
959
+ .quality-item {
960
+ display: flex;
961
+ align-items: center;
962
+ justify-content: space-between;
963
+ padding: 16px 24px;
964
+ cursor: pointer;
965
+ transition: var(--transition-fast);
966
+ }
967
+
968
+ .quality-item:hover {
969
+ background: var(--overlay-lighter);
970
+ }
971
+
972
+ .quality-item.active {
973
+ background: var(--bg-gradient-3);
974
+ }
975
+
976
+ .quality-item.loading {
977
+ pointer-events: none;
978
+ }
979
+
980
+ .quality-main {
981
+ flex: 1;
982
+ }
983
+
984
+ .quality-name {
985
+ font-size: 16px;
986
+ font-weight: 600;
987
+ color: var(--text-primary);
988
+ margin-bottom: 2px;
989
+ }
990
+
991
+ .quality-item.active .quality-name {
992
+ color: var(--accent-red);
993
+ }
994
+
995
+ .quality-desc {
996
+ font-size: 13px;
997
+ color: var(--text-secondary);
998
+ }
999
+
1000
+ .quality-actions {
1001
+ display: flex;
1002
+ align-items: center;
1003
+ gap: 12px;
1004
+ }
1005
+
1006
+ .quality-size {
1007
+ font-size: 11px;
1008
+ color: var(--text-tertiary);
1009
+ min-width: 60px;
1010
+ text-align: right;
1011
+ }
1012
+
1013
+ .quality-check {
1014
+ color: var(--accent-red);
1015
+ font-size: 14px;
1016
+ }
1017
+
1018
+ .quality-note {
1019
+ display: flex;
1020
+ align-items: center;
1021
+ gap: 8px;
1022
+ padding: 16px 24px;
1023
+ background: rgba(255, 255, 255, 0.02);
1024
+ border-top: 1px solid var(--border-lighter);
1025
+ }
1026
+
1027
+ .quality-note i {
1028
+ color: var(--text-tertiary);
1029
+ font-size: 12px;
1030
+ }
1031
+
1032
+ .quality-note span {
1033
+ font-size: 12px;
1034
+ color: var(--text-tertiary);
1035
+ line-height: 1.4;
1036
+ }
1037
+
1038
+ @keyframes fadeIn {
1039
+ from {
1040
+ opacity: 0;
1041
+ }
1042
+ to {
1043
+ opacity: 1;
1044
+ }
1045
+ }
1046
+
1047
+ @keyframes slideUpPanel {
1048
+ from {
1049
+ transform: translateY(100%);
1050
+ opacity: 0;
1051
+ }
1052
+ to {
1053
+ transform: translateY(0);
1054
+ opacity: 1;
1055
+ }
1056
+ }
1057
+ </style>
src/components/player/LyricsView.vue CHANGED
@@ -26,11 +26,6 @@
26
  <!-- 底部占位 -->
27
  <div class="lyrics-spacer"></div>
28
  </div>
29
-
30
- <!-- PC端歌词提示 -->
31
- <div class="desktop-lyrics-hint" v-if="isDesktop">
32
- <p>歌词已完整显示</p>
33
- </div>
34
  </div>
35
  </template>
36
 
@@ -113,9 +108,6 @@ const findCurrentLine = (time) => {
113
  const scrollToLine = (index, smooth = true) => {
114
  if (!lyricsContainer.value || index < 0 || !lineRefs.value[index]) return
115
 
116
- // 桌面端不滚动,显示完整歌词
117
- if (isDesktop.value) return
118
-
119
  const container = lyricsContainer.value
120
  const lineElement = lineRefs.value[index]
121
 
@@ -250,23 +242,6 @@ onUnmounted(() => {
250
  }
251
  })
252
 
253
- // 监听当前时间变化
254
- watch(() => props.currentTime, (newTime) => {
255
- if (!parsedLyrics.value.length) return
256
-
257
- const newIndex = findCurrentLine(newTime)
258
- if (newIndex !== currentLineIndex.value) {
259
- currentLineIndex.value = newIndex
260
-
261
- // 只有在自动滚动开启且歌词行有效时才滚动
262
- if (autoScroll.value && newIndex >= 0) {
263
- nextTick(() => {
264
- scrollToLine(newIndex)
265
- })
266
- }
267
- }
268
- }, { immediate: false })
269
-
270
  // 监听歌词变化
271
  watch(() => props.lyrics, (newLyrics) => {
272
  // 重置滚动状态
@@ -279,12 +254,37 @@ watch(() => props.lyrics, (newLyrics) => {
279
  if (props.currentTime > 0 && newLyrics && newLyrics.length > 0) {
280
  const index = findCurrentLine(props.currentTime)
281
  currentLineIndex.value = index
282
- if (index >= 0 && autoScroll.value) {
 
283
  scrollToLine(index, false)
284
  }
285
  }
286
  })
287
  }, { immediate: true })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
288
  </script>
289
 
290
  <style scoped>
@@ -364,25 +364,6 @@ watch(() => props.lyrics, (newLyrics) => {
364
  height: 200px;
365
  }
366
 
367
- .desktop-lyrics-hint {
368
- position: absolute;
369
- top: 20px;
370
- left: 50%;
371
- transform: translateX(-50%);
372
- background: rgba(0, 0, 0, 0.6);
373
- color: rgba(255, 255, 255, 0.8);
374
- padding: 6px 12px;
375
- border-radius: 12px;
376
- font-size: 12px;
377
- backdrop-filter: blur(4px);
378
- display: none;
379
- }
380
-
381
- @media (min-width: 768px) {
382
- .desktop-lyrics-hint {
383
- display: block;
384
- }
385
- }
386
 
387
  /* 响应式 */
388
  @media (max-width: 375px) {
@@ -422,11 +403,6 @@ watch(() => props.lyrics, (newLyrics) => {
422
  padding: 16px 0;
423
  font-size: 1.1em;
424
  }
425
-
426
- /* PC端显示所有歌词 */
427
- .lyrics-content {
428
- transform: translateY(0) !important;
429
- }
430
  }
431
 
432
  /* 平滑滚动动画 */
 
26
  <!-- 底部占位 -->
27
  <div class="lyrics-spacer"></div>
28
  </div>
 
 
 
 
 
29
  </div>
30
  </template>
31
 
 
108
  const scrollToLine = (index, smooth = true) => {
109
  if (!lyricsContainer.value || index < 0 || !lineRefs.value[index]) return
110
 
 
 
 
111
  const container = lyricsContainer.value
112
  const lineElement = lineRefs.value[index]
113
 
 
242
  }
243
  })
244
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
245
  // 监听歌词变化
246
  watch(() => props.lyrics, (newLyrics) => {
247
  // 重置滚动状态
 
254
  if (props.currentTime > 0 && newLyrics && newLyrics.length > 0) {
255
  const index = findCurrentLine(props.currentTime)
256
  currentLineIndex.value = index
257
+ if (index >= 0) {
258
+ // 强制滚动到当前位置,无论是否自动滚动
259
  scrollToLine(index, false)
260
  }
261
  }
262
  })
263
  }, { immediate: true })
264
+
265
+ // 添加对初始时间的监听,确保刷新页面时能正确定位
266
+ watch(() => props.currentTime, (newTime, oldTime) => {
267
+ if (!parsedLyrics.value.length) return
268
+
269
+ // 如果是第一次设置时间(从0变化到有效时间)或者时间有显著跳跃(进度条拖拽)
270
+ const isInitialTime = oldTime === 0 && newTime > 0
271
+ const isSeek = Math.abs(newTime - oldTime) > 2 // 时间跳跃超过2秒认为是拖拽
272
+
273
+ const newIndex = findCurrentLine(newTime)
274
+ if (newIndex !== currentLineIndex.value) {
275
+ currentLineIndex.value = newIndex
276
+
277
+ // 在初始化或拖拽时强制滚动
278
+ if ((isInitialTime || isSeek) && newIndex >= 0) {
279
+ scrollToLine(newIndex, !isSeek) // 拖拽时不使用动画
280
+ } else if (autoScroll.value && newIndex >= 0) {
281
+ // 正常播放时的滚动
282
+ nextTick(() => {
283
+ scrollToLine(newIndex)
284
+ })
285
+ }
286
+ }
287
+ }, { immediate: false })
288
  </script>
289
 
290
  <style scoped>
 
364
  height: 200px;
365
  }
366
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
367
 
368
  /* 响应式 */
369
  @media (max-width: 375px) {
 
403
  padding: 16px 0;
404
  font-size: 1.1em;
405
  }
 
 
 
 
 
406
  }
407
 
408
  /* 平滑滚动动画 */
src/components/player/PlaylistPanel.vue CHANGED
@@ -45,7 +45,6 @@
45
  :key="`${song.id}-${index}`"
46
  :song="song"
47
  :index="index"
48
- :cover="song.cover"
49
  :show-remove="true"
50
  :show-actions="false"
51
  @play="handleSongClick"
 
45
  :key="`${song.id}-${index}`"
46
  :song="song"
47
  :index="index"
 
48
  :show-remove="true"
49
  :show-actions="false"
50
  @play="handleSongClick"
src/components/player/ProgressBar.vue CHANGED
@@ -174,7 +174,7 @@ onUnmounted(() => {
174
  .progress-bar-container {
175
  width: 100%;
176
  padding: 0 4px;
177
- margin-top: 45px;
178
  }
179
 
180
  .progress-bar {
 
174
  .progress-bar-container {
175
  width: 100%;
176
  padding: 0 4px;
177
+ margin-top: 16px;
178
  }
179
 
180
  .progress-bar {
src/components/search/SearchBox.vue CHANGED
@@ -78,6 +78,10 @@ const clearSearch = () => {
78
  searchKeyword.value = ''
79
  }
80
 
 
 
 
 
81
  const showSearchHistory = () => {
82
  // 跳转到搜索历史页面
83
  router.push('/search-history')
@@ -94,6 +98,11 @@ watch(() => searchStore.keyword, (newKeyword) => {
94
  searchKeyword.value = newKeyword
95
  }
96
  })
 
 
 
 
 
97
  </script>
98
 
99
  <style scoped>
 
78
  searchKeyword.value = ''
79
  }
80
 
81
+ const clearKeyword = () => {
82
+ searchKeyword.value = ''
83
+ }
84
+
85
  const showSearchHistory = () => {
86
  // 跳转到搜索历史页面
87
  router.push('/search-history')
 
98
  searchKeyword.value = newKeyword
99
  }
100
  })
101
+
102
+ // 暴露方法给父组件
103
+ defineExpose({
104
+ clearKeyword
105
+ })
106
  </script>
107
 
108
  <style scoped>
src/components/search/SearchResults.vue CHANGED
@@ -33,6 +33,10 @@
33
  <div v-else-if="results.length" class="results-list">
34
  <div class="results-header">
35
  <span class="results-count">找到 {{ results.length }} 首歌曲</span>
 
 
 
 
36
  </div>
37
 
38
  <div class="results-list-container" ref="resultsContainer">
@@ -104,7 +108,7 @@ const props = defineProps({
104
  }
105
  })
106
 
107
- const emit = defineEmits(['retry', 'search', 'play', 'loadMore', 'showMoreActions'])
108
 
109
  const searchStore = useSearchStore()
110
  const playerStore = usePlayerStore()
@@ -130,6 +134,14 @@ const hasMore = computed(() => searchStore.hasMore)
130
  const loadingMore = computed(() => loading.value && results.value.length > 0)
131
 
132
  // 方法
 
 
 
 
 
 
 
 
133
  const handlePlay = async (song, index) => {
134
  try {
135
  // SOLID原则:使用playQueueStore管理播放列表
@@ -249,9 +261,11 @@ onUnmounted(() => {
249
  <style scoped>
250
  .search-results {
251
  flex: 1;
252
- overflow-y: auto;
253
  padding-bottom: 20px;
254
- margin-top: 10px;
 
 
255
  }
256
 
257
  /* 状态样式 */
@@ -382,10 +396,17 @@ onUnmounted(() => {
382
 
383
  /* 结果列表 */
384
  .results-list {
385
- padding: 0 0 20px;
 
 
 
 
386
  }
387
 
388
  .results-header {
 
 
 
389
  padding: 16px 20px 8px;
390
  border-bottom: 1px solid var(--border-strong);
391
  margin-bottom: 8px;
@@ -399,9 +420,49 @@ onUnmounted(() => {
399
  font-weight: 500;
400
  }
401
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
402
  .results-list-container {
403
- max-height: 70vh;
404
  overflow-y: auto;
 
405
  }
406
 
407
  /* 加载更多 */
@@ -452,4 +513,105 @@ onUnmounted(() => {
452
  padding: 12px 16px 8px;
453
  }
454
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
455
  </style>
 
33
  <div v-else-if="results.length" class="results-list">
34
  <div class="results-header">
35
  <span class="results-count">找到 {{ results.length }} 首歌曲</span>
36
+ <button class="clear-results-btn" @click="handleClearResults" title="清除搜索结果">
37
+ <i class="fas fa-times"></i>
38
+ <span class="clear-text">清除</span>
39
+ </button>
40
  </div>
41
 
42
  <div class="results-list-container" ref="resultsContainer">
 
108
  }
109
  })
110
 
111
+ const emit = defineEmits(['retry', 'search', 'play', 'loadMore', 'showMoreActions', 'clearResults'])
112
 
113
  const searchStore = useSearchStore()
114
  const playerStore = usePlayerStore()
 
134
  const loadingMore = computed(() => loading.value && results.value.length > 0)
135
 
136
  // 方法
137
+ const handleClearResults = () => {
138
+ // 清除搜索结果
139
+ searchStore.clearResults()
140
+ // 发出清除事件给父组件
141
+ emit('clearResults')
142
+ toastStore.success('已清除搜索结果')
143
+ }
144
+
145
  const handlePlay = async (song, index) => {
146
  try {
147
  // SOLID原则:使用playQueueStore管理播放列表
 
261
  <style scoped>
262
  .search-results {
263
  flex: 1;
264
+ overflow: hidden; /* 改为hidden,防止出现滚动条 */
265
  padding-bottom: 20px;
266
+ margin-top: 0; /* 移除顶部边距 */
267
+ display: flex;
268
+ flex-direction: column;
269
  }
270
 
271
  /* 状态样式 */
 
396
 
397
  /* 结果列表 */
398
  .results-list {
399
+ padding: 0;
400
+ flex: 1;
401
+ display: flex;
402
+ flex-direction: column;
403
+ overflow: hidden;
404
  }
405
 
406
  .results-header {
407
+ display: flex;
408
+ align-items: center;
409
+ justify-content: space-between;
410
  padding: 16px 20px 8px;
411
  border-bottom: 1px solid var(--border-strong);
412
  margin-bottom: 8px;
 
420
  font-weight: 500;
421
  }
422
 
423
+ .clear-results-btn {
424
+ display: flex;
425
+ align-items: center;
426
+ gap: 6px;
427
+ padding: 6px 12px;
428
+ background: transparent;
429
+ border: 1px solid var(--border-light);
430
+ border-radius: 16px;
431
+ color: var(--text-secondary);
432
+ font-size: 12px;
433
+ cursor: pointer;
434
+ transition: var(--transition-fast);
435
+ }
436
+
437
+ .clear-results-btn:hover {
438
+ background: #ff4444;
439
+ color: white;
440
+ border-color: #ff4444;
441
+ }
442
+
443
+ .clear-text {
444
+ font-size: 12px;
445
+ }
446
+
447
+ /* 响应式:小屏幕隐藏文字,只显示图标 */
448
+ @media (max-width: 480px) {
449
+ .clear-text {
450
+ display: none;
451
+ }
452
+
453
+ .clear-results-btn {
454
+ width: 32px;
455
+ height: 32px;
456
+ padding: 0;
457
+ border-radius: 50%;
458
+ justify-content: center;
459
+ }
460
+ }
461
+
462
  .results-list-container {
463
+ flex: 1;
464
  overflow-y: auto;
465
+ padding: 0 16px;
466
  }
467
 
468
  /* 加载更多 */
 
513
  padding: 12px 16px 8px;
514
  }
515
  }
516
+
517
+ /* PC端适配 */
518
+ @media (min-width: 1024px) {
519
+ .search-results {
520
+ padding-bottom: 0; /* 移除底部padding */
521
+ height: 100%; /* 占满高度 */
522
+ overflow: hidden;
523
+ }
524
+
525
+ .loading-state,
526
+ .error-state,
527
+ .empty-state,
528
+ .default-state {
529
+ padding: 60px 40px; /* 减少padding */
530
+ margin: 20px 32px; /* 调整边距 */
531
+ border-radius: 16px;
532
+ flex: 1;
533
+ justify-content: center;
534
+ }
535
+
536
+ .loading-spinner,
537
+ .error-icon,
538
+ .empty-icon,
539
+ .default-icon {
540
+ width: 80px;
541
+ height: 80px;
542
+ font-size: 32px;
543
+ }
544
+
545
+ .loading-state p,
546
+ .error-message,
547
+ .empty-message,
548
+ .default-message {
549
+ font-size: 20px;
550
+ }
551
+
552
+ .empty-tip {
553
+ font-size: 16px;
554
+ }
555
+
556
+ .suggestions {
557
+ margin-top: 40px;
558
+ max-width: 600px;
559
+ }
560
+
561
+ .suggestions-title {
562
+ font-size: 18px;
563
+ }
564
+
565
+ .suggestion-tags {
566
+ grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
567
+ gap: 16px;
568
+ }
569
+
570
+ .suggestion-tag {
571
+ padding: 16px 20px;
572
+ font-size: 16px;
573
+ border-radius: 25px;
574
+ }
575
+
576
+ .results-list {
577
+ height: 100%;
578
+ padding: 0;
579
+ }
580
+
581
+ .results-header {
582
+ padding: 20px 32px 16px;
583
+ background: var(--bg-primary);
584
+ border-radius: 0;
585
+ border-bottom: 1px solid var(--border-light);
586
+ flex-shrink: 0;
587
+ }
588
+
589
+ .results-count {
590
+ font-size: 16px;
591
+ }
592
+
593
+ .results-list-container {
594
+ flex: 1;
595
+ overflow-y: auto;
596
+ padding: 0 32px;
597
+ max-height: none; /* 移除最大高度限制 */
598
+ }
599
+
600
+ .load-more {
601
+ padding: 32px;
602
+ }
603
+
604
+ .loading-more {
605
+ font-size: 16px;
606
+ gap: 12px;
607
+ }
608
+
609
+ .no-more {
610
+ padding: 32px;
611
+ }
612
+
613
+ .no-more span {
614
+ font-size: 16px;
615
+ }
616
+ }
617
  </style>
src/components/search/SongItem.vue CHANGED
@@ -1,7 +1,7 @@
1
  <template>
2
  <div class="favorite-item" :class="{
3
  selected: isSelected,
4
- playing: isCurrentSong
5
  }">
6
  <!-- 批量选择复选框 -->
7
  <div v-if="showBatchActions" class="item-checkbox">
@@ -15,8 +15,9 @@
15
  </label>
16
  </div>
17
 
18
- <!-- 歌曲信息 -->
19
- <div class="song-info" @click="handleClick">
 
20
  <div class="song-cover">
21
  <img
22
  :src="cover || getSongCover()"
@@ -34,18 +35,23 @@
34
  </div>
35
  </div>
36
 
37
- <div class="song-details">
38
- <h3 class="song-name">{{ song.name }}</h3>
39
- <p class="song-meta">
40
- <span class="artist">{{ formatArtist(song.artist) }}</span>
41
- </p>
 
 
 
 
 
42
  </div>
43
  </div>
44
 
45
  <!-- 操作按钮 -->
46
  <div v-if="!showBatchActions && showActions" class="song-actions">
47
  <!-- 播放状态指示器 -->
48
- <div v-if="isCurrentSong" class="playing-indicator">
49
  <div v-if="isPlaying" class="sound-waves">
50
  <div class="sound-bar"></div>
51
  <div class="sound-bar"></div>
@@ -127,6 +133,21 @@ const props = defineProps({
127
  cover: {
128
  type: String,
129
  default: ''
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
  }
131
  })
132
 
@@ -284,12 +305,19 @@ onMounted(() => {
284
  transform: rotate(45deg);
285
  }
286
 
 
 
 
 
 
 
 
 
287
  .song-info {
288
  display: flex;
289
  align-items: center;
290
  flex: 1;
291
  cursor: pointer;
292
- gap: 12px;
293
  min-width: 0;
294
  }
295
 
@@ -500,4 +528,5 @@ onMounted(() => {
500
  font-size: 12px;
501
  }
502
  }
 
503
  </style>
 
1
  <template>
2
  <div class="favorite-item" :class="{
3
  selected: isSelected,
4
+ playing: shouldShowPlaying && isCurrentSong
5
  }">
6
  <!-- 批量选择复选框 -->
7
  <div v-if="showBatchActions" class="item-checkbox">
 
15
  </label>
16
  </div>
17
 
18
+ <!-- 歌曲信息区域 -->
19
+ <div class="song-item-content">
20
+ <!-- 歌曲封面 -->
21
  <div class="song-cover">
22
  <img
23
  :src="cover || getSongCover()"
 
35
  </div>
36
  </div>
37
 
38
+ <!-- 歌曲详细信息 - 可点击播放 -->
39
+ <div class="song-info" @click="handleClick">
40
+ <div class="song-details">
41
+ <h3 class="song-name">{{ song.name }}</h3>
42
+ <p class="song-meta">
43
+ <span class="artist">{{ formatArtist(song.artist) }}</span>
44
+ <span v-if="playTime" class="play-time">• {{ playTime }}</span>
45
+ <span v-if="playCount" class="play-count">• 播放 {{ playCount }} 次</span>
46
+ </p>
47
+ </div>
48
  </div>
49
  </div>
50
 
51
  <!-- 操作按钮 -->
52
  <div v-if="!showBatchActions && showActions" class="song-actions">
53
  <!-- 播放状态指示器 -->
54
+ <div v-if="shouldShowPlaying && isCurrentSong" class="playing-indicator">
55
  <div v-if="isPlaying" class="sound-waves">
56
  <div class="sound-bar"></div>
57
  <div class="sound-bar"></div>
 
133
  cover: {
134
  type: String,
135
  default: ''
136
+ },
137
+ // 播放次数(用于最多播放视图)
138
+ playCount: {
139
+ type: Number,
140
+ default: 0
141
+ },
142
+ // 播放时间(用于历史记录视图)
143
+ playTime: {
144
+ type: String,
145
+ default: ''
146
+ },
147
+ // 是否应该显示播放状态(用于历史记录去重)
148
+ shouldShowPlaying: {
149
+ type: Boolean,
150
+ default: true
151
  }
152
  })
153
 
 
305
  transform: rotate(45deg);
306
  }
307
 
308
+ .song-item-content {
309
+ display: flex;
310
+ align-items: center;
311
+ flex: 1;
312
+ gap: 12px;
313
+ min-width: 0;
314
+ }
315
+
316
  .song-info {
317
  display: flex;
318
  align-items: center;
319
  flex: 1;
320
  cursor: pointer;
 
321
  min-width: 0;
322
  }
323
 
 
528
  font-size: 12px;
529
  }
530
  }
531
+
532
  </style>
src/components/search/SourceSelector.vue CHANGED
@@ -85,6 +85,34 @@ const selectSource = (sourceCode) => {
85
  box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.2);
86
  }
87
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  @keyframes slideUp {
89
  from { transform: translateY(100%); }
90
  to { transform: translateY(0); }
 
85
  box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.2);
86
  }
87
 
88
+ /* PC端适配 */
89
+ @media (min-width: 768px) {
90
+ .source-selector-overlay {
91
+ align-items: center;
92
+ justify-content: center;
93
+ background: rgba(0, 0, 0, 0.5);
94
+ }
95
+
96
+ .source-selector-content {
97
+ width: 400px;
98
+ max-height: 500px;
99
+ border-radius: 16px;
100
+ animation: scaleIn 0.3s ease;
101
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
102
+ }
103
+
104
+ @keyframes scaleIn {
105
+ from {
106
+ opacity: 0;
107
+ transform: scale(0.9) translateY(20px);
108
+ }
109
+ to {
110
+ opacity: 1;
111
+ transform: scale(1) translateY(0);
112
+ }
113
+ }
114
+ }
115
+
116
  @keyframes slideUp {
117
  from { transform: translateY(100%); }
118
  to { transform: translateY(0); }
src/main.js CHANGED
@@ -81,4 +81,4 @@ app.config.warnHandler = (msg, instance, trace) => {
81
  console.warn('Trace:', trace)
82
  }
83
 
84
- console.log('🎵 云音乐PWA 启动完成')
 
81
  console.warn('Trace:', trace)
82
  }
83
 
84
+ console.log('🎵 云音乐 启动完成')
src/router/index.js CHANGED
@@ -28,7 +28,7 @@ const routes = [
28
  },
29
  {
30
  path: '/my-music',
31
- name: 'MyMusic',
32
  component: MyMusicPage,
33
  meta: {
34
  title: '我的音乐',
@@ -70,7 +70,7 @@ const routes = [
70
  component: PlayQueuePage,
71
  meta: {
72
  title: '播放列表',
73
- keepAlive: true // 启用缓存,避免重复加载图片,播放列表通过响应式数据实现实时更新
74
  }
75
  },
76
  {
@@ -121,9 +121,9 @@ const router = createRouter({
121
  router.beforeEach((to, from, next) => {
122
  // 设置页面标题
123
  if (to.meta?.title) {
124
- document.title = `${to.meta.title} - 云音乐PWA`
125
  }
126
  next()
127
  })
128
 
129
- export default router
 
28
  },
29
  {
30
  path: '/my-music',
31
+ name: 'MyMusic',
32
  component: MyMusicPage,
33
  meta: {
34
  title: '我的音乐',
 
70
  component: PlayQueuePage,
71
  meta: {
72
  title: '播放列表',
73
+ keepAlive: false
74
  }
75
  },
76
  {
 
121
  router.beforeEach((to, from, next) => {
122
  // 设置页面标题
123
  if (to.meta?.title) {
124
+ document.title = `${to.meta.title} - 云音乐`
125
  }
126
  next()
127
  })
128
 
129
+ export default router
src/stores/favorites.js CHANGED
@@ -107,9 +107,35 @@ export const useFavoritesStore = defineStore('favorites', () => {
107
  const lowerKeyword = keyword.toLowerCase().trim()
108
  return favorites.value.filter(fav => {
109
  const song = fav.song
110
- return song.name.toLowerCase().includes(lowerKeyword) ||
111
- song.artist.toLowerCase().includes(lowerKeyword) ||
112
- (song.album && song.album.toLowerCase().includes(lowerKeyword))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  })
114
  }
115
 
 
107
  const lowerKeyword = keyword.toLowerCase().trim()
108
  return favorites.value.filter(fav => {
109
  const song = fav.song
110
+ if (!song) return false
111
+
112
+ // 安全的字符串比较函数
113
+ const safeIncludes = (text) => {
114
+ if (!text) return false
115
+ if (typeof text === 'string') {
116
+ return text.toLowerCase().includes(lowerKeyword)
117
+ }
118
+ if (Array.isArray(text)) {
119
+ // 如果是数组,检查每个元素
120
+ return text.some(item => {
121
+ if (typeof item === 'string') {
122
+ return item.toLowerCase().includes(lowerKeyword)
123
+ }
124
+ if (typeof item === 'object' && item?.name) {
125
+ return item.name.toLowerCase().includes(lowerKeyword)
126
+ }
127
+ return false
128
+ })
129
+ }
130
+ if (typeof text === 'object' && text?.name) {
131
+ return text.name.toLowerCase().includes(lowerKeyword)
132
+ }
133
+ return String(text).toLowerCase().includes(lowerKeyword)
134
+ }
135
+
136
+ return safeIncludes(song.name) ||
137
+ safeIncludes(song.artist) ||
138
+ safeIncludes(song.album)
139
  })
140
  }
141
 
src/stores/history.js CHANGED
@@ -236,18 +236,60 @@ export const useHistoryStore = defineStore('history', () => {
236
  }
237
 
238
  const lowerKeyword = keyword.toLowerCase().trim()
239
- return history.value.filter(item =>
240
- item.song.name.toLowerCase().includes(lowerKeyword) ||
241
- item.song.artist.toLowerCase().includes(lowerKeyword) ||
242
- (item.song.album && item.song.album.toLowerCase().includes(lowerKeyword))
243
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
244
  }
245
 
246
  // 按艺术家筛选
247
  const getHistoryByArtist = (artist) => {
248
- return history.value.filter(item =>
249
- item.song.artist === artist
250
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
251
  }
252
 
253
  // 按时间范围筛选
 
236
  }
237
 
238
  const lowerKeyword = keyword.toLowerCase().trim()
239
+ return history.value.filter(item => {
240
+ const song = item.song
241
+ if (!song) return false
242
+
243
+ // 安全的字符串比较函数
244
+ const safeIncludes = (text) => {
245
+ if (!text) return false
246
+ if (typeof text === 'string') {
247
+ return text.toLowerCase().includes(lowerKeyword)
248
+ }
249
+ if (Array.isArray(text)) {
250
+ // 如果是数组,检查每个元素
251
+ return text.some(item => {
252
+ if (typeof item === 'string') {
253
+ return item.toLowerCase().includes(lowerKeyword)
254
+ }
255
+ if (typeof item === 'object' && item?.name) {
256
+ return item.name.toLowerCase().includes(lowerKeyword)
257
+ }
258
+ return false
259
+ })
260
+ }
261
+ if (typeof text === 'object' && text?.name) {
262
+ return text.name.toLowerCase().includes(lowerKeyword)
263
+ }
264
+ return String(text).toLowerCase().includes(lowerKeyword)
265
+ }
266
+
267
+ return safeIncludes(song.name) ||
268
+ safeIncludes(song.artist) ||
269
+ safeIncludes(song.album)
270
+ })
271
  }
272
 
273
  // 按艺术家筛选
274
  const getHistoryByArtist = (artist) => {
275
+ return history.value.filter(item => {
276
+ if (!item.song || !item.song.artist) return false
277
+
278
+ // 安全的艺术家比较
279
+ const songArtist = item.song.artist
280
+ if (typeof songArtist === 'string') {
281
+ return songArtist === artist
282
+ }
283
+ if (Array.isArray(songArtist)) {
284
+ return songArtist.some(a =>
285
+ typeof a === 'string' ? a === artist : a?.name === artist
286
+ )
287
+ }
288
+ if (typeof songArtist === 'object' && songArtist?.name) {
289
+ return songArtist.name === artist
290
+ }
291
+ return String(songArtist) === artist
292
+ })
293
  }
294
 
295
  // 按时间范围筛选
src/stores/playqueue.js CHANGED
@@ -34,7 +34,21 @@ export const usePlayQueueStore = defineStore('playqueue', () => {
34
  return false
35
  }
36
 
37
- queue.value = songs.slice(0, MAX_QUEUE_SIZE)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  currentIndex.value = Math.max(0, Math.min(startIndex, queue.value.length - 1))
39
  saveQueue()
40
  return true
@@ -264,11 +278,35 @@ export const usePlayQueueStore = defineStore('playqueue', () => {
264
  const lowerKeyword = keyword.toLowerCase().trim()
265
  return queue.value
266
  .map((song, index) => ({ song, index }))
267
- .filter(({ song }) =>
268
- song.name.toLowerCase().includes(lowerKeyword) ||
269
- song.artist.toLowerCase().includes(lowerKeyword) ||
270
- (song.album && song.album.toLowerCase().includes(lowerKeyword))
271
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
272
  }
273
 
274
  // 存储管理
 
34
  return false
35
  }
36
 
37
+ // 去重:移除重复的歌曲
38
+ const uniqueSongs = []
39
+ const seen = new Set()
40
+
41
+ for (const song of songs) {
42
+ if (song && song.id) {
43
+ const key = `${song.source}-${song.id}`
44
+ if (!seen.has(key)) {
45
+ seen.add(key)
46
+ uniqueSongs.push(song)
47
+ }
48
+ }
49
+ }
50
+
51
+ queue.value = uniqueSongs.slice(0, MAX_QUEUE_SIZE)
52
  currentIndex.value = Math.max(0, Math.min(startIndex, queue.value.length - 1))
53
  saveQueue()
54
  return true
 
278
  const lowerKeyword = keyword.toLowerCase().trim()
279
  return queue.value
280
  .map((song, index) => ({ song, index }))
281
+ .filter(({ song }) => {
282
+ // 安全的字符串比较函数
283
+ const safeIncludes = (text) => {
284
+ if (!text) return false
285
+ if (typeof text === 'string') {
286
+ return text.toLowerCase().includes(lowerKeyword)
287
+ }
288
+ if (Array.isArray(text)) {
289
+ // 如果是数组,检查每个元素
290
+ return text.some(item => {
291
+ if (typeof item === 'string') {
292
+ return item.toLowerCase().includes(lowerKeyword)
293
+ }
294
+ if (typeof item === 'object' && item?.name) {
295
+ return item.name.toLowerCase().includes(lowerKeyword)
296
+ }
297
+ return false
298
+ })
299
+ }
300
+ if (typeof text === 'object' && text?.name) {
301
+ return text.name.toLowerCase().includes(lowerKeyword)
302
+ }
303
+ return String(text).toLowerCase().includes(lowerKeyword)
304
+ }
305
+
306
+ return safeIncludes(song.name) ||
307
+ safeIncludes(song.artist) ||
308
+ safeIncludes(song.album)
309
+ })
310
  }
311
 
312
  // 存储管理
src/styles/global.css CHANGED
@@ -5,6 +5,11 @@
5
  --mini-player-height: 64px;
6
  --list-item-height: 64px;
7
  --touch-target: 44px;
 
 
 
 
 
8
 
9
  /* 圆角 */
10
  --radius-small: 12px;
 
5
  --mini-player-height: 64px;
6
  --list-item-height: 64px;
7
  --touch-target: 44px;
8
+ --sidebar-width: 280px;
9
+
10
+ /* PC端组件尺寸 */
11
+ --pc-list-item-height: 80px;
12
+ --pc-touch-target: 48px;
13
 
14
  /* 圆角 */
15
  --radius-small: 12px;
src/views/FavoritesPage.vue CHANGED
@@ -2,10 +2,10 @@
2
  <div class="favorites-page">
3
  <!-- 头部 -->
4
  <div class="page-header">
5
- <h1 class="page-title">
6
  <i class="fas fa-heart"></i>
7
  我喜欢的音乐
8
- </h1>
9
  <div class="header-actions">
10
  <button v-if="!isEmpty" class="action-btn" @click="playAll">
11
  <i class="fas fa-play"></i>
@@ -17,7 +17,7 @@
17
  </div>
18
 
19
  <!-- 搜索框 -->
20
- <div v-if="!isEmpty" class="search-section">
21
  <div class="search-box">
22
  <i class="fas fa-search"></i>
23
  <input
@@ -256,6 +256,21 @@ const playAll = async () => {
256
  }
257
  }
258
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
259
  // 收藏操作
260
  const toggleFavorite = async (song) => {
261
  const result = await favoritesStore.removeFromFavorites(song)
@@ -441,19 +456,21 @@ onMounted(async () => {
441
  overflow-y: auto;
442
  }
443
 
 
444
  .page-header {
445
  display: flex;
446
  align-items: center;
447
  justify-content: space-between;
448
  padding: 16px;
449
  border-bottom: 1px solid var(--border-lighter);
 
450
  }
451
 
452
  .page-title {
453
  display: flex;
454
  align-items: center;
455
  gap: 12px;
456
- font-size: 24px;
457
  font-weight: 700;
458
  color: var(--text-primary);
459
  margin: 0;
@@ -471,21 +488,38 @@ onMounted(async () => {
471
  .action-btn {
472
  display: flex;
473
  align-items: center;
474
- gap: 6px;
475
- padding: 8px 16px;
 
476
  border: none;
477
- background: var(--accent-red);
478
- color: white;
479
- border-radius: 20px;
480
  font-size: 14px;
481
- font-weight: 500;
482
  cursor: pointer;
483
  transition: var(--transition-fast);
484
  }
485
 
486
  .action-btn:hover {
487
- background: var(--accent-red-hover);
488
- transform: translateY(-1px);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
489
  }
490
 
491
  .stats-card {
@@ -883,20 +917,158 @@ onMounted(async () => {
883
  }
884
  }
885
 
886
- @media (min-width: 768px) {
 
887
  .favorites-page {
888
- max-width: 1200px;
889
- margin: 0 auto;
 
 
890
  }
891
 
892
- .stats-card {
893
- max-width: 600px;
894
- margin: 16px auto;
 
 
 
 
895
  }
896
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
897
  .favorites-list {
898
- max-width: 800px;
899
- margin: 0 auto;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
900
  }
901
  }
902
  </style>
 
2
  <div class="favorites-page">
3
  <!-- 头部 -->
4
  <div class="page-header">
5
+ <div class="page-title">
6
  <i class="fas fa-heart"></i>
7
  我喜欢的音乐
8
+ </div>
9
  <div class="header-actions">
10
  <button v-if="!isEmpty" class="action-btn" @click="playAll">
11
  <i class="fas fa-play"></i>
 
17
  </div>
18
 
19
  <!-- 搜索框 -->
20
+ <div v-if="!isEmpty" class="search-section hidden-desktop">
21
  <div class="search-box">
22
  <i class="fas fa-search"></i>
23
  <input
 
256
  }
257
  }
258
 
259
+ const shufflePlay = async () => {
260
+ if (isEmpty.value) return
261
+
262
+ const songs = displayedFavorites.value.map(fav => fav.song)
263
+ const shuffled = [...songs].sort(() => Math.random() - 0.5)
264
+ const result = playQueueStore.setQueue(shuffled, 0)
265
+
266
+ if (result && shuffled.length > 0) {
267
+ await playerStore.playSong(shuffled[0])
268
+ toastStore.success(`开始随机播放 ${shuffled.length} 首歌曲`)
269
+ } else {
270
+ toastStore.error('播放失败')
271
+ }
272
+ }
273
+
274
  // 收藏操作
275
  const toggleFavorite = async (song) => {
276
  const result = await favoritesStore.removeFromFavorites(song)
 
456
  overflow-y: auto;
457
  }
458
 
459
+ /* 使用标准的page-header样式 */
460
  .page-header {
461
  display: flex;
462
  align-items: center;
463
  justify-content: space-between;
464
  padding: 16px;
465
  border-bottom: 1px solid var(--border-lighter);
466
+ background: var(--bg-primary);
467
  }
468
 
469
  .page-title {
470
  display: flex;
471
  align-items: center;
472
  gap: 12px;
473
+ font-size: 20px;
474
  font-weight: 700;
475
  color: var(--text-primary);
476
  margin: 0;
 
488
  .action-btn {
489
  display: flex;
490
  align-items: center;
491
+ justify-content: center;
492
+ width: 36px;
493
+ height: 36px;
494
  border: none;
495
+ background: rgba(255, 255, 255, 0.1);
496
+ color: var(--text-secondary);
497
+ border-radius: 50%;
498
  font-size: 14px;
 
499
  cursor: pointer;
500
  transition: var(--transition-fast);
501
  }
502
 
503
  .action-btn:hover {
504
+ background: var(--accent-red);
505
+ color: white;
506
+ transform: scale(1.1);
507
+ }
508
+
509
+ .action-btn.active {
510
+ background: var(--accent-red);
511
+ color: white;
512
+ }
513
+
514
+ .content-area {
515
+ flex: 1;
516
+ min-height: 0;
517
+ }
518
+
519
+ .favorites-list {
520
+ background: var(--bg-card);
521
+ margin: 0;
522
+ overflow: hidden;
523
  }
524
 
525
  .stats-card {
 
917
  }
918
  }
919
 
920
+ /* PC端适配 */
921
+ @media (min-width: 1024px) {
922
  .favorites-page {
923
+ height: 100vh;
924
+ overflow: hidden;
925
+ display: flex;
926
+ flex-direction: column;
927
  }
928
 
929
+ .page-header {
930
+ display: flex;
931
+ align-items: center;
932
+ justify-content: space-between;
933
+ padding: 16px;
934
+ border-bottom: 1px solid var(--border-lighter);
935
+ background: var(--bg-primary);
936
  }
937
 
938
+ .page-title {
939
+ display: flex;
940
+ align-items: center;
941
+ gap: 12px;
942
+ font-size: 20px;
943
+ font-weight: 700;
944
+ color: var(--text-primary);
945
+ margin: 0;
946
+ }
947
+
948
+ .page-title i {
949
+ color: var(--accent-red);
950
+ }
951
+
952
+ .header-search {
953
+ flex: 0 0 300px;
954
+ }
955
+
956
+ .header-search .search-box {
957
+ background: var(--bg-secondary);
958
+ border: 1px solid var(--border-light);
959
+ }
960
+
961
+ .header-actions {
962
+ display: flex;
963
+ gap: 8px;
964
+ }
965
+
966
+ .action-btn {
967
+ display: flex;
968
+ align-items: center;
969
+ gap: 6px;
970
+ padding: 8px 16px;
971
+ border: none;
972
+ background: var(--accent-red);
973
+ color: white;
974
+ border-radius: 20px;
975
+ font-size: 14px;
976
+ font-weight: 500;
977
+ cursor: pointer;
978
+ transition: var(--transition-fast);
979
+ }
980
+
981
+ .action-btn:hover {
982
+ background: var(--accent-red-hover);
983
+ transform: translateY(-1px);
984
+ }
985
+
986
+ .search-section {
987
+ display: none;
988
+ }
989
+
990
+ .batch-actions {
991
+ flex-shrink: 0;
992
+ padding: 16px 32px;
993
+ margin: 0;
994
+ border-radius: 0;
995
+ background: var(--bg-secondary);
996
+ border-bottom: 1px solid var(--border-light);
997
+ }
998
+
999
+ .content-area {
1000
+ flex: 1;
1001
+ overflow: hidden;
1002
+ display: flex;
1003
+ flex-direction: column;
1004
+ }
1005
+
1006
+ .empty-state {
1007
+ flex: 1;
1008
+ padding: 80px 60px;
1009
+ margin: 0;
1010
+ background: var(--bg-primary);
1011
+ border: none;
1012
+ border-radius: 0;
1013
+ justify-content: center;
1014
+ }
1015
+
1016
+ .empty-state i {
1017
+ font-size: 80px;
1018
+ }
1019
+
1020
+ .empty-state p {
1021
+ font-size: 18px;
1022
+ }
1023
+
1024
+ .empty-tip {
1025
+ font-size: 16px;
1026
+ }
1027
+
1028
+ /* 修复歌曲名溢出问题 */
1029
  .favorites-list {
1030
+ flex: 1;
1031
+ overflow-y: auto;
1032
+ padding: 0;
1033
+ background: var(--bg-primary);
1034
+ }
1035
+
1036
+ /* 确保列表项的正确布局 */
1037
+ .favorites-list .favorite-item {
1038
+ display: flex;
1039
+ align-items: center;
1040
+ padding: 12px 16px;
1041
+ }
1042
+
1043
+ .favorites-list .item-content {
1044
+ flex: 1;
1045
+ min-width: 0;
1046
+ display: flex;
1047
+ align-items: center;
1048
+ gap: 12px;
1049
+ }
1050
+
1051
+ .favorites-list .song-info {
1052
+ flex: 1;
1053
+ min-width: 0;
1054
+ overflow: hidden;
1055
+ }
1056
+
1057
+ .favorites-list .song-name {
1058
+ overflow: hidden;
1059
+ text-overflow: ellipsis;
1060
+ white-space: nowrap;
1061
+ }
1062
+
1063
+ .favorites-list .song-meta {
1064
+ overflow: hidden;
1065
+ text-overflow: ellipsis;
1066
+ white-space: nowrap;
1067
+ }
1068
+
1069
+ .favorites-list .item-actions {
1070
+ flex-shrink: 0;
1071
+ margin-left: 12px;
1072
  }
1073
  }
1074
  </style>
src/views/FullPlayerPage.vue CHANGED
@@ -1,5 +1,12 @@
1
  <template>
2
- <div class="full-player" :class="{ 'lyrics-mode': showLyrics }">
 
 
 
 
 
 
 
3
  <!-- 背景 -->
4
  <div class="background-overlay">
5
  <img
@@ -170,6 +177,7 @@ import { useHistoryStore } from '@/stores/history'
170
  import { useToastStore } from '@/stores/toast'
171
  import { useSettingsStore } from '@/stores/settings'
172
  import { musicApi, utils } from '@/services/musicApi'
 
173
  import AlbumCover from '@/components/player/AlbumCover.vue'
174
  import PlayControls from '@/components/player/PlayControls.vue'
175
  import ProgressBar from '@/components/player/ProgressBar.vue'
@@ -1311,6 +1319,106 @@ watch(currentSong, (newSong, oldSong) => {
1311
  }
1312
  }
1313
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1314
  /* 动画效果 */
1315
  .full-player {
1316
  animation: slideUp 0.3s ease-out;
 
1
  <template>
2
+ <!-- PC端显示DesktopFullPlayer组件 -->
3
+ <DesktopFullPlayer
4
+ v-if="isDesktop"
5
+ @close="handleBack"
6
+ />
7
+
8
+ <!-- 移动端保持原有布局 -->
9
+ <div v-else class="full-player" :class="{ 'lyrics-mode': showLyrics }">
10
  <!-- 背景 -->
11
  <div class="background-overlay">
12
  <img
 
177
  import { useToastStore } from '@/stores/toast'
178
  import { useSettingsStore } from '@/stores/settings'
179
  import { musicApi, utils } from '@/services/musicApi'
180
+ import DesktopFullPlayer from '@/components/player/DesktopFullPlayer.vue'
181
  import AlbumCover from '@/components/player/AlbumCover.vue'
182
  import PlayControls from '@/components/player/PlayControls.vue'
183
  import ProgressBar from '@/components/player/ProgressBar.vue'
 
1319
  }
1320
  }
1321
 
1322
+ /* PC端进一步优化 */
1323
+ @media (min-width: 1024px) {
1324
+ .full-player {
1325
+ max-width: 800px;
1326
+ height: 90vh;
1327
+ top: 5vh;
1328
+ bottom: auto;
1329
+ border-radius: 20px;
1330
+ overflow: hidden;
1331
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
1332
+ }
1333
+
1334
+ .player-header {
1335
+ padding: 24px 32px;
1336
+ }
1337
+
1338
+ .back-btn,
1339
+ .more-btn {
1340
+ width: 48px;
1341
+ height: 48px;
1342
+ font-size: 18px;
1343
+ }
1344
+
1345
+ .song-info-header .song-title {
1346
+ font-size: 20px;
1347
+ }
1348
+
1349
+ .song-info-header .artist-name {
1350
+ font-size: 14px;
1351
+ position: static;
1352
+ transform: none;
1353
+ width: auto;
1354
+ margin-top: 4px;
1355
+ }
1356
+
1357
+ .player-content {
1358
+ padding: 40px 60px;
1359
+ display: grid;
1360
+ grid-template-columns: 1fr 1fr;
1361
+ gap: 60px;
1362
+ align-items: start;
1363
+ }
1364
+
1365
+ .cover-section {
1366
+ margin-top: 0;
1367
+ max-width: none;
1368
+ }
1369
+
1370
+ .lyrics-section {
1371
+ margin-top: 0;
1372
+ max-width: none;
1373
+ }
1374
+
1375
+ .song-info {
1376
+ grid-column: 1 / 3;
1377
+ margin-top: 0;
1378
+ text-align: left;
1379
+ }
1380
+
1381
+ .current-lyric {
1382
+ text-align: left;
1383
+ justify-content: flex-start;
1384
+ }
1385
+
1386
+ .lyric-text {
1387
+ font-size: 22px;
1388
+ text-align: left;
1389
+ }
1390
+
1391
+ .artist-album-row {
1392
+ justify-content: flex-start;
1393
+ }
1394
+
1395
+ .player-controls {
1396
+ padding: 20px 60px 40px;
1397
+ }
1398
+
1399
+ /* 网格布局下的歌词和封面 */
1400
+ .full-player:not(.lyrics-mode) .player-content {
1401
+ grid-template-columns: 1fr;
1402
+ text-align: center;
1403
+ }
1404
+
1405
+ .full-player:not(.lyrics-mode) .song-info {
1406
+ text-align: center;
1407
+ }
1408
+
1409
+ .full-player:not(.lyrics-mode) .current-lyric {
1410
+ justify-content: center;
1411
+ }
1412
+
1413
+ .full-player:not(.lyrics-mode) .lyric-text {
1414
+ text-align: center;
1415
+ }
1416
+
1417
+ .full-player:not(.lyrics-mode) .artist-album-row {
1418
+ justify-content: center;
1419
+ }
1420
+ }
1421
+
1422
  /* 动画效果 */
1423
  .full-player {
1424
  animation: slideUp 0.3s ease-out;
src/views/HistoryPage.vue CHANGED
@@ -6,44 +6,48 @@
6
  <i class="fas fa-history"></i>
7
  播放历史
8
  </h1>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  <div class="header-actions">
10
  <button v-if="!isEmpty" class="action-btn danger" @click="confirmClearAll">
11
  <i class="fas fa-trash"></i>
12
- <span>清空</span>
13
  </button>
14
  </div>
15
  </div>
16
 
17
- <!-- 统计信息 -->
18
- <div class="stats-grid">
19
- <div class="stat-card">
20
- <div class="stat-number">{{ totalPlays }}</div>
21
- <div class="stat-label">总播放</div>
22
- </div>
23
- <div class="stat-card">
24
- <div class="stat-number">{{ uniqueSongs }}</div>
25
- <div class="stat-label">不同歌曲</div>
26
- </div>
27
- </div>
28
 
29
- <!-- 筛选和搜索 -->
30
- <div v-if="!isEmpty" class="filter-section">
31
  <div class="filter-tabs">
32
  <button
33
  class="filter-tab"
34
- :class="{ active: viewMode === 'recent' }"
35
  @click="switchView('recent')"
36
  >
37
  <i class="fas fa-clock"></i>
38
- 最近播放
39
- </button>
40
- <button
41
- class="filter-tab"
42
- :class="{ active: viewMode === 'grouped' }"
43
- @click="switchView('grouped')"
44
- >
45
- <i class="fas fa-calendar"></i>
46
- 按日期
47
  </button>
48
  <button
49
  class="filter-tab"
@@ -55,6 +59,7 @@
55
  </button>
56
  </div>
57
 
 
58
  <div class="search-box">
59
  <i class="fas fa-search"></i>
60
  <input
@@ -84,47 +89,18 @@
84
 
85
  <!-- 最近播放 -->
86
  <div v-else-if="viewMode === 'recent'" class="recent-view">
87
- <div class="history-list">
88
- <div
89
  v-for="(item, index) in displayedHistory"
90
  :key="`${item.song.id}-${item.timestamp}`"
91
- class="history-item"
92
- >
93
- <div class="item-content" @click="playFromHistory(item.song, index)">
94
- <div class="song-cover">
95
- <img
96
- :src="getSongCover(item.song)"
97
- :alt="item.song.name"
98
- @error="handleImageError"
99
- />
100
- <div class="play-overlay">
101
- <i class="fas fa-play"></i>
102
- </div>
103
- </div>
104
-
105
- <div class="song-info">
106
- <h3 class="song-name">{{ item.song.name }}</h3>
107
- <p class="song-meta">
108
- <span class="artist">{{ item.song.artist }}</span>
109
- <span v-if="item.song.album" class="album"> - {{ item.song.album }}</span>
110
- </p>
111
- <p class="play-time">{{ formatPlayTime(item.timestamp) }}</p>
112
- </div>
113
- </div>
114
-
115
- <div class="item-actions">
116
- <button
117
- class="action-btn favorite-btn"
118
- :class="{ active: isFavorite(item.song) }"
119
- @click="toggleFavorite(item.song)"
120
- >
121
- <i class="fas fa-heart"></i>
122
- </button>
123
- <button class="action-btn" @click="handleShowMoreActions(item.song, $event)">
124
- <i class="fas fa-ellipsis-h"></i>
125
- </button>
126
- </div>
127
- </div>
128
  </div>
129
  </div>
130
 
@@ -141,95 +117,34 @@
141
  </div>
142
 
143
  <div class="group-content">
144
- <div
145
  v-for="(item, index) in group"
146
  :key="`${item.song.id}-${item.timestamp}`"
147
- class="history-item"
148
- >
149
- <div class="item-content" @click="playFromGroup(item.song, group, index)">
150
- <div class="song-cover">
151
- <img
152
- :src="getSongCover(item.song)"
153
- :alt="item.song.name"
154
- @error="handleImageError"
155
- />
156
- <div class="play-overlay">
157
- <i class="fas fa-play"></i>
158
- </div>
159
- </div>
160
-
161
- <div class="song-info">
162
- <h3 class="song-name">{{ item.song.name }}</h3>
163
- <p class="song-meta">
164
- <span class="artist">{{ item.song.artist }}</span>
165
- <span v-if="item.song.album" class="album"> - {{ item.song.album }}</span>
166
- </p>
167
- <p class="play-time">{{ formatPlayTime(item.timestamp, true) }}</p>
168
- </div>
169
- </div>
170
-
171
- <div class="item-actions">
172
- <button
173
- class="action-btn favorite-btn"
174
- :class="{ active: isFavorite(item.song) }"
175
- @click="toggleFavorite(item.song)"
176
- >
177
- <i class="fas fa-heart"></i>
178
- </button>
179
- <button class="action-btn" @click="showMoreActions(item.song, $event)">
180
- <i class="fas fa-ellipsis-h"></i>
181
- </button>
182
- </div>
183
- </div>
184
  </div>
185
  </div>
186
  </div>
187
 
188
  <!-- 最多播放 -->
189
  <div v-else-if="viewMode === 'top'" class="top-view">
190
- <div class="top-list">
191
- <div
192
  v-for="(item, index) in topPlayedSongs"
193
  :key="item.songKey"
194
- class="top-item"
195
- >
196
- <div class="rank-badge">{{ index + 1 }}</div>
197
-
198
- <div class="item-content" @click="playTopSong(item.song)">
199
- <div class="song-cover">
200
- <img
201
- :src="getSongCover(item.song)"
202
- :alt="item.song.name"
203
- @error="handleImageError"
204
- />
205
- <div class="play-overlay">
206
- <i class="fas fa-play"></i>
207
- </div>
208
- </div>
209
-
210
- <div class="song-info">
211
- <h3 class="song-name">{{ item.song.name }}</h3>
212
- <p class="song-meta">
213
- <span class="artist">{{ item.song.artist }}</span>
214
- <span v-if="item.song.album" class="album"> - {{ item.song.album }}</span>
215
- </p>
216
- <p class="play-count">播放 {{ item.count }} 次</p>
217
- </div>
218
- </div>
219
-
220
- <div class="item-actions">
221
- <button
222
- class="action-btn favorite-btn"
223
- :class="{ active: isFavorite(item.song) }"
224
- @click="toggleFavorite(item.song)"
225
- >
226
- <i class="fas fa-heart"></i>
227
- </button>
228
- <button class="action-btn" @click="handleShowMoreActions(item.song, $event)">
229
- <i class="fas fa-ellipsis-h"></i>
230
- </button>
231
- </div>
232
- </div>
233
  </div>
234
  </div>
235
  </div>
@@ -265,6 +180,7 @@ import { useToastStore } from '@/stores/toast'
265
  import { imageCacheManager } from '@/utils/imageCache'
266
  import MoreActionsPanel from '@/components/player/MoreActionsPanel.vue'
267
  import ConfirmDialog from '@/components/common/ConfirmDialog.vue'
 
268
 
269
  const router = useRouter()
270
  const historyStore = useHistoryStore()
@@ -274,13 +190,12 @@ const playQueueStore = usePlayQueueStore()
274
  const toastStore = useToastStore()
275
 
276
  // 响应式数据
277
- const viewMode = ref('grouped') // 'recent', 'grouped', 'top' - 默认显示按日期
278
  const searchKeyword = ref('')
279
  const selectedSong = ref(null)
280
  const showMoreActions = ref(false)
281
  const moreActionsRef = ref(null)
282
  const confirmDialogRef = ref(null)
283
- const songCovers = ref(new Map()) // 存储异步加载的封面URL
284
 
285
  // 确认对话框状态
286
  const confirmDialog = ref({
@@ -297,20 +212,25 @@ const history = computed(() => historyStore.history)
297
  const totalPlays = computed(() => historyStore.totalPlays)
298
  const uniqueSongs = computed(() => historyStore.uniqueSongs)
299
  const isEmpty = computed(() => historyStore.isEmpty)
 
 
300
 
301
  // 播放统计
302
  const playStats = computed(() => historyStore.getPlayStats())
303
 
304
  // 搜索后的历史记录
305
  const displayedHistory = computed(() => {
306
- const baseHistory = viewMode.value === 'recent'
307
- ? historyStore.getRecentSongs(100)
308
  : history.value
309
 
310
  if (!searchKeyword.value.trim()) {
311
  return baseHistory
312
  }
313
- return historyStore.searchHistory(searchKeyword.value)
 
 
 
314
  })
315
 
316
  // 按日期分组的历史记录
@@ -320,7 +240,45 @@ const groupedHistory = computed(() => {
320
 
321
  // 最多播放的歌曲
322
  const topPlayedSongs = computed(() => {
323
- return historyStore.getTopPlayedSongs(50)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
324
  })
325
 
326
  // 方法
@@ -336,19 +294,48 @@ const formatDuration = (seconds) => {
336
 
337
  const formatPlayTime = (timestamp, showTime = false) => {
338
  const date = new Date(timestamp)
 
 
 
339
 
340
- if (showTime) {
341
- return date.toLocaleTimeString('zh-CN', {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
342
  hour: '2-digit',
343
  minute: '2-digit'
344
  })
345
  }
346
-
347
- // 移除"多少分钟前"显示,直接显示时间
348
- return date.toLocaleTimeString('zh-CN', {
349
- hour: '2-digit',
350
- minute: '2-digit'
351
- })
352
  }
353
 
354
  const formatDate = (dateString) => {
@@ -370,53 +357,16 @@ const formatDate = (dateString) => {
370
  }
371
  }
372
 
373
- const getSongCover = (song) => {
374
- const songKey = `${song.source}-${song.pic_id || song.id}`
375
-
376
- // 先返回缓存的URL或默认图片
377
- const cached = songCovers.value.get(songKey)
378
- if (cached) {
379
- return cached
380
  }
381
-
382
- // 异步加载封面URL
383
- loadSongCover(song)
384
-
385
- // 返回默认图片或playerStore缓存
386
- return playerStore.getCachedCover(song) || imageCacheManager.getDefaultImage()
387
- }
388
-
389
- const loadSongCover = async (song) => {
390
- const songKey = `${song.source}-${song.pic_id || song.id}`
391
-
392
- // 如果已经在加载中,跳过
393
- if (songCovers.value.has(songKey)) {
394
- return
395
- }
396
-
397
- // 设置默认值,防止重复加载
398
- songCovers.value.set(songKey, imageCacheManager.getDefaultImage())
399
-
400
- // 使用新的缓存机制
401
- if (song.pic_id || song.id) {
402
- try {
403
- const { getCachedMusicPicUrlWithDelay } = await import('@/utils/musicPicCache.js')
404
- const coverUrl = await getCachedMusicPicUrlWithDelay(
405
- song.source,
406
- song.pic_id || song.id,
407
- 300
408
- )
409
- if (coverUrl) {
410
- songCovers.value.set(songKey, coverUrl)
411
- }
412
- } catch (error) {
413
- console.error('获取历史歌曲封面失败:', error)
414
- }
415
  }
416
- }
417
-
418
- const handleImageError = (event) => {
419
- event.target.src = imageCacheManager.getDefaultImage()
420
  }
421
 
422
  const switchView = (mode) => {
@@ -437,13 +387,30 @@ const isFavorite = (song) => {
437
  return favoritesStore.isFavorite(song)
438
  }
439
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
440
  const toggleFavorite = async (song) => {
441
  const result = await favoritesStore.toggleFavorite(song)
442
  const message = result ? '已添加到我喜欢的音乐' : '已从我喜欢的音乐中移除'
443
  toastStore.success(message)
444
  }
445
 
446
- // 播放相关
447
  const playFromHistory = async (song, index) => {
448
  const songs = displayedHistory.value.map(item => item.song)
449
  const result = playQueueStore.setQueue(songs, index)
@@ -468,14 +435,23 @@ const playFromGroup = async (song, group, index) => {
468
  }
469
  }
470
 
471
- const playTopSong = async (song) => {
472
- const result = playQueueStore.addToQueue(song, 'end')
473
-
474
- if (result.success) {
475
- await playerStore.playSong(song)
476
- toastStore.success(`开始播放 "${song.name}"`)
477
- } else {
478
- toastStore.error('播放失败')
 
 
 
 
 
 
 
 
 
479
  }
480
  }
481
 
@@ -554,19 +530,21 @@ onMounted(() => {
554
  overflow-y: auto;
555
  }
556
 
 
557
  .page-header {
558
  display: flex;
559
  align-items: center;
560
  justify-content: space-between;
561
  padding: 16px;
562
  border-bottom: 1px solid var(--border-lighter);
 
563
  }
564
 
565
  .page-title {
566
  display: flex;
567
  align-items: center;
568
  gap: 12px;
569
- font-size: 24px;
570
  font-weight: 700;
571
  color: var(--text-primary);
572
  margin: 0;
@@ -576,6 +554,28 @@ onMounted(() => {
576
  color: var(--accent-red);
577
  }
578
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
579
  .header-actions {
580
  display: flex;
581
  gap: 8px;
@@ -584,57 +584,77 @@ onMounted(() => {
584
  .action-btn {
585
  display: flex;
586
  align-items: center;
587
- gap: 6px;
588
- padding: 8px 16px;
 
589
  border: none;
590
- background: var(--accent-red);
591
- color: white;
592
- border-radius: 20px;
593
  font-size: 14px;
594
- font-weight: 500;
595
  cursor: pointer;
596
  transition: var(--transition-fast);
597
  }
598
 
599
  .action-btn:hover {
600
- background: var(--accent-red-hover);
601
- transform: translateY(-1px);
 
602
  }
603
 
604
  .action-btn.danger {
605
- background: #ff4444;
606
  }
607
 
608
  .action-btn.danger:hover {
609
- background: #ff6666;
 
610
  }
611
 
612
- .stats-grid {
613
- display: grid;
614
- grid-template-columns: repeat(2, 1fr);
615
- gap: 12px;
616
- padding: 16px;
617
  background: var(--bg-card);
618
- margin: 16px;
619
- border-radius: var(--radius-medium);
620
- border: 1px solid var(--border-light);
621
  }
622
 
623
- .stat-card {
624
- text-align: center;
625
- padding: 16px 8px;
 
 
 
626
  }
627
 
628
- .stat-number {
629
- font-size: 20px;
630
- font-weight: 700;
631
- color: var(--accent-red);
632
- margin-bottom: 4px;
 
 
 
 
 
 
 
 
633
  }
634
 
635
- .stat-label {
636
- font-size: 12px;
637
- color: var(--text-secondary);
 
 
 
 
 
 
 
 
 
 
638
  }
639
 
640
  .filter-section {
@@ -643,6 +663,7 @@ onMounted(() => {
643
 
644
  .filter-tabs {
645
  display: flex;
 
646
  gap: 8px;
647
  margin-bottom: 16px;
648
  overflow-x: auto;
@@ -716,11 +737,6 @@ onMounted(() => {
716
  color: var(--text-primary);
717
  }
718
 
719
- .content-area {
720
- flex: 1;
721
- min-height: 0;
722
- }
723
-
724
  .empty-state {
725
  display: flex;
726
  flex-direction: column;
@@ -768,8 +784,10 @@ onMounted(() => {
768
  transform: scale(1.05);
769
  }
770
 
771
- .history-list,
772
- .top-list {
 
 
773
  padding-bottom: calc(var(--mini-player-height) + var(--tabbar-height) + 20px);
774
  }
775
 
@@ -781,6 +799,8 @@ onMounted(() => {
781
  border-bottom: 1px solid var(--border-lighter);
782
  background: var(--bg-card);
783
  transition: var(--transition-fast);
 
 
784
  }
785
 
786
  .history-item:hover,
@@ -788,6 +808,12 @@ onMounted(() => {
788
  background: var(--bg-gradient-1);
789
  }
790
 
 
 
 
 
 
 
791
  .rank-badge {
792
  width: 32px;
793
  height: 32px;
@@ -803,12 +829,26 @@ onMounted(() => {
803
  flex-shrink: 0;
804
  }
805
 
 
 
 
 
 
 
 
 
 
 
 
 
806
  .item-content {
807
  display: flex;
808
  align-items: center;
809
  flex: 1;
810
  cursor: pointer;
811
  gap: 12px;
 
 
812
  }
813
 
814
  .song-cover {
@@ -849,9 +889,75 @@ onMounted(() => {
849
  font-size: 16px;
850
  }
851
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
852
  .song-info {
853
  flex: 1;
854
  min-width: 0;
 
855
  }
856
 
857
  .song-name {
@@ -862,6 +968,7 @@ onMounted(() => {
862
  overflow: hidden;
863
  text-overflow: ellipsis;
864
  white-space: nowrap;
 
865
  }
866
 
867
  .song-meta {
@@ -871,6 +978,7 @@ onMounted(() => {
871
  overflow: hidden;
872
  text-overflow: ellipsis;
873
  white-space: nowrap;
 
874
  }
875
 
876
  .play-time,
@@ -884,6 +992,8 @@ onMounted(() => {
884
  display: flex;
885
  gap: 8px;
886
  flex-shrink: 0;
 
 
887
  }
888
 
889
  .item-actions .action-btn {
@@ -999,21 +1109,261 @@ onMounted(() => {
999
  }
1000
  }
1001
 
1002
- @media (min-width: 768px) {
 
1003
  .history-page {
1004
- max-width: 1200px;
1005
- margin: 0 auto;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1006
  }
1007
 
1008
  .stats-grid {
1009
- max-width: 600px;
1010
- margin: 16px auto;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1011
  }
1012
 
1013
- .history-list,
1014
- .top-list {
1015
- max-width: 800px;
1016
- margin: 0 auto;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1017
  }
1018
  }
1019
  </style>
 
6
  <i class="fas fa-history"></i>
7
  播放历史
8
  </h1>
9
+
10
+ <!-- PC端统计信息和筛选 -->
11
+ <div v-if="!isEmpty" class="stats-and-filters hidden-mobile">
12
+
13
+ <div class="filter-tabs">
14
+ <button
15
+ class="filter-tab"
16
+ :class="{ active: viewMode === 'recent' || viewMode === 'grouped' }"
17
+ @click="switchView('recent')"
18
+ >
19
+ <i class="fas fa-clock"></i>
20
+ 时间顺序
21
+ </button>
22
+ <button
23
+ class="filter-tab"
24
+ :class="{ active: viewMode === 'top' }"
25
+ @click="switchView('top')"
26
+ >
27
+ <i class="fas fa-fire"></i>
28
+ 最多播放
29
+ </button>
30
+ </div>
31
+ </div>
32
+
33
  <div class="header-actions">
34
  <button v-if="!isEmpty" class="action-btn danger" @click="confirmClearAll">
35
  <i class="fas fa-trash"></i>
 
36
  </button>
37
  </div>
38
  </div>
39
 
 
 
 
 
 
 
 
 
 
 
 
40
 
41
+ <!-- 移动端筛选和搜索 -->
42
+ <div v-if="!isEmpty" class="filter-section hidden-desktop">
43
  <div class="filter-tabs">
44
  <button
45
  class="filter-tab"
46
+ :class="{ active: viewMode === 'recent' || viewMode === 'grouped' }"
47
  @click="switchView('recent')"
48
  >
49
  <i class="fas fa-clock"></i>
50
+ 时间顺序
 
 
 
 
 
 
 
 
51
  </button>
52
  <button
53
  class="filter-tab"
 
59
  </button>
60
  </div>
61
 
62
+ <!-- 移动端搜索框 -->
63
  <div class="search-box">
64
  <i class="fas fa-search"></i>
65
  <input
 
89
 
90
  <!-- 最近播放 -->
91
  <div v-else-if="viewMode === 'recent'" class="recent-view">
92
+ <div class="songs-list">
93
+ <SongItem
94
  v-for="(item, index) in displayedHistory"
95
  :key="`${item.song.id}-${item.timestamp}`"
96
+ :song="item.song"
97
+ :index="index"
98
+ :show-actions="true"
99
+ :play-time="formatPlayTime(item.timestamp)"
100
+ :should-show-playing="shouldShowPlayingState(item.song, index, displayedHistory)"
101
+ @play="playFromHistory"
102
+ @showMoreActions="handleShowMoreActions"
103
+ />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  </div>
105
  </div>
106
 
 
117
  </div>
118
 
119
  <div class="group-content">
120
+ <SongItem
121
  v-for="(item, index) in group"
122
  :key="`${item.song.id}-${item.timestamp}`"
123
+ :song="item.song"
124
+ :index="index"
125
+ :show-actions="true"
126
+ :play-time="formatPlayTime(item.timestamp, true)"
127
+ :should-show-playing="shouldShowPlayingState(item.song, index, group)"
128
+ @play="(song, idx) => playFromGroup(song, group, idx)"
129
+ @showMoreActions="handleShowMoreActions"
130
+ />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
  </div>
132
  </div>
133
  </div>
134
 
135
  <!-- 最多播放 -->
136
  <div v-else-if="viewMode === 'top'" class="top-view">
137
+ <div class="songs-list">
138
+ <SongItem
139
  v-for="(item, index) in topPlayedSongs"
140
  :key="item.songKey"
141
+ :song="item.song"
142
+ :index="index"
143
+ :show-actions="true"
144
+ :play-count="item.count"
145
+ @play="playTopSong"
146
+ @showMoreActions="handleShowMoreActions"
147
+ />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
  </div>
149
  </div>
150
  </div>
 
180
  import { imageCacheManager } from '@/utils/imageCache'
181
  import MoreActionsPanel from '@/components/player/MoreActionsPanel.vue'
182
  import ConfirmDialog from '@/components/common/ConfirmDialog.vue'
183
+ import SongItem from '@/components/search/SongItem.vue'
184
 
185
  const router = useRouter()
186
  const historyStore = useHistoryStore()
 
190
  const toastStore = useToastStore()
191
 
192
  // 响应式数据
193
+ const viewMode = ref('top') // 'recent', 'grouped', 'top' - 默认显示最多播放
194
  const searchKeyword = ref('')
195
  const selectedSong = ref(null)
196
  const showMoreActions = ref(false)
197
  const moreActionsRef = ref(null)
198
  const confirmDialogRef = ref(null)
 
199
 
200
  // 确认对话框状态
201
  const confirmDialog = ref({
 
212
  const totalPlays = computed(() => historyStore.totalPlays)
213
  const uniqueSongs = computed(() => historyStore.uniqueSongs)
214
  const isEmpty = computed(() => historyStore.isEmpty)
215
+ const currentSong = computed(() => playerStore.currentSong)
216
+ const isPlaying = computed(() => playerStore.isPlaying)
217
 
218
  // 播放统计
219
  const playStats = computed(() => historyStore.getPlayStats())
220
 
221
  // 搜索后的历史记录
222
  const displayedHistory = computed(() => {
223
+ const baseHistory = viewMode.value === 'recent' || viewMode.value === 'grouped'
224
+ ? [...history.value].sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp)) // 时间逆序:最新的在前面
225
  : history.value
226
 
227
  if (!searchKeyword.value.trim()) {
228
  return baseHistory
229
  }
230
+
231
+ // 搜索时也要保持时间逆序
232
+ const searchResults = historyStore.searchHistory(searchKeyword.value)
233
+ return searchResults.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp))
234
  })
235
 
236
  // 按日期分组的历史记录
 
240
 
241
  // 最多播放的歌曲
242
  const topPlayedSongs = computed(() => {
243
+ const allTopSongs = historyStore.getTopPlayedSongs(50)
244
+
245
+ // 如果有搜索关键词,需要过滤最多播放的歌曲
246
+ if (!searchKeyword.value.trim()) {
247
+ return allTopSongs
248
+ }
249
+
250
+ const lowerKeyword = searchKeyword.value.toLowerCase().trim()
251
+ return allTopSongs.filter(item => {
252
+ const song = item.song
253
+ if (!song) return false
254
+
255
+ // 安全的字符串比较函数
256
+ const safeIncludes = (text) => {
257
+ if (!text) return false
258
+ if (typeof text === 'string') {
259
+ return text.toLowerCase().includes(lowerKeyword)
260
+ }
261
+ if (Array.isArray(text)) {
262
+ return text.some(item => {
263
+ if (typeof item === 'string') {
264
+ return item.toLowerCase().includes(lowerKeyword)
265
+ }
266
+ if (typeof item === 'object' && item?.name) {
267
+ return item.name.toLowerCase().includes(lowerKeyword)
268
+ }
269
+ return false
270
+ })
271
+ }
272
+ if (typeof text === 'object' && text?.name) {
273
+ return text.name.toLowerCase().includes(lowerKeyword)
274
+ }
275
+ return String(text).toLowerCase().includes(lowerKeyword)
276
+ }
277
+
278
+ return safeIncludes(song.name) ||
279
+ safeIncludes(song.artist) ||
280
+ safeIncludes(song.album)
281
+ })
282
  })
283
 
284
  // 方法
 
294
 
295
  const formatPlayTime = (timestamp, showTime = false) => {
296
  const date = new Date(timestamp)
297
+ const today = new Date()
298
+ const yesterday = new Date(today)
299
+ yesterday.setDate(yesterday.getDate() - 1)
300
 
301
+ // 判断日期
302
+ if (date.toDateString() === today.toDateString()) {
303
+ if (showTime) {
304
+ return date.toLocaleTimeString('zh-CN', {
305
+ hour: '2-digit',
306
+ minute: '2-digit'
307
+ })
308
+ }
309
+ return '今天 ' + date.toLocaleTimeString('zh-CN', {
310
+ hour: '2-digit',
311
+ minute: '2-digit'
312
+ })
313
+ } else if (date.toDateString() === yesterday.toDateString()) {
314
+ if (showTime) {
315
+ return date.toLocaleTimeString('zh-CN', {
316
+ hour: '2-digit',
317
+ minute: '2-digit'
318
+ })
319
+ }
320
+ return '昨天 ' + date.toLocaleTimeString('zh-CN', {
321
+ hour: '2-digit',
322
+ minute: '2-digit'
323
+ })
324
+ } else {
325
+ if (showTime) {
326
+ return date.toLocaleTimeString('zh-CN', {
327
+ hour: '2-digit',
328
+ minute: '2-digit'
329
+ })
330
+ }
331
+ return date.toLocaleDateString('zh-CN', {
332
+ month: 'short',
333
+ day: 'numeric'
334
+ }) + ' ' + date.toLocaleTimeString('zh-CN', {
335
  hour: '2-digit',
336
  minute: '2-digit'
337
  })
338
  }
 
 
 
 
 
 
339
  }
340
 
341
  const formatDate = (dateString) => {
 
357
  }
358
  }
359
 
360
+ const formatArtist = (artist) => {
361
+ if (!artist) return '未知歌手'
362
+ if (typeof artist === 'string') return artist
363
+ if (Array.isArray(artist)) {
364
+ return artist.map(a => typeof a === 'object' ? a.name : a).join('、')
 
 
365
  }
366
+ if (typeof artist === 'object' && artist.name) {
367
+ return artist.name
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
368
  }
369
+ return String(artist)
 
 
 
370
  }
371
 
372
  const switchView = (mode) => {
 
387
  return favoritesStore.isFavorite(song)
388
  }
389
 
390
+ // 检查是否是当前播放的歌曲
391
+ const isCurrentSong = (song) => {
392
+ if (!currentSong.value || !song) return false
393
+ return currentSong.value.id === song.id && currentSong.value.source === song.source
394
+ }
395
+
396
+ // 检查历史记录中同一首歌是否应该显示播放状态(只在第一个显示)
397
+ const shouldShowPlayingState = (song, index, list) => {
398
+ if (!isCurrentSong(song)) return false
399
+
400
+ // 找到同一首歌的第一个索引
401
+ const firstIndex = list.findIndex(item =>
402
+ item.song.id === song.id && item.song.source === song.source
403
+ )
404
+
405
+ return index === firstIndex
406
+ }
407
+
408
  const toggleFavorite = async (song) => {
409
  const result = await favoritesStore.toggleFavorite(song)
410
  const message = result ? '已添加到我喜欢的音乐' : '已从我喜欢的音乐中移除'
411
  toastStore.success(message)
412
  }
413
 
 
414
  const playFromHistory = async (song, index) => {
415
  const songs = displayedHistory.value.map(item => item.song)
416
  const result = playQueueStore.setQueue(songs, index)
 
435
  }
436
  }
437
 
438
+ const playTopSong = async (song, index) => {
439
+ try {
440
+ // 获取最多播放歌曲列表
441
+ const topSongs = topPlayedSongs.value.map(item => item.song)
442
+
443
+ // 设置播放列表,从选择的歌曲开始播放
444
+ const result = playQueueStore.setQueue(topSongs, index)
445
+
446
+ if (result) {
447
+ await playerStore.playSong(song)
448
+ toastStore.success(`开始播放 "${song.name}"`)
449
+ } else {
450
+ throw new Error('设置播放列表失败')
451
+ }
452
+ } catch (error) {
453
+ console.error('播放最多播放歌曲失败:', error)
454
+ toastStore.error('播放失败,请重试')
455
  }
456
  }
457
 
 
530
  overflow-y: auto;
531
  }
532
 
533
+ /* 使用标准的page-header样式 */
534
  .page-header {
535
  display: flex;
536
  align-items: center;
537
  justify-content: space-between;
538
  padding: 16px;
539
  border-bottom: 1px solid var(--border-lighter);
540
+ background: var(--bg-primary);
541
  }
542
 
543
  .page-title {
544
  display: flex;
545
  align-items: center;
546
  gap: 12px;
547
+ font-size: 20px;
548
  font-weight: 700;
549
  color: var(--text-primary);
550
  margin: 0;
 
554
  color: var(--accent-red);
555
  }
556
 
557
+ .header-stats {
558
+ display: flex;
559
+ gap: 24px;
560
+ margin: 0 20px;
561
+ }
562
+
563
+ .stat-item {
564
+ text-align: center;
565
+ }
566
+
567
+ .stat-number {
568
+ font-size: 18px;
569
+ font-weight: 700;
570
+ color: var(--accent-red);
571
+ margin-bottom: 4px;
572
+ }
573
+
574
+ .stat-label {
575
+ font-size: 12px;
576
+ color: var(--text-secondary);
577
+ }
578
+
579
  .header-actions {
580
  display: flex;
581
  gap: 8px;
 
584
  .action-btn {
585
  display: flex;
586
  align-items: center;
587
+ justify-content: center;
588
+ width: 36px;
589
+ height: 36px;
590
  border: none;
591
+ background: rgba(255, 255, 255, 0.1);
592
+ color: var(--text-secondary);
593
+ border-radius: 50%;
594
  font-size: 14px;
 
595
  cursor: pointer;
596
  transition: var(--transition-fast);
597
  }
598
 
599
  .action-btn:hover {
600
+ background: var(--accent-red);
601
+ color: white;
602
+ transform: scale(1.1);
603
  }
604
 
605
  .action-btn.danger {
606
+ color: #ff4444;
607
  }
608
 
609
  .action-btn.danger:hover {
610
+ background: #ff4444;
611
+ color: white;
612
  }
613
 
614
+ .filter-controls {
615
+ display: flex;
616
+ justify-content: center;
617
+ padding: 12px 16px;
618
+ border-bottom: 1px solid var(--border-lighter);
619
  background: var(--bg-card);
 
 
 
620
  }
621
 
622
+ .filter-tabs {
623
+ display: flex;
624
+ gap: 8px;
625
+ margin: 0;
626
+ padding: 0;
627
+ overflow: visible;
628
  }
629
 
630
+ .filter-tab {
631
+ display: flex;
632
+ align-items: center;
633
+ gap: 6px;
634
+ padding: 8px 16px;
635
+ border: none;
636
+ background: rgba(255, 255, 255, 0.1);
637
+ color: var(--text-secondary);
638
+ border-radius: 20px;
639
+ font-size: 14px;
640
+ cursor: pointer;
641
+ white-space: nowrap;
642
+ transition: var(--transition-fast);
643
  }
644
 
645
+ .filter-tab:hover {
646
+ background: rgba(255, 255, 255, 0.2);
647
+ color: var(--text-primary);
648
+ }
649
+
650
+ .filter-tab.active {
651
+ background: var(--accent-red);
652
+ color: white;
653
+ }
654
+
655
+ .content-area {
656
+ flex: 1;
657
+ min-height: 0;
658
  }
659
 
660
  .filter-section {
 
663
 
664
  .filter-tabs {
665
  display: flex;
666
+ justify-content: center;
667
  gap: 8px;
668
  margin-bottom: 16px;
669
  overflow-x: auto;
 
737
  color: var(--text-primary);
738
  }
739
 
 
 
 
 
 
740
  .empty-state {
741
  display: flex;
742
  flex-direction: column;
 
784
  transform: scale(1.05);
785
  }
786
 
787
+ .songs-list {
788
+ background: var(--bg-card);
789
+ margin: 0;
790
+ overflow: hidden;
791
  padding-bottom: calc(var(--mini-player-height) + var(--tabbar-height) + 20px);
792
  }
793
 
 
799
  border-bottom: 1px solid var(--border-lighter);
800
  background: var(--bg-card);
801
  transition: var(--transition-fast);
802
+ width: 100%;
803
+ box-sizing: border-box;
804
  }
805
 
806
  .history-item:hover,
 
808
  background: var(--bg-gradient-1);
809
  }
810
 
811
+ .history-item.currently-playing,
812
+ .top-item.currently-playing {
813
+ background: var(--bg-gradient-2);
814
+ border-left: 4px solid var(--accent-red);
815
+ }
816
+
817
  .rank-badge {
818
  width: 32px;
819
  height: 32px;
 
829
  flex-shrink: 0;
830
  }
831
 
832
+ .top-item-wrapper {
833
+ display: flex;
834
+ align-items: center;
835
+ background: var(--bg-card);
836
+ border-bottom: 1px solid var(--border-lighter);
837
+ }
838
+
839
+ .top-item-wrapper .favorite-item {
840
+ flex: 1;
841
+ border-bottom: none;
842
+ }
843
+
844
  .item-content {
845
  display: flex;
846
  align-items: center;
847
  flex: 1;
848
  cursor: pointer;
849
  gap: 12px;
850
+ min-width: 0;
851
+ overflow: hidden;
852
  }
853
 
854
  .song-cover {
 
889
  font-size: 16px;
890
  }
891
 
892
+ .playing-indicator {
893
+ width: 36px;
894
+ height: 36px;
895
+ border-radius: 50%;
896
+ background: var(--accent-red);
897
+ display: flex;
898
+ align-items: center;
899
+ justify-content: center;
900
+ box-shadow: 0 2px 8px rgba(255, 107, 107, 0.3);
901
+ margin-right: 8px;
902
+ flex-shrink: 0;
903
+ }
904
+
905
+ .playing-indicator i {
906
+ color: white;
907
+ font-size: 16px;
908
+ }
909
+
910
+ /* 动态声音波形 */
911
+ .sound-waves {
912
+ display: flex;
913
+ align-items: center;
914
+ gap: 1px;
915
+ height: 12px;
916
+ }
917
+
918
+ .sound-bar {
919
+ width: 2px;
920
+ height: 6px;
921
+ background: white;
922
+ border-radius: 1px;
923
+ animation: sound-wave 1.2s ease-in-out infinite;
924
+ }
925
+
926
+ .sound-bar:nth-child(1) {
927
+ animation-delay: 0s;
928
+ height: 8px;
929
+ }
930
+
931
+ .sound-bar:nth-child(2) {
932
+ animation-delay: 0.2s;
933
+ height: 12px;
934
+ }
935
+
936
+ .sound-bar:nth-child(3) {
937
+ animation-delay: 0.4s;
938
+ height: 6px;
939
+ }
940
+
941
+ @keyframes sound-wave {
942
+ 0%, 100% {
943
+ opacity: 0.4;
944
+ transform: scaleY(0.6);
945
+ }
946
+ 50% {
947
+ opacity: 1;
948
+ transform: scaleY(1);
949
+ }
950
+ }
951
+
952
+ @keyframes pulse {
953
+ 0%, 100% { opacity: 1; }
954
+ 50% { opacity: 0.7; }
955
+ }
956
+
957
  .song-info {
958
  flex: 1;
959
  min-width: 0;
960
+ overflow: hidden;
961
  }
962
 
963
  .song-name {
 
968
  overflow: hidden;
969
  text-overflow: ellipsis;
970
  white-space: nowrap;
971
+ width: 100%;
972
  }
973
 
974
  .song-meta {
 
978
  overflow: hidden;
979
  text-overflow: ellipsis;
980
  white-space: nowrap;
981
+ width: 100%;
982
  }
983
 
984
  .play-time,
 
992
  display: flex;
993
  gap: 8px;
994
  flex-shrink: 0;
995
+ margin-left: auto;
996
+ padding-left: 12px;
997
  }
998
 
999
  .item-actions .action-btn {
 
1109
  }
1110
  }
1111
 
1112
+ /* PC端适配 */
1113
+ @media (min-width: 1024px) {
1114
  .history-page {
1115
+ height: 100vh;
1116
+ overflow: hidden;
1117
+ display: flex;
1118
+ flex-direction: column;
1119
+ }
1120
+
1121
+ .page-header {
1122
+ display: flex;
1123
+ align-items: center;
1124
+ justify-content: space-between;
1125
+ padding: 16px;
1126
+ border-bottom: 1px solid var(--border-lighter);
1127
+ background: var(--bg-primary);
1128
+ }
1129
+
1130
+ .page-title {
1131
+ display: flex;
1132
+ align-items: center;
1133
+ gap: 12px;
1134
+ font-size: 20px;
1135
+ font-weight: 700;
1136
+ color: var(--text-primary);
1137
+ margin: 0;
1138
+ }
1139
+
1140
+ .page-title i {
1141
+ color: var(--accent-red);
1142
+ }
1143
+
1144
+ .header-search {
1145
+ flex: 0 0 300px;
1146
+ }
1147
+
1148
+ .header-search .search-box {
1149
+ background: var(--bg-secondary);
1150
+ border: 1px solid var(--border-light);
1151
+ }
1152
+
1153
+ .header-actions {
1154
+ display: flex;
1155
+ gap: 8px;
1156
+ }
1157
+
1158
+ .action-btn {
1159
+ display: flex;
1160
+ align-items: center;
1161
+ justify-content: center;
1162
+ width: 36px;
1163
+ height: 36px;
1164
+ border: none;
1165
+ background: rgba(255, 255, 255, 0.1);
1166
+ color: var(--text-secondary);
1167
+ border-radius: 50%;
1168
+ font-size: 14px;
1169
+ cursor: pointer;
1170
+ transition: var(--transition-fast);
1171
+ }
1172
+
1173
+ .action-btn:hover {
1174
+ background: var(--accent-red);
1175
+ color: white;
1176
+ transform: scale(1.1);
1177
+ }
1178
+
1179
+ .action-btn.danger {
1180
+ color: #ff4444;
1181
+ }
1182
+
1183
+ .action-btn.danger:hover {
1184
+ background: #ff4444;
1185
+ color: white;
1186
+ }
1187
+
1188
+ .stats-and-filters {
1189
+ flex: 1;
1190
+ display: flex;
1191
+ align-items: center;
1192
+ justify-content: center;
1193
+ gap: 32px;
1194
+ margin: 0 32px;
1195
+ background: transparent;
1196
+ border: none;
1197
  }
1198
 
1199
  .stats-grid {
1200
+ display: flex;
1201
+ gap: 24px;
1202
+ margin: 0;
1203
+ padding: 0;
1204
+ border: none;
1205
+ border-radius: 0;
1206
+ background: transparent;
1207
+ }
1208
+
1209
+ .stat-card {
1210
+ text-align: center;
1211
+ padding: 0;
1212
+ }
1213
+
1214
+ .stat-number {
1215
+ font-size: 18px;
1216
+ font-weight: 700;
1217
+ color: var(--accent-red);
1218
+ margin-bottom: 4px;
1219
+ }
1220
+
1221
+ .stat-label {
1222
+ font-size: 12px;
1223
+ color: var(--text-secondary);
1224
+ }
1225
+
1226
+ .filter-tabs {
1227
+ display: flex;
1228
+ gap: 8px;
1229
+ margin: 0;
1230
+ padding: 0;
1231
+ overflow: visible;
1232
+ }
1233
+
1234
+ .filter-tab {
1235
+ display: flex;
1236
+ align-items: center;
1237
+ gap: 6px;
1238
+ padding: 8px 16px;
1239
+ border: none;
1240
+ background: rgba(255, 255, 255, 0.1);
1241
+ color: var(--text-secondary);
1242
+ border-radius: 20px;
1243
+ font-size: 14px;
1244
+ cursor: pointer;
1245
+ white-space: nowrap;
1246
+ transition: var(--transition-fast);
1247
+ }
1248
+
1249
+ .filter-tab:hover {
1250
+ background: rgba(255, 255, 255, 0.2);
1251
+ color: var(--text-primary);
1252
+ }
1253
+
1254
+ .filter-tab.active {
1255
+ background: var(--accent-red);
1256
+ color: white;
1257
  }
1258
 
1259
+ .filter-section {
1260
+ display: none;
1261
+ }
1262
+
1263
+ .content-area {
1264
+ flex: 1;
1265
+ overflow: hidden;
1266
+ display: flex;
1267
+ flex-direction: column;
1268
+ }
1269
+
1270
+ .recent-view,
1271
+ .grouped-view,
1272
+ .top-view {
1273
+ flex: 1;
1274
+ overflow: hidden;
1275
+ display: flex;
1276
+ flex-direction: column;
1277
+ }
1278
+
1279
+ .empty-state {
1280
+ flex: 1;
1281
+ padding: 80px 60px;
1282
+ margin: 0;
1283
+ background: var(--bg-primary);
1284
+ border: none;
1285
+ border-radius: 0;
1286
+ justify-content: center;
1287
+ }
1288
+
1289
+ .empty-state i {
1290
+ font-size: 80px;
1291
+ }
1292
+
1293
+ .empty-state p {
1294
+ font-size: 18px;
1295
+ }
1296
+
1297
+ .empty-tip {
1298
+ font-size: 16px;
1299
+ }
1300
+
1301
+ .songs-list {
1302
+ flex: 1;
1303
+ overflow-y: auto;
1304
+ padding: 0;
1305
+ background: var(--bg-primary);
1306
+ }
1307
+
1308
+ .date-group {
1309
+ margin-bottom: 0;
1310
+ }
1311
+
1312
+ .group-header {
1313
+ position: sticky;
1314
+ top: 0;
1315
+ z-index: 10;
1316
+ background: var(--bg-secondary);
1317
+ backdrop-filter: blur(10px);
1318
+ }
1319
+
1320
+ /* 修复歌曲名溢出问题 */
1321
+ .history-item,
1322
+ .top-item {
1323
+ padding: 12px 16px;
1324
+ width: 100%;
1325
+ box-sizing: border-box;
1326
+ display: flex;
1327
+ align-items: center;
1328
+ }
1329
+
1330
+ .item-content {
1331
+ flex: 1;
1332
+ min-width: 0;
1333
+ display: flex;
1334
+ align-items: center;
1335
+ gap: 12px;
1336
+ overflow: hidden;
1337
+ }
1338
+
1339
+ .song-info {
1340
+ flex: 1;
1341
+ min-width: 0;
1342
+ overflow: hidden;
1343
+ }
1344
+
1345
+ .song-name {
1346
+ overflow: hidden;
1347
+ text-overflow: ellipsis;
1348
+ white-space: nowrap;
1349
+ width: 100%;
1350
+ max-width: 100%;
1351
+ }
1352
+
1353
+ .song-meta {
1354
+ overflow: hidden;
1355
+ text-overflow: ellipsis;
1356
+ white-space: nowrap;
1357
+ width: 100%;
1358
+ max-width: 100%;
1359
+ }
1360
+
1361
+ .item-actions {
1362
+ flex-shrink: 0;
1363
+ margin-left: auto;
1364
+ padding-left: 12px;
1365
+ display: flex;
1366
+ gap: 8px;
1367
  }
1368
  }
1369
  </style>
src/views/HomePage.vue CHANGED
@@ -2,7 +2,7 @@
2
  <div class="home-page">
3
  <!-- 搜索区域 -->
4
  <div class="search-section">
5
- <SearchBox @search="handleSearch" />
6
  </div>
7
 
8
  <!-- 主内容区域 -->
@@ -15,6 +15,7 @@
15
  @play="handlePlay"
16
  @loadMore="handleLoadMore"
17
  @showMoreActions="handleShowMoreActions"
 
18
  />
19
  </div>
20
 
@@ -53,6 +54,7 @@ const hasSearched = ref(false)
53
  const lastSearchKeyword = ref('')
54
  const showMoreActions = ref(false)
55
  const selectedSong = ref(null)
 
56
 
57
  // 计算属性
58
  const searchResults = computed(() => searchStore.searchResults)
@@ -91,6 +93,18 @@ const retrySearch = () => {
91
  }
92
  }
93
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  const handlePlay = async (song, index) => {
95
  try {
96
  // SOLID原则:单一职责 - playQueueStore负责队列管理,playerStore负责播放控制
@@ -297,24 +311,75 @@ onMounted(() => {
297
 
298
  @media (min-width: 1024px) {
299
  .home-page {
300
- flex-direction: row;
301
- gap: 24px;
302
- padding: 24px;
303
- max-width: 1400px;
 
 
 
 
304
  }
305
 
306
  .search-section {
307
- width: 350px;
308
  flex-shrink: 0;
309
- border-right: 1px solid rgba(255, 255, 255, 0.1);
310
- border-bottom: none;
311
- padding: 24px;
312
- min-height: 100px;
 
 
 
 
 
313
  }
314
 
315
  .content-section {
316
  flex: 1;
 
 
317
  border-top: none;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
318
  }
319
  }
320
  </style>
 
2
  <div class="home-page">
3
  <!-- 搜索区域 -->
4
  <div class="search-section">
5
+ <SearchBox @search="handleSearch" ref="searchBoxRef" />
6
  </div>
7
 
8
  <!-- 主内容区域 -->
 
15
  @play="handlePlay"
16
  @loadMore="handleLoadMore"
17
  @showMoreActions="handleShowMoreActions"
18
+ @clearResults="handleClearResults"
19
  />
20
  </div>
21
 
 
54
  const lastSearchKeyword = ref('')
55
  const showMoreActions = ref(false)
56
  const selectedSong = ref(null)
57
+ const searchBoxRef = ref(null)
58
 
59
  // 计算属性
60
  const searchResults = computed(() => searchStore.searchResults)
 
93
  }
94
  }
95
 
96
+ const handleClearResults = () => {
97
+ // 清除搜索相关状态
98
+ hasSearched.value = false
99
+ lastSearchKeyword.value = ''
100
+ searchError.value = ''
101
+
102
+ // 清除搜索框关键字
103
+ if (searchBoxRef.value && searchBoxRef.value.clearKeyword) {
104
+ searchBoxRef.value.clearKeyword()
105
+ }
106
+ }
107
+
108
  const handlePlay = async (song, index) => {
109
  try {
110
  // SOLID原则:单一职责 - playQueueStore负责队列管理,playerStore负责播放控制
 
311
 
312
  @media (min-width: 1024px) {
313
  .home-page {
314
+ height: 100vh;
315
+ overflow: hidden;
316
+ display: flex;
317
+ flex-direction: column;
318
+ padding: 0;
319
+ max-width: 100%;
320
+ border: none;
321
+ box-shadow: none;
322
  }
323
 
324
  .search-section {
 
325
  flex-shrink: 0;
326
+ background: var(--bg-card);
327
+ backdrop-filter: blur(20px);
328
+ border-bottom: 1px solid var(--border-light);
329
+ padding: 20px 32px;
330
+ min-height: 80px;
331
+ position: relative;
332
+ top: 0;
333
+ z-index: 10;
334
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
335
  }
336
 
337
  .content-section {
338
  flex: 1;
339
+ padding: 0;
340
+ overflow: hidden;
341
  border-top: none;
342
+ background: var(--bg-primary);
343
+ display: flex;
344
+ flex-direction: column;
345
+ }
346
+
347
+ /* 修复搜索结果中歌曲名溢出问题 */
348
+ .content-section .song-item {
349
+ display: flex;
350
+ align-items: center;
351
+ padding: 12px 16px;
352
+ }
353
+
354
+ .content-section .item-content {
355
+ flex: 1;
356
+ min-width: 0;
357
+ display: flex;
358
+ align-items: center;
359
+ gap: 12px;
360
+ }
361
+
362
+ .content-section .song-info {
363
+ flex: 1;
364
+ min-width: 0;
365
+ overflow: hidden;
366
+ }
367
+
368
+ .content-section .song-name {
369
+ overflow: hidden;
370
+ text-overflow: ellipsis;
371
+ white-space: nowrap;
372
+ }
373
+
374
+ .content-section .song-meta {
375
+ overflow: hidden;
376
+ text-overflow: ellipsis;
377
+ white-space: nowrap;
378
+ }
379
+
380
+ .content-section .item-actions {
381
+ flex-shrink: 0;
382
+ margin-left: 12px;
383
  }
384
  }
385
  </style>
src/views/PlayQueuePage.vue CHANGED
@@ -17,7 +17,7 @@
17
  </div>
18
 
19
  <!-- 搜索框 -->
20
- <div v-if="!isEmpty" class="search-section">
21
  <div class="search-box">
22
  <i class="fas fa-search"></i>
23
  <input
@@ -447,12 +447,14 @@ onMounted(() => {
447
  overflow-y: auto;
448
  }
449
 
 
450
  .page-header {
451
  display: flex;
452
  align-items: center;
453
  justify-content: space-between;
454
  padding: 16px;
455
  border-bottom: 1px solid var(--border-lighter);
 
456
  }
457
 
458
  .page-title {
@@ -494,12 +496,20 @@ onMounted(() => {
494
  transform: translateY(-1px);
495
  }
496
 
497
- .action-btn.danger {
498
- background: #ff4444;
 
499
  }
500
 
501
- .action-btn.danger:hover {
502
- background: #ff6666;
 
 
 
 
 
 
 
503
  }
504
 
505
  .current-playing {
@@ -920,22 +930,119 @@ onMounted(() => {
920
  }
921
  }
922
 
923
- @media (min-width: 768px) {
 
924
  .play-queue-page {
925
- max-width: 1200px;
926
- margin: 0 auto;
 
 
927
  }
928
-
929
- .current-playing,
930
- .queue-stats {
931
- max-width: 800px;
932
- margin-left: auto;
933
- margin-right: auto;
 
 
934
  }
935
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
936
  .queue-list {
937
- max-width: 800px;
938
- margin: 0 auto;
 
 
939
  }
940
  }
941
  </style>
 
17
  </div>
18
 
19
  <!-- 搜索框 -->
20
+ <div v-if="!isEmpty" class="search-section hidden-desktop">
21
  <div class="search-box">
22
  <i class="fas fa-search"></i>
23
  <input
 
447
  overflow-y: auto;
448
  }
449
 
450
+ /* 使用标准的page-header样式 */
451
  .page-header {
452
  display: flex;
453
  align-items: center;
454
  justify-content: space-between;
455
  padding: 16px;
456
  border-bottom: 1px solid var(--border-lighter);
457
+ background: var(--bg-primary);
458
  }
459
 
460
  .page-title {
 
496
  transform: translateY(-1px);
497
  }
498
 
499
+ .action-btn.active {
500
+ background: var(--accent-red);
501
+ color: white;
502
  }
503
 
504
+ .content-area {
505
+ flex: 1;
506
+ min-height: 0;
507
+ }
508
+
509
+ .queue-list {
510
+ background: var(--bg-card);
511
+ margin: 0;
512
+ overflow: hidden;
513
  }
514
 
515
  .current-playing {
 
930
  }
931
  }
932
 
933
+ /* PC端适配 */
934
+ @media (min-width: 1024px) {
935
  .play-queue-page {
936
+ height: 100vh;
937
+ overflow: hidden;
938
+ display: flex;
939
+ flex-direction: column;
940
  }
941
+
942
+ .page-header {
943
+ display: flex;
944
+ align-items: center;
945
+ justify-content: space-between;
946
+ padding: 16px;
947
+ border-bottom: 1px solid var(--border-lighter);
948
+ background: var(--bg-primary);
949
  }
950
+
951
+ .page-title {
952
+ display: flex;
953
+ align-items: center;
954
+ gap: 12px;
955
+ font-size: 24px;
956
+ font-weight: 700;
957
+ color: var(--text-primary);
958
+ margin: 0;
959
+ }
960
+
961
+ .page-title i {
962
+ color: var(--accent-red);
963
+ }
964
+
965
+ .header-search {
966
+ flex: 0 0 300px;
967
+ }
968
+
969
+ .header-search .search-box {
970
+ background: var(--bg-secondary);
971
+ border: 1px solid var(--border-light);
972
+ }
973
+
974
+ .header-actions {
975
+ display: flex;
976
+ gap: 8px;
977
+ }
978
+
979
+ .action-btn {
980
+ display: flex;
981
+ align-items: center;
982
+ gap: 6px;
983
+ padding: 8px 16px;
984
+ border: none;
985
+ background: var(--accent-red);
986
+ color: white;
987
+ border-radius: 20px;
988
+ font-size: 14px;
989
+ font-weight: 500;
990
+ cursor: pointer;
991
+ transition: var(--transition-fast);
992
+ }
993
+
994
+ .action-btn:hover {
995
+ background: var(--accent-red-hover);
996
+ transform: translateY(-1px);
997
+ }
998
+
999
+ .search-section {
1000
+ display: none;
1001
+ }
1002
+
1003
+ .batch-actions {
1004
+ flex-shrink: 0;
1005
+ padding: 16px 32px;
1006
+ margin: 0;
1007
+ border-radius: 0;
1008
+ background: var(--bg-secondary);
1009
+ border-bottom: 1px solid var(--border-light);
1010
+ }
1011
+
1012
+ .content-area {
1013
+ flex: 1;
1014
+ overflow: hidden;
1015
+ display: flex;
1016
+ flex-direction: column;
1017
+ }
1018
+
1019
+ .empty-state {
1020
+ flex: 1;
1021
+ padding: 80px 60px;
1022
+ margin: 0;
1023
+ background: var(--bg-primary);
1024
+ border: none;
1025
+ border-radius: 0;
1026
+ justify-content: center;
1027
+ }
1028
+
1029
+ .empty-state i {
1030
+ font-size: 80px;
1031
+ }
1032
+
1033
+ .empty-state p {
1034
+ font-size: 18px;
1035
+ }
1036
+
1037
+ .empty-tip {
1038
+ font-size: 16px;
1039
+ }
1040
+
1041
  .queue-list {
1042
+ flex: 1;
1043
+ overflow-y: auto;
1044
+ padding: 0;
1045
+ background: var(--bg-primary);
1046
  }
1047
  }
1048
  </style>
src/views/PlaylistDetailPage.vue CHANGED
@@ -917,4 +917,63 @@ watch(() => route.params.id, () => {
917
  padding: 10px 12px;
918
  }
919
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
920
  </style>
 
917
  padding: 10px 12px;
918
  }
919
  }
920
+
921
+ /* PC端适配 */
922
+ @media (min-width: 1024px) {
923
+ .playlist-detail-page {
924
+ height: 100vh;
925
+ overflow: hidden;
926
+ display: flex;
927
+ flex-direction: column;
928
+ }
929
+
930
+ .page-header {
931
+ flex-shrink: 0;
932
+ }
933
+
934
+ .play-controls {
935
+ flex-shrink: 0;
936
+ margin: 0;
937
+ padding: 16px 32px;
938
+ background: var(--bg-card);
939
+ border-bottom: 1px solid var(--border-light);
940
+ }
941
+
942
+ .songs-content {
943
+ flex: 1;
944
+ overflow: hidden;
945
+ display: flex;
946
+ flex-direction: column;
947
+ }
948
+
949
+ .songs-list {
950
+ flex: 1;
951
+ overflow-y: auto;
952
+ padding: 0;
953
+ background: var(--bg-primary);
954
+ margin: 0;
955
+ }
956
+
957
+ .empty-playlist {
958
+ flex: 1;
959
+ padding: 80px 60px;
960
+ margin: 0;
961
+ background: var(--bg-primary);
962
+ border: none;
963
+ border-radius: 0;
964
+ justify-content: center;
965
+ }
966
+
967
+ .empty-playlist i {
968
+ font-size: 80px;
969
+ }
970
+
971
+ .empty-playlist p {
972
+ font-size: 18px;
973
+ }
974
+
975
+ .empty-tip {
976
+ font-size: 16px;
977
+ }
978
+ }
979
  </style>
src/views/PlaylistsPage.vue CHANGED
@@ -648,14 +648,53 @@ onMounted(async () => {
648
  }
649
  }
650
 
651
- @media (min-width: 768px) {
 
652
  .playlists-page {
653
- max-width: 1200px;
654
- margin: 0 auto;
 
 
 
 
 
 
 
 
 
 
 
 
 
655
  }
656
 
657
  .playlists-list {
658
- padding-bottom: 24px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
659
  }
660
  }
661
  </style>
 
648
  }
649
  }
650
 
651
+ /* PC端适配 */
652
+ @media (min-width: 1024px) {
653
  .playlists-page {
654
+ height: 100vh;
655
+ overflow: hidden;
656
+ display: flex;
657
+ flex-direction: column;
658
+ }
659
+
660
+ .page-header {
661
+ flex-shrink: 0;
662
+ }
663
+
664
+ .playlists-content {
665
+ flex: 1;
666
+ overflow: hidden;
667
+ display: flex;
668
+ flex-direction: column;
669
  }
670
 
671
  .playlists-list {
672
+ flex: 1;
673
+ overflow-y: auto;
674
+ padding: 0;
675
+ background: var(--bg-primary);
676
+ }
677
+
678
+ .empty-state {
679
+ flex: 1;
680
+ padding: 80px 60px;
681
+ margin: 0;
682
+ background: var(--bg-primary);
683
+ border: none;
684
+ border-radius: 0;
685
+ justify-content: center;
686
+ }
687
+
688
+ .empty-state i {
689
+ font-size: 80px;
690
+ }
691
+
692
+ .empty-state p {
693
+ font-size: 18px;
694
+ }
695
+
696
+ .empty-tip {
697
+ font-size: 16px;
698
  }
699
  }
700
  </style>
src/views/SearchHistoryPage.vue CHANGED
@@ -82,15 +82,20 @@ const goBack = () => {
82
  router.back()
83
  }
84
 
85
- const selectHistoryItem = (item) => {
86
- // 跳转回首页并搜索
87
- router.push('/')
88
- // 延迟执行搜索,确保路由跳转完成
89
- setTimeout(() => {
90
- // 这里需要触发搜索
91
- // 可以通过设置 store 中的关键词,然后在首页监听这个变化
92
- searchStore.setKeyword(item)
93
- }, 100)
 
 
 
 
 
94
  }
95
 
96
  const deleteHistoryItem = (index) => {
 
82
  router.back()
83
  }
84
 
85
+ const selectHistoryItem = async (item) => {
86
+ try {
87
+ // 跳转回首页
88
+ router.push('/home')
89
+ // 延迟执行搜索,确保路由跳转完成
90
+ setTimeout(async () => {
91
+ // 直接执行搜索
92
+ await searchStore.searchMusic(item)
93
+ toastStore.success(`开始搜索:"${item}"`)
94
+ }, 100)
95
+ } catch (error) {
96
+ console.error('搜索历史项目失败:', error)
97
+ toastStore.error('搜索失败,请重试')
98
+ }
99
  }
100
 
101
  const deleteHistoryItem = (index) => {
src/views/SettingsPage.vue CHANGED
@@ -409,9 +409,9 @@
409
  <i class="fas fa-music"></i>
410
  </div>
411
  <div class="app-details">
412
- <h3>云音乐 PWA</h3>
413
  <p>版本 {{ appVersion }}</p>
414
- <p>基于 Vue 3 构建的渐进式音乐应用</p>
415
  </div>
416
  </div>
417
  </div>
@@ -1464,4 +1464,84 @@ input:checked + .slider:before {
1464
  max-width: 200px; /* 限制最大宽度 */
1465
  }
1466
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1467
  </style>
 
409
  <i class="fas fa-music"></i>
410
  </div>
411
  <div class="app-details">
412
+ <h3>云音乐</h3>
413
  <p>版本 {{ appVersion }}</p>
414
+ <p>感谢GD音乐台(music.gdstudio.xyz)提供接口支持</p>
415
  </div>
416
  </div>
417
  </div>
 
1464
  max-width: 200px; /* 限制最大宽度 */
1465
  }
1466
  }
1467
+
1468
+ /* PC端进一步优化 */
1469
+ @media (min-width: 1024px) {
1470
+ .settings-page {
1471
+ padding: 32px;
1472
+ max-width: 100%;
1473
+ height: 100vh;
1474
+ overflow-y: auto;
1475
+ }
1476
+
1477
+ .page-header {
1478
+ padding: 0 0 32px;
1479
+ border-bottom: 2px solid var(--border-light);
1480
+ margin-bottom: 40px;
1481
+ }
1482
+
1483
+ .page-title {
1484
+ font-size: 32px;
1485
+ gap: 16px;
1486
+ }
1487
+
1488
+ .settings-content {
1489
+ display: grid;
1490
+ grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
1491
+ gap: 32px;
1492
+ max-width: 1200px;
1493
+ }
1494
+
1495
+ .settings-section {
1496
+ padding: 32px;
1497
+ border-radius: 16px;
1498
+ border: 1px solid var(--border-light);
1499
+ }
1500
+
1501
+ .section-title {
1502
+ font-size: 20px;
1503
+ margin-bottom: 24px;
1504
+ }
1505
+
1506
+ .setting-item {
1507
+ padding: 20px 0;
1508
+ }
1509
+
1510
+ .setting-item:not(:last-child) {
1511
+ border-bottom: 1px solid var(--border-lighter);
1512
+ }
1513
+
1514
+ .setting-label span {
1515
+ font-size: 16px;
1516
+ }
1517
+
1518
+ .setting-desc {
1519
+ font-size: 14px;
1520
+ }
1521
+
1522
+ .pc-select {
1523
+ min-width: 160px;
1524
+ max-width: 220px;
1525
+ height: 44px;
1526
+ font-size: 14px;
1527
+ }
1528
+
1529
+ .toggle-switch {
1530
+ width: 60px;
1531
+ height: 32px;
1532
+ }
1533
+
1534
+ .toggle-switch::after {
1535
+ width: 26px;
1536
+ height: 26px;
1537
+ }
1538
+
1539
+ .toggle-switch.active::after {
1540
+ transform: translateX(28px);
1541
+ }
1542
+
1543
+ .danger-zone {
1544
+ grid-column: 1 / -1;
1545
+ }
1546
+ }
1547
  </style>
vite.config.js CHANGED
@@ -11,9 +11,9 @@ export default defineConfig({
11
  registerType: 'autoUpdate',
12
  includeAssets: ['favicon.svg', 'icons/apple-touch-icon.svg'],
13
  manifest: {
14
- name: '云音乐PWA',
15
  short_name: '云音乐',
16
- description: '基于Vue3的PWA音乐播放器,网易云音乐风格',
17
  theme_color: '#ff6b6b',
18
  background_color: '#0c0c0c',
19
  display: 'standalone',
 
11
  registerType: 'autoUpdate',
12
  includeAssets: ['favicon.svg', 'icons/apple-touch-icon.svg'],
13
  manifest: {
14
+ name: '云音乐',
15
  short_name: '云音乐',
16
+ description: 'PWA音乐播放器',
17
  theme_color: '#ff6b6b',
18
  background_color: '#0c0c0c',
19
  display: 'standalone',