hookehuyr

feat(音频播放器): 添加音频播放器组件及功能

新增音频播放器组件,支持播放控制、进度条调节、音量控制、播放列表等功能。添加了FontAwesome图标库依赖,并更新了相关路由和页面配置。
...@@ -8,6 +8,9 @@ ...@@ -8,6 +8,9 @@
8 "name": "vue-vite", 8 "name": "vue-vite",
9 "version": "0.0.0", 9 "version": "0.0.0",
10 "dependencies": { 10 "dependencies": {
11 + "@fortawesome/fontawesome-svg-core": "^6.5.1",
12 + "@fortawesome/free-solid-svg-icons": "^6.5.1",
13 + "@fortawesome/vue-fontawesome": "^3.0.5",
11 "@heroicons/vue": "^2.2.0", 14 "@heroicons/vue": "^2.2.0",
12 "@vant/touch-emulator": "^1.4.0", 15 "@vant/touch-emulator": "^1.4.0",
13 "@vant/use": "^1.6.0", 16 "@vant/use": "^1.6.0",
...@@ -830,6 +833,48 @@ ...@@ -830,6 +833,48 @@
830 "node": ">=18" 833 "node": ">=18"
831 } 834 }
832 }, 835 },
836 + "node_modules/@fortawesome/fontawesome-common-types": {
837 + "version": "6.5.1",
838 + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.1.tgz",
839 + "integrity": "sha512-GkWzv+L6d2bI5f/Vk6ikJ9xtl7dfXtoRu3YGE6nq0p/FFqA1ebMOAWg3XgRyb0I6LYyYkiAo+3/KrwuBp8xG7A==",
840 + "hasInstallScript": true,
841 + "engines": {
842 + "node": ">=6"
843 + }
844 + },
845 + "node_modules/@fortawesome/fontawesome-svg-core": {
846 + "version": "6.5.1",
847 + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.5.1.tgz",
848 + "integrity": "sha512-MfRCYlQPXoLlpem+egxjfkEuP9UQswTrlCOsknus/NcMoblTH2g0jPrapbcIb04KGA7E2GZxbAccGZfWoYgsrQ==",
849 + "hasInstallScript": true,
850 + "dependencies": {
851 + "@fortawesome/fontawesome-common-types": "6.5.1"
852 + },
853 + "engines": {
854 + "node": ">=6"
855 + }
856 + },
857 + "node_modules/@fortawesome/free-solid-svg-icons": {
858 + "version": "6.5.1",
859 + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.5.1.tgz",
860 + "integrity": "sha512-S1PPfU3mIJa59biTtXJz1oI0+KAXW6bkAb31XKhxdxtuXDiUIFsih4JR1v5BbxY7hVHsD1RKq+jRkVRaf773NQ==",
861 + "hasInstallScript": true,
862 + "dependencies": {
863 + "@fortawesome/fontawesome-common-types": "6.5.1"
864 + },
865 + "engines": {
866 + "node": ">=6"
867 + }
868 + },
869 + "node_modules/@fortawesome/vue-fontawesome": {
870 + "version": "3.0.5",
871 + "resolved": "https://registry.npmjs.org/@fortawesome/vue-fontawesome/-/vue-fontawesome-3.0.5.tgz",
872 + "integrity": "sha512-isZZ4+utQH9qg9cWxWYHQ9GwI3r5FeO7GnmzKYV+gbjxcptQhh+F99iZXi1Y9AvFUEgy8kRpAdvDlbb3drWFrw==",
873 + "peerDependencies": {
874 + "@fortawesome/fontawesome-svg-core": "~1 || ~6",
875 + "vue": ">= 3.0.0 < 4"
876 + }
877 + },
833 "node_modules/@heroicons/vue": { 878 "node_modules/@heroicons/vue": {
834 "version": "2.2.0", 879 "version": "2.2.0",
835 "resolved": "https://registry.npmjs.org/@heroicons/vue/-/vue-2.2.0.tgz", 880 "resolved": "https://registry.npmjs.org/@heroicons/vue/-/vue-2.2.0.tgz",
......
...@@ -15,6 +15,9 @@ ...@@ -15,6 +15,9 @@
15 "dev_upload": "npm run build_tar && npm run scp-dev && npm run dec-dev && npm run remove_tar" 15 "dev_upload": "npm run build_tar && npm run scp-dev && npm run dec-dev && npm run remove_tar"
16 }, 16 },
17 "dependencies": { 17 "dependencies": {
18 + "@fortawesome/fontawesome-svg-core": "^6.5.1",
19 + "@fortawesome/free-solid-svg-icons": "^6.5.1",
20 + "@fortawesome/vue-fontawesome": "^3.0.5",
18 "@heroicons/vue": "^2.2.0", 21 "@heroicons/vue": "^2.2.0",
19 "@vant/touch-emulator": "^1.4.0", 22 "@vant/touch-emulator": "^1.4.0",
20 "@vant/use": "^1.6.0", 23 "@vant/use": "^1.6.0",
......
...@@ -10,6 +10,7 @@ declare module 'vue' { ...@@ -10,6 +10,7 @@ declare module 'vue' {
10 export interface GlobalComponents { 10 export interface GlobalComponents {
11 ActivityCard: typeof import('./components/ui/ActivityCard.vue')['default'] 11 ActivityCard: typeof import('./components/ui/ActivityCard.vue')['default']
12 AppLayout: typeof import('./components/layout/AppLayout.vue')['default'] 12 AppLayout: typeof import('./components/layout/AppLayout.vue')['default']
13 + AudioPlayer: typeof import('./components/ui/AudioPlayer.vue')['default']
13 BottomNav: typeof import('./components/layout/BottomNav.vue')['default'] 14 BottomNav: typeof import('./components/layout/BottomNav.vue')['default']
14 CheckInDialog: typeof import('./components/ui/CheckInDialog.vue')['default'] 15 CheckInDialog: typeof import('./components/ui/CheckInDialog.vue')['default']
15 ConfirmDialog: typeof import('./components/ui/ConfirmDialog.vue')['default'] 16 ConfirmDialog: typeof import('./components/ui/ConfirmDialog.vue')['default']
......
1 +<!--
2 + * @Date: 2025-04-07 12:35:35
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2025-04-07 14:30:26
5 + * @FilePath: /mlaj/src/components/ui/audioPlayer.vue
6 + * @Description: 音频播放器组件,支持播放控制、进度条调节、音量控制、播放列表等功能
7 +-->
8 +<template>
9 + <!-- 音频播放器主容器 -->
10 + <div class="audio-player bg-white rounded-lg shadow-xl overflow-hidden max-w-3xl mx-auto p-4 sm:p-6">
11 + <!-- 封面与控制区:显示当前播放歌曲的封面、标题、艺术家和播放控制按钮 -->
12 + <div class="flex flex-col sm:flex-row items-center space-y-4 sm:space-y-0 sm:space-x-4">
13 + <!-- 歌曲封面 -->
14 + <div class="w-32 h-32 sm:w-24 sm:h-24 rounded-lg overflow-hidden">
15 + <img :src="currentSong.cover" alt="封面" class="w-full h-full object-cover">
16 + </div>
17 +
18 + <!-- 歌曲信息 -->
19 + <div class="flex-1 text-center sm:text-left">
20 + <h3 class="text-xl sm:text-lg font-medium">{{ currentSong.title }}</h3>
21 + <p class="text-sm text-gray-500">{{ currentSong.artist }}</p>
22 + </div>
23 +
24 + <!-- 播放控制按钮组:上一首、播放/暂停、下一首 -->
25 + <div class="flex items-center justify-center space-x-8 sm:space-x-6 w-full sm:w-auto">
26 + <button
27 + @click="prevSong"
28 + class="w-12 h-12 sm:w-10 sm:h-10 flex items-center justify-center rounded-full bg-gray-100 hover:bg-gray-200 transition-colors"
29 + >
30 + <font-awesome-icon icon="backward-step" class="text-xl text-gray-600" />
31 + </button>
32 + <button
33 + @click="togglePlay"
34 + :class="{'paused': !isPlaying, 'opacity-50 cursor-not-allowed': isLoading}"
35 + :disabled="isLoading"
36 + class="w-16 h-16 sm:w-14 sm:h-14 flex items-center justify-center rounded-full bg-blue-500 hover:bg-blue-600 transition-colors shadow-lg"
37 + >
38 + <font-awesome-icon
39 + :icon="['fas' , isPlaying ? 'pause' : 'play']"
40 + :class="{ 'fa-spin': isLoading }"
41 + class="text-3xl"
42 + />
43 + </button>
44 + <button
45 + @click="nextSong"
46 + class="w-12 h-12 sm:w-10 sm:h-10 flex items-center justify-center rounded-full bg-gray-100 hover:bg-gray-200 transition-colors"
47 + >
48 + <font-awesome-icon icon="forward-step" class="text-xl text-gray-600" />
49 + </button>
50 + </div>
51 + </div>
52 +
53 + <!-- 进度条与时间:显示当前播放时间、总时长和可拖动的进度条 -->
54 + <div class="mt-4">
55 + <div class="flex items-center justify-between text-sm text-gray-500">
56 + <span>{{ formatTime(currentTime) }}</span>
57 + <span>{{ formatTime(duration) }}</span>
58 + </div>
59 +
60 + <div class="progress-bar relative mt-2">
61 + <input
62 + type="range"
63 + :value="progress"
64 + @input="handleProgressChange"
65 + @change="seekTo"
66 + class="w-full appearance-none bg-gray-200 rounded-full h-1.5 cursor-pointer"
67 + >
68 + <div
69 + :style="{ width: `${progress}%` }"
70 + class="progress-track absolute left-0 top-0 h-full rounded-full bg-gradient-to-r from-blue-500 to-purple-500 transition-all"
71 + ></div>
72 + </div>
73 + </div>
74 +
75 + <!-- 音量与设置:音量控制滑块和播放列表按钮 -->
76 + <div class="flex items-center justify-between mt-6">
77 + <div class="flex items-center space-x-2">
78 + <font-awesome-icon :icon="volume === 0 ? 'volume-off' : 'volume-up'" />
79 + <input
80 + type="range"
81 + :value="volume"
82 + @input="handleVolumeChange"
83 + min="0"
84 + max="100"
85 + step="1"
86 + class="w-24 appearance-none bg-gray-200 rounded-full h-1.5 cursor-pointer"
87 + >
88 + </div>
89 +
90 + <div class="flex items-center space-x-4">
91 + <!-- 播放列表按钮 -->
92 + <button @click="togglePlaylist"><font-awesome-icon icon="list" /></button>
93 + </div>
94 + </div>
95 +
96 + <!-- 播放列表:可切换显示的歌曲列表面板 -->
97 + <div v-show="isPlaylistVisible" class="playlist mt-4 overscroll-contain">
98 + <div class="playlist-header flex justify-between items-center px-2 py-1 bg-gray-100 rounded-t-lg">
99 + <h4 class="font-medium">播放列表 ({{ songs.length }})</h4>
100 + <button @click="closePlaylist"><font-awesome-icon icon="xmark" /></button>
101 + </div>
102 + <div class="playlist-items" style="max-height: 16rem; overflow-y: auto; -webkit-overflow-scrolling: touch;">
103 + <div
104 + v-for="(song, index) in songs"
105 + :key="song.id"
106 + :class="{'active': index === currentIndex}"
107 + @click="selectSong(index)"
108 + class="px-2 py-3 hover:bg-gray-100 transition-colors"
109 + >
110 + {{ song.title }} - {{ song.artist }}
111 + </div>
112 + </div>
113 + </div>
114 + </div>
115 +</template>
116 +
117 +<script setup>
118 +import { ref, computed, onMounted, watch } from 'vue'
119 +import { formatTime } from '@/utils/time'
120 +
121 +// 组件属性定义
122 +const props = defineProps({
123 + songs: { type: Array, required: true }, // 音频列表数据
124 + initialIndex: { type: Number, default: 0 } // 初始播放索引
125 +})
126 +
127 +// 音频播放器状态管理
128 +const audio = ref(null) // 音频实例
129 +const isPlaying = ref(false) // 播放状态
130 +const isLoading = ref(false) // 加载状态
131 +const currentIndex = ref(props.initialIndex) // 当前播放索引
132 +const currentTime = ref(0) // 当前播放时间
133 +const duration = ref(0) // 音频总时长
134 +const progress = ref(0) // 播放进度
135 +const volume = ref(100) // 音量值
136 +const isPlaylistVisible = ref(false) // 播放列表显示状态
137 +const speed = ref(1.0) // 播放速度
138 +
139 +// 计算属性
140 +const currentSong = computed(() => props.songs[currentIndex.value]) // 当前播放的歌曲
141 +
142 +// 生命周期钩子
143 +onMounted(() => {
144 + loadAudio()
145 + audio.value?.addEventListener('timeupdate', updateProgress)
146 + audio.value?.addEventListener('ended', handleEnded)
147 +})
148 +
149 +// 核心方法:音频加载
150 +const loadAudio = async () => {
151 + if (!currentSong.value) return
152 + isLoading.value = true
153 + try {
154 + if (audio.value) {
155 + audio.value.removeEventListener('timeupdate', updateProgress)
156 + audio.value.removeEventListener('ended', handleEnded)
157 + }
158 + audio.value = new Audio(currentSong.value.url)
159 + audio.value.volume = volume.value / 100
160 + audio.value.playbackRate = speed.value
161 + audio.value.addEventListener('timeupdate', updateProgress)
162 + audio.value.addEventListener('ended', handleEnded)
163 + await audio.value.load()
164 + } catch (error) {
165 + console.error('加载音频失败:', error)
166 + } finally {
167 + isLoading.value = false
168 + }
169 +}
170 +
171 +// 播放控制:切换播放/暂停状态
172 +const togglePlay = async () => {
173 + if (isLoading.value) return
174 + try {
175 + if (!audio.value) {
176 + await loadAudio()
177 + }
178 + if (isPlaying.value) {
179 + await audio.value?.pause()
180 + } else {
181 + await audio.value?.play()
182 + }
183 + isPlaying.value = !isPlaying.value
184 + } catch (error) {
185 + console.error('播放控制失败:', error)
186 + }
187 +}
188 +
189 +// 进度更新
190 +const updateProgress = () => {
191 + if (!audio.value) return
192 + currentTime.value = audio.value.currentTime
193 + duration.value = audio.value.duration
194 + progress.value = (currentTime.value / duration.value) * 100 || 0
195 +}
196 +
197 +// 播放结束处理
198 +const handleEnded = () => {
199 + nextSong()
200 +}
201 +
202 +// 控制方法:切换到上一首
203 +const prevSong = async () => {
204 + if (audio.value) {
205 + isPlaying.value = false
206 + await audio.value.pause()
207 + audio.value = null
208 + }
209 + currentIndex.value = (currentIndex.value - 1 + props.songs.length) % props.songs.length
210 + await loadAudio()
211 + if (audio.value) {
212 + await audio.value.play()
213 + // 使用非线性映射来调整音量变化的灵敏度
214 + const normalizedVolume = Math.pow(volume.value / 100, 2)
215 + audio.value.volume = normalizedVolume
216 + isPlaying.value = true
217 + }
218 +}
219 +
220 +// 控制方法:切换到下一首
221 +const nextSong = async () => {
222 + if (audio.value) {
223 + isPlaying.value = false
224 + await audio.value.pause()
225 + audio.value = null
226 + }
227 + currentIndex.value = (currentIndex.value + 1) % props.songs.length
228 + await loadAudio()
229 + if (audio.value) {
230 + await audio.value.play()
231 + // 使用非线性映射来调整音量变化的灵敏度
232 + const normalizedVolume = Math.pow(volume.value / 100, 2)
233 + audio.value.volume = normalizedVolume
234 + isPlaying.value = true
235 + }
236 +}
237 +
238 +// 重新播放当前歌曲
239 +const replaySong = () => {
240 + audio.value?.seek(0)
241 + togglePlay()
242 +}
243 +
244 +// 进度条交互处理
245 +const handleProgressChange = (e) => {
246 + const target = e.target
247 + progress.value = parseFloat(target.value)
248 +}
249 +
250 +// 跳转到指定进度
251 +const seekTo = () => {
252 + if (!audio.value || !duration.value) return
253 + audio.value.currentTime = (progress.value / 100) * duration.value
254 +}
255 +
256 +// 音量控制处理
257 +const handleVolumeChange = (e) => {
258 + const target = e.target
259 + volume.value = parseFloat(target.value)
260 + // 使用非线性映射来调整音量变化的灵敏度
261 + const normalizedVolume = Math.pow(volume.value / 100, 2)
262 + if (audio.value) {
263 + audio.value.volume = normalizedVolume
264 + }
265 +}
266 +
267 +// 监听歌曲列表变化
268 +watch(() => props.songs, () => {
269 + currentIndex.value = 0
270 + loadAudio()
271 +}, { deep: true })
272 +
273 +// 组件卸载时清理事件监听
274 +onUnmounted(() => {
275 + audio.value?.removeEventListener('timeupdate', updateProgress)
276 + audio.value?.removeEventListener('ended', handleEnded)
277 +})
278 +
279 +// 播放列表相关方法
280 +const togglePlaylist = () => {
281 + isPlaylistVisible.value = !isPlaylistVisible.value
282 +}
283 +
284 +const closePlaylist = () => {
285 + isPlaylistVisible.value = false
286 +}
287 +
288 +// 选择并播放指定歌曲
289 +const selectSong = async (index) => {
290 + if (audio.value) {
291 + isPlaying.value = false
292 + await audio.value.pause()
293 + audio.value = null
294 + }
295 + currentIndex.value = index
296 + await loadAudio()
297 + if (audio.value) {
298 + await audio.value.play()
299 + isPlaying.value = true
300 + // 滚动到当前播放的音频
301 + const playlistItems = document.querySelector('.playlist-items')
302 + const activeItem = playlistItems?.querySelector('.active')
303 + if (playlistItems && activeItem) {
304 + activeItem.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
305 + }
306 + }
307 +}
308 +
309 +// 监听播放状态变化,确保当前播放项在可视区域内
310 +watch([isPlaying, currentIndex], () => {
311 + if (isPlaying.value) {
312 + const playlistItems = document.querySelector('.playlist-items')
313 + const activeItem = playlistItems?.querySelector('.active')
314 + if (playlistItems && activeItem) {
315 + activeItem.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
316 + }
317 + }
318 +})
319 +</script>
320 +
321 +<style scoped>
322 +/* 音频播放器样式变量 */
323 +.audio-player {
324 + --progress-height: 4px;
325 +}
326 +
327 +/* 进度条样式 */
328 +.progress-bar {
329 + height: var(--progress-height);
330 + border-radius: var(--progress-height);
331 +}
332 +
333 +.progress-track {
334 + height: var(--progress-height);
335 + border-radius: var(--progress-height);
336 +}
337 +
338 +/* 按钮交互动画 */
339 +button:not(:disabled) {
340 + transition: all 0.3s ease;
341 +}
342 +
343 +@media (hover: hover) {
344 + button:not(:disabled):hover {
345 + transform: scale(1.1);
346 + }
347 +}
348 +
349 +button.paused .fa-pause {
350 + animation: pulse 1.5s infinite;
351 +}
352 +
353 +button:disabled {
354 + cursor: not-allowed;
355 +}
356 +
357 +/* 暂停按钮动画 */
358 +@keyframes pulse {
359 + 0% { transform: scale(1); }
360 + 50% { transform: scale(1.1); }
361 + 100% { transform: scale(1); }
362 +}
363 +
364 +/* 播放列表样式 */
365 +.playlist-items {
366 + border: 1px solid #e5e7eb;
367 + border-top: 0;
368 + border-radius: 0 0 0.375rem 0.375rem;
369 +}
370 +
371 +.active {
372 + background-color: #f3f4f6;
373 + font-weight: 500;
374 + color: #1a1a1a;
375 +}
376 +
377 +/* 移动端样式优化 */
378 +@media (max-width: 640px) {
379 + .audio-player {
380 + --progress-height: 6px;
381 + }
382 +
383 + input[type="range"] {
384 + height: var(--progress-height);
385 + }
386 +}
387 +</style>
1 /* 1 /*
2 * @Date: 2025-03-20 20:36:36 2 * @Date: 2025-03-20 20:36:36
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2025-03-24 23:32:16 4 + * @LastEditTime: 2025-04-07 14:24:50
5 * @FilePath: /mlaj/src/main.js 5 * @FilePath: /mlaj/src/main.js
6 * @Description: 文件描述 6 * @Description: 文件描述
7 */ 7 */
...@@ -13,10 +13,21 @@ import axios from '@/utils/axios'; ...@@ -13,10 +13,21 @@ import axios from '@/utils/axios';
13 import 'vant/lib/index.css' 13 import 'vant/lib/index.css'
14 import '@vant/touch-emulator'; 14 import '@vant/touch-emulator';
15 15
16 +/* import the fontawesome core */
17 +import { library } from '@fortawesome/fontawesome-svg-core'
18 +/* import font awesome icon component */
19 +import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
20 +/* import specific icons */
21 +import { faCirclePause, faCirclePlay, faPlay, faPause, faBackwardStep, faForwardStep, faVolumeUp, faRedo, faRepeat, faList, faChevronDown, faVolumeOff, faXmark } from '@fortawesome/free-solid-svg-icons'
22 +
23 +/* add icons to the library */
24 +library.add(faCirclePause, faCirclePlay, faPlay, faPause, faBackwardStep, faForwardStep, faVolumeUp, faRedo, faRepeat, faList, faChevronDown, faVolumeOff, faXmark)
25 +
16 const app = createApp(App) 26 const app = createApp(App)
17 // 屏蔽警告信息 27 // 屏蔽警告信息
18 app.config.warnHandler = () => null; 28 app.config.warnHandler = () => null;
19 29
20 app.config.globalProperties.$http = axios; // 关键语句 30 app.config.globalProperties.$http = axios; // 关键语句
31 +app.component('font-awesome-icon', FontAwesomeIcon)
21 app.use(router) 32 app.use(router)
22 app.mount('#app') 33 app.mount('#app')
......
...@@ -163,6 +163,12 @@ export const routes = [ ...@@ -163,6 +163,12 @@ export const routes = [
163 meta: { title: '修改密码' }, 163 meta: { title: '修改密码' },
164 }, 164 },
165 { 165 {
166 + path: '/profile/settings/audio',
167 + name: 'AudioPlayer',
168 + component: () => import('../views/profile/settings/AudioPlayerPage.vue'),
169 + meta: { title: '音频播放' },
170 + },
171 + {
166 path: '/profile/learning-records', 172 path: '/profile/learning-records',
167 name: 'LearningRecords', 173 name: 'LearningRecords',
168 component: () => import('../views/profile/LearningRecordsPage.vue'), 174 component: () => import('../views/profile/LearningRecordsPage.vue'),
......
1 +/*
2 + * @Date: 2025-04-07 12:41:59
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2025-04-07 12:42:05
5 + * @FilePath: /mlaj/src/utils/time.js
6 + * @Description: 文件描述
7 + */
8 +/**
9 + * 格式化时间戳为 mm:ss 或 hh:mm:ss 格式
10 + * @param {number} seconds - 总秒数(支持小数)
11 + * @returns {string} 格式化后的时间字符串
12 + */
13 +export function formatTime(seconds) {
14 + if (isNaN(seconds) || seconds < 0) return '0:00'
15 +
16 + const hours = Math.floor(seconds / 3600)
17 + seconds %= 3600
18 + const minutes = Math.floor(seconds / 60)
19 + seconds = Math.floor(seconds % 60)
20 +
21 + const pad = (n) => n.toString().padStart(2, '0')
22 + if (hours > 0) {
23 + return `${hours}:${pad(minutes)}:${pad(seconds)}`
24 + }
25 + return `${minutes}:${pad(seconds)}`
26 +}
...@@ -64,6 +64,17 @@ ...@@ -64,6 +64,17 @@
64 <ChevronRightIcon class="w-5 h-5 text-gray-400" /> 64 <ChevronRightIcon class="w-5 h-5 text-gray-400" />
65 </div> 65 </div>
66 </div> 66 </div>
67 +
68 + <!-- 音频播放 -->
69 + <div class="p-4" @click="router.push('/profile/settings/audio')">
70 + <div class="flex items-center justify-between">
71 + <div>
72 + <h3 class="text-base font-medium text-gray-900">音频播放</h3>
73 + <p class="text-sm text-gray-500">播放音频文件</p>
74 + </div>
75 + <ChevronRightIcon class="w-5 h-5 text-gray-400" />
76 + </div>
77 + </div>
67 </div> 78 </div>
68 </FrostedGlass> 79 </FrostedGlass>
69 </div> 80 </div>
......
1 +<!--
2 + * @Date: 2025-03-24 13:04:21
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2025-04-07 14:06:35
5 + * @FilePath: /mlaj/src/views/profile/settings/AudioPlayerPage.vue
6 + * @Description: 音频播放页面
7 +-->
8 +<template>
9 + <AppLayout>
10 + <div class="bg-gradient-to-br from-green-50 via-green-100/30 to-blue-50/30 min-h-screen">
11 + <div class="px-4 py-6">
12 + <FrostedGlass class="rounded-xl overflow-hidden">
13 + <AudioPlayer :songs="audioList" />
14 + </FrostedGlass>
15 + </div>
16 + </div>
17 + </AppLayout>
18 +</template>
19 +
20 +<script setup>
21 +import { ref } from 'vue';
22 +import AppLayout from '@/components/layout/AppLayout.vue';
23 +import FrostedGlass from '@/components/ui/FrostedGlass.vue';
24 +import AudioPlayer from '@/components/ui/AudioPlayer.vue';
25 +
26 +// 测试音频数据
27 +const audioList = ref([
28 + {
29 + id: 1,
30 + title: '示例音频 1',
31 + artist: '演唱者 1',
32 + cover: 'https://cdn.ipadbiz.cn/mlaj/images/zMRLZh40kms.jpg',
33 + url: 'https://img.tukuppt.com/newpreview_music/09/03/95/5c8af46b01eb138909.mp3'
34 + },
35 + {
36 + id: 2,
37 + title: '示例音频 2',
38 + artist: '演唱者 2',
39 + cover: 'https://cdn.ipadbiz.cn/mlaj/images/zMRLZh40kms.jpg',
40 + url: 'https://img.tukuppt.com/newpreview_music/09/03/72/5c8ad96fbf47328809.mp3'
41 + },
42 + {
43 + id: 3,
44 + title: '示例音频 3',
45 + artist: '演唱者 3',
46 + cover: 'https://cdn.ipadbiz.cn/mlaj/images/zMRLZh40kms.jpg',
47 + url: 'https://img.tukuppt.com/newpreview_music/09/03/95/5c8af46b01eb138909.mp3'
48 + },
49 + {
50 + id: 4,
51 + title: '示例音频 4',
52 + artist: '演唱者 4',
53 + cover: 'https://cdn.ipadbiz.cn/mlaj/images/zMRLZh40kms.jpg',
54 + url: 'https://img.tukuppt.com/newpreview_music/09/03/72/5c8ad96fbf47328809.mp3'
55 + },
56 + {
57 + id: 5,
58 + title: '示例音频 5',
59 + artist: '演唱者 5',
60 + cover: 'https://cdn.ipadbiz.cn/mlaj/images/zMRLZh40kms.jpg',
61 + url: 'https://img.tukuppt.com/newpreview_music/09/03/95/5c8af46b01eb138909.mp3'
62 + },
63 + {
64 + id: 6,
65 + title: '示例音频 6',
66 + artist: '演唱者 6',
67 + cover: 'https://cdn.ipadbiz.cn/mlaj/images/zMRLZh40kms.jpg',
68 + url: 'https://img.tukuppt.com/newpreview_music/09/03/72/5c8ad96fbf47328809.mp3'
69 + },
70 + {
71 + id: 7,
72 + title: '示例音频 7',
73 + artist: '演唱者 7',
74 + cover: 'https://cdn.ipadbiz.cn/mlaj/images/zMRLZh40kms.jpg',
75 + url: 'https://img.tukuppt.com/newpreview_music/09/03/95/5c8af46b01eb138909.mp3'
76 + },
77 + {
78 + id: 8,
79 + title: '示例音频 8',
80 + artist: '演唱者 8',
81 + cover: 'https://cdn.ipadbiz.cn/mlaj/images/zMRLZh40kms.jpg',
82 + url: 'https://img.tukuppt.com/newpreview_music/09/03/72/5c8ad96fbf47328809.mp3'
83 + },
84 +]);
85 +</script>
1 <!-- 1 <!--
2 - * @Date: 2025-03-20 19:53:12 2 + * @Date: 2025-03-21 13:12:37
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2025-03-20 23:56:05 4 + * @LastEditTime: 2025-04-07 12:57:28
5 - * @FilePath: /mlaj/src/App.vue 5 + * @FilePath: /mlaj/src/views/test.vue
6 * @Description: 文件描述 6 * @Description: 文件描述
7 --> 7 -->
8 -<script setup>
9 -const arr = [];
10 -arr.push({ a: '1' });
11 -arr.push({ a: '2', b: 'random1' });
12 -arr.push({ a: '3', b: 'random2', c: true });
13 -arr.push({ a: '4', b: 'random3', d: 123 });
14 -arr.push({ a: '5', b: 'random4', e: new Date() });
15 -
16 -const arr2 = arr.map(item => {
17 - return {
18 - ...item,
19 - b: 'random5'
20 - }
21 -})
22 -
23 -console.warn(arr2);
24 -</script>
25 -
26 -<template>
27 -</template>
28 -
29 -<style>
30 -
31 -</style>
......