diff --git a/public/img/audio-player/listPlay.svg b/public/img/audio-player/order-list_loop.svg similarity index 100% rename from public/img/audio-player/listPlay.svg rename to public/img/audio-player/order-list_loop.svg diff --git a/public/img/audio-player/randomPlay.svg b/public/img/audio-player/order-random.svg similarity index 100% rename from public/img/audio-player/randomPlay.svg rename to public/img/audio-player/order-random.svg diff --git a/public/img/audio-player/singleLoop.svg b/public/img/audio-player/order-single.svg similarity index 100% rename from public/img/audio-player/singleLoop.svg rename to public/img/audio-player/order-single.svg diff --git a/public/img/audio-player/listLoop.svg b/public/img/audio-player/play-list.svg similarity index 100% rename from public/img/audio-player/listLoop.svg rename to public/img/audio-player/play-list.svg diff --git a/src/components/AudioPlayer/Player.tsx b/src/components/AudioPlayer/Player.tsx index c85aefc..cba86e1 100644 --- a/src/components/AudioPlayer/Player.tsx +++ b/src/components/AudioPlayer/Player.tsx @@ -2,8 +2,6 @@ /** * 待优化: * 1. 拖动滚动条时音轨不要动,等拖动完毕后再动 - * 2. 切歌时,时间显示 - * 3. 预加载 */ import { useState, useRef, useEffect } from 'react'; @@ -14,99 +12,128 @@ import PlayerControl from './PlayerControl'; import useAudioStore from '@/store/audio'; export default function AudioPlayer({ className }: { className?: string }) { - const { audio, switchSongByDiff } = useAudioStore( + const { audioId, order, playQueue, switchSongByDiff, setOrder } = useAudioStore( useShallow((state) => { return { - audio: state.audio, - playList: state.playList, + audioId: state.audioId, + order: state.order, + playQueue: state.playQueue, + setOrder: state.setOrder, switchSongByDiff: state.switchSongByDiff, }; }), ); - // const musicPlayers = useRef(typeof Audio !== 'undefined' ? new Audio('') : undefined); - const audioRef = useRef(new Audio('')); + const audio: SongInfo = playQueue.find((item) => item.id === audioId) || playQueue[0]; + + const audioRef = useRef(new Audio(audio.src)); + const isReady = useRef(false); // 是否加载过组件 const [isPlaying, setIsPlaying] = useState(false); // 播放状态 const [trackProgress, setTrackProgress] = useState(0); // 音频进度(s) - const isFirst = useRef(false); // 第一次进入 - const { duration } = audioRef.current; // 音频总时长(s) + const [duration, setDuration] = useState(0); // 音频总时长(s) - // 播放/暂停 - const handlePlay = () => { - setIsPlaying(!isPlaying); + // 修改播放顺序 + const handleChangeOrder = () => { + // 修改 icon & hover + const orderArr: PlayOrder[] = ['list_loop', 'random', 'single']; + const index = orderArr.indexOf(order); + setOrder(orderArr[(index + 1) % 3]); }; - // 上一首 - const handlePlayPre = () => { - switchSongByDiff(-1); + // 播放/暂停 + const handlePlay = () => { + setIsPlaying((isPlaying) => !isPlaying); }; - // 下一首 - const handlePlayNext = () => { - switchSongByDiff(1); + /** + * @description 切换音乐 + * @param direction -1: 上一首 1: 下一首 + */ + const handleSwitchAudio = (direction: number) => { + if (order === 'single') { + audioRef.current.currentTime = 0; + !isPlaying && setIsPlaying(true); + return; + } + switchSongByDiff(direction); }; // 调整播放进度 const handleChangeProgress = (value: string) => { audioRef.current.currentTime = Number(value); setTrackProgress(audioRef.current.currentTime); + setIsPlaying(true); }; // 监听音频进度 const onTimeUpdate = () => { - if (audioRef.current.ended) handlePlayNext(); - setTrackProgress(audioRef.current.currentTime); - }; + // 完成播放 + if (audioRef.current.ended) { + handleSwitchAudio(1); + audioRef.current.play(); + } - // 给 audio 绑定 timeupdate 监听 - const handleTimeUpdate = (status: boolean) => { - status - ? audioRef.current.addEventListener('timeupdate', onTimeUpdate) - : audioRef.current.removeEventListener('timeupdate', onTimeUpdate); + setTrackProgress(audioRef.current.currentTime); }; - // 播放/暂停 + // 播放/暂停事件 useEffect(() => { isPlaying ? audioRef.current.play() : audioRef.current.pause(); }, [isPlaying]); // 切换歌曲 useEffect(() => { + const handleLoadedMetadata = () => { + setDuration(audioRef.current.duration); + }; + audioRef.current.pause(); - if (!audio) return; audioRef.current = new Audio(audio.src); - setTrackProgress(audioRef.current.currentTime); - console.log('id变化', { audio, isFirst: isFirst.current }); - if (isFirst.current) { + audioRef.current.addEventListener('timeupdate', onTimeUpdate); + + if (isReady.current) { + audioRef.current.addEventListener('loadedmetadata', handleLoadedMetadata); // 获取音频时长 audioRef.current.play(); setIsPlaying(true); - handleTimeUpdate(true); } else { - isFirst.current = true; + isReady.current = true; } - }, [audio]); - // 卸载时的处理 + return () => { + audioRef.current.removeEventListener('loadedmetadata', handleLoadedMetadata); + }; + }, [audioId]); + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.code === 'Space' || e.keyCode === 32) { + handlePlay(); + } + }; + + document.addEventListener('keydown', handleKeyDown); // 监听快捷键 + setIsPlaying(false); // 禁止自动播放 + return () => { audioRef.current.pause(); - handleTimeUpdate(false); + audioRef.current.removeEventListener('timeupdate', onTimeUpdate); + document.removeEventListener('keydown', handleKeyDown); }; }, []); return ( -
- -
+ handleSwitchAudio(-1)} + onNext={() => handleSwitchAudio(1)} + onOrder={handleChangeOrder} + className={className} + trackProgress={trackProgress} + onChangeProgress={handleChangeProgress} + /> ); } diff --git a/src/components/AudioPlayer/PlayerControl.tsx b/src/components/AudioPlayer/PlayerControl.tsx index 8252927..1920403 100644 --- a/src/components/AudioPlayer/PlayerControl.tsx +++ b/src/components/AudioPlayer/PlayerControl.tsx @@ -8,8 +8,10 @@ import { AutoScrollContainer } from '@/components'; interface Props { audio: SongInfo | null; + order: PlayOrder; playStatus: boolean; onPlay: () => void; + onOrder: () => void; onPrev: () => void; onNext: () => void; onChangeProgress: (value: string) => void; @@ -20,8 +22,10 @@ interface Props { export default function AudioPlayer({ audio, + order, playStatus, onPlay, + onOrder, onPrev, onNext, onChangeProgress, @@ -34,6 +38,13 @@ export default function AudioPlayer({ -webkit-gradient(linear, 0% 0%, 100% 0%, color-stop(${currentPercentage}, rgb(24,24,24)), color-stop(${currentPercentage}, rgba(0,0,0,0.1))) `; // 进度条样式 + // 播放顺序 + const orders: Record = { + list_loop: { label: '列表循环', icon: '/img/audio-player/order-list_loop.svg' }, + random: { label: '随机播放', icon: '/img/audio-player/order-random.svg' }, + single: { label: '单曲循环', icon: '/img/audio-player/order-single.svg' }, + }; + return (
{/* 专辑封面 */} @@ -45,7 +56,7 @@ export default function AudioPlayer({ {/* title & author */}
- +

{audio?.title}

{`${audio?.artist}/${audio?.album}`}

@@ -77,14 +88,14 @@ export default function AudioPlayer({ {/* control */}
- {/* 上一首 */} diff --git a/src/components/Song/SongCardList.tsx b/src/components/Song/SongCardList.tsx index 45c219f..21cc663 100644 --- a/src/components/Song/SongCardList.tsx +++ b/src/components/Song/SongCardList.tsx @@ -15,30 +15,31 @@ interface Props { } export default function JournalItem({ listId, songList, className }: Props) { - const { setPlayList, audio, playList, playListId, setAudio } = useAudioStore( + const { setPlayList, audioIndex, playListId, setAudioIndex } = useAudioStore( useShallow((state) => ({ setplayList: state.setPlayList, - audio: state.audio, - setAudio: state.setAudio, + audioIndex: state.audioIndex, + setAudioIndex: state.setAudioIndex, playList: state.playList, setPlayList: state.setPlayList, playListId: state.playListId, })), ); - const handlePlayList = (audioId: string) => { + const handlePlayList = (index: number) => { // 正在播放当前歌单 if (playListId !== listId) { setPlayList({ id: listId, list: songList }); } + // 正在播放其他歌单 - setAudio(audioId); + setAudioIndex(index); }; return (
- {songList.map((song: SongInfo) => ( - handlePlayList(audioId)} /> + {songList.map((song: SongInfo, index: number) => ( + handlePlayList(index)} /> ))}
); diff --git a/src/components/common/AutoScrollContainer.tsx b/src/components/common/AutoScrollContainer.tsx index 6c9fa78..91f4c35 100644 --- a/src/components/common/AutoScrollContainer.tsx +++ b/src/components/common/AutoScrollContainer.tsx @@ -30,6 +30,7 @@ const TextScroll: React.FC = ({ const [animation, setAnimation] = useState(''); // 滚动动画 const handleMouseMove = (isMouseInside: boolean) => { + if (auto) return; if (!hover) return; // 未传入 'hover = true' 时不响应鼠标事件 if (!childrenRef.current) return; // 获取不到 children 时不响应 if (isMouseInside && isHovered) return; // 鼠标已经进入时不响应 @@ -55,6 +56,16 @@ const TextScroll: React.FC = ({ } }, [children, speed, hover, animation]); // 当children变化时重新计算 + useEffect(() => { + const timer = setTimeout(() => { + if (childrenRef.current) childrenRef.current.style.animation = animation; + }, 600); + + return () => { + clearTimeout(timer); + }; + }, [childrenRef.current]); + return (
= ({ onMouseEnter={() => handleMouseMove(true)} onMouseLeave={() => handleMouseMove(false)} > -
+
{children} {isOverflowing && ( <> diff --git a/src/store/audio.ts b/src/store/audio.ts index 3e33968..b4c8f0e 100644 --- a/src/store/audio.ts +++ b/src/store/audio.ts @@ -1,37 +1,29 @@ import { produce } from 'immer'; +import cloneDeep from 'lodash/cloneDeep'; import { create } from 'zustand'; import { devtools, persist, createJSONStorage } from 'zustand/middleware'; -import { apiGetSongInfo } from '@/services'; - interface AuioState { /** 播放状态 */ playStatus: boolean; /** 播放列表id */ playListId: string; + /** 播放顺序 */ + order: PlayOrder; /** 播放器显示状态 */ show: boolean; - /** 正在播放的音频信息 */ - audio: SongInfo | null; - /** 播放列表 */ - playList: Array; - /** - * @description 设置正在播放的音频 - * @param id 音频 id,必须在 playList 内 - */ - setAudio: (id: string | null) => void; - /** @description 显示/隐藏播放器 */ + audioId: string; + /** 歌单列表 */ + playList: SongInfo[]; + /** 播放队列 */ + playQueue: SongInfo[]; + setOrder: (order: PlayOrder) => void; + setAudioId: (id: string) => void; + // 显示/隐藏播放器 setShow: (value: boolean) => void; - /** @description 设置播放列表 */ + // 设置播放列表 setPlayList: (params: { id: string; list: SongInfo[] }) => void; - /** @description 获取歌曲信息 */ - getSongInfoById: (id: string) => Promise; - /** 根据id切换歌曲 */ - switchSongById: (id: string) => Promise; - /** - * @description 切换歌曲 - * @diff 切歌距离 -1: 上一首 1: 下一首 - */ + // 切换歌曲 -1: 上一首 1: 下一首 switchSongByDiff: (diff: number) => void; } @@ -39,73 +31,89 @@ const useAuioState = create()( devtools( persist( (set, get) => { - const setAudio = (id: string | null) => { - console.log('触发setAudio'); - const audio = get().playList.find((item) => item?.id === id) || null; + const setAudioId = (id: string) => { + if (id === get().audioId) return; // 当前歌曲 + if (get().playQueue.findIndex((item) => item.id === id) < 0) return; // 不在歌单内 + set( produce((state) => { - state.audio = audio; + state.audioId = id; }), ); }; - /** - * @description 根据 id 获取歌曲信息 - * @mark 暂时未使用,启用时需要修改逻辑,`set({ audio: res.data });`会导致歌曲自动播放 - */ - const getSongInfoById = async (id: string) => { - const res = await apiGetSongInfo(id); - if (res.code === 200) set({ audio: res.data }); - return res.code === 200; - }; - const switchSongByDiff = (diff: number) => { if (diff === 0) return; - const playList = get().playList; - if (!playList) return; + const playQueue = get().playQueue; + if (!playQueue) return; - const index = playList.findIndex((item) => item.id === get().audio?.id); + const index = playQueue.findIndex((item) => item.id === get().audioId); const nextIndex = index + diff; // 计算后的 index // 判断 index 是否越界,计算音频 id let audioId = ''; if (nextIndex < 0) { - audioId = playList[playList.length - 1].id; - } else if (nextIndex >= playList.length) { - audioId = playList[0].id; + audioId = playQueue[playQueue.length - 1].id; + } else if (nextIndex >= playQueue.length) { + audioId = playQueue[0].id; } else { - audioId = playList[nextIndex].id; + audioId = playQueue[nextIndex].id; } - setAudio(audioId); + set( + produce((state) => { + state.audioId = audioId; + }), + ); + }; + + const setOrder = (order: PlayOrder) => { + const playList = cloneDeep(get().playList); + let queue: SongInfo[]; + switch (order) { + case 'list_loop': + queue = playList; + break; + case 'random': + queue = playList.sort(() => Math.random() - 0.5); + break; + case 'single': + queue = [playList[get().playList.findIndex((item) => item.id === get().audioId)]]; + break; + } + set( + produce((state) => { + state.order = order; + state.playQueue = queue; + }), + ); }; return { + audioId: '', + order: 'list_loop', playStatus: false, show: false, - audio: null, + playListId: '', playList: [], - setAudio, + playQueue: [], + setAudioId, + setOrder, setShow: (value) => set( produce((state) => { state.show = value; }), ), - switchSongById: async (id: string) => { - const res = await getSongInfoById(id); - return res; - }, - playListId: '', setPlayList: (params) => { set( produce((state) => { state.playListId = params.id; state.playList = params.list; + state.playQueue = params.list; }), ); }, - getSongInfoById, switchSongByDiff, }; }, diff --git a/src/types/player.d.ts b/src/types/player.d.ts new file mode 100644 index 0000000..ba5704a --- /dev/null +++ b/src/types/player.d.ts @@ -0,0 +1 @@ +declare type PlayOrder = 'list_loop' | 'random' | 'single';