feat(AudioPlayer):

1. Add play order;
2. Fix refresh auto play bug;
3. Add shortcut key;
mack-mac
mackt 8 months ago
parent 40fffcda82
commit beb8b5c643

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Before

Width:  |  Height:  |  Size: 917 B

After

Width:  |  Height:  |  Size: 917 B

@ -2,8 +2,6 @@
/** /**
* *
* 1. * 1.
* 2.
* 3.
*/ */
import { useState, useRef, useEffect } from 'react'; import { useState, useRef, useEffect } from 'react';
@ -14,99 +12,128 @@ import PlayerControl from './PlayerControl';
import useAudioStore from '@/store/audio'; import useAudioStore from '@/store/audio';
export default function AudioPlayer({ className }: { className?: string }) { export default function AudioPlayer({ className }: { className?: string }) {
const { audio, switchSongByDiff } = useAudioStore( const { audioId, order, playQueue, switchSongByDiff, setOrder } = useAudioStore(
useShallow((state) => { useShallow((state) => {
return { return {
audio: state.audio, audioId: state.audioId,
playList: state.playList, order: state.order,
playQueue: state.playQueue,
setOrder: state.setOrder,
switchSongByDiff: state.switchSongByDiff, switchSongByDiff: state.switchSongByDiff,
}; };
}), }),
); );
// const musicPlayers = useRef<HTMLAudioElement | undefined>(typeof Audio !== 'undefined' ? new Audio('') : undefined); const audio: SongInfo = playQueue.find((item) => item.id === audioId) || playQueue[0];
const audioRef = useRef(new Audio(''));
const audioRef = useRef(new Audio(audio.src));
const isReady = useRef<boolean>(false); // 是否加载过组件
const [isPlaying, setIsPlaying] = useState(false); // 播放状态 const [isPlaying, setIsPlaying] = useState(false); // 播放状态
const [trackProgress, setTrackProgress] = useState<number>(0); // 音频进度(s) const [trackProgress, setTrackProgress] = useState<number>(0); // 音频进度(s)
const isFirst = useRef<boolean>(false); // 第一次进入 const [duration, setDuration] = useState<number>(0); // 音频总时长(s)
const { duration } = audioRef.current; // 音频总时长(s)
// 播放/暂停 // 修改播放顺序
const handlePlay = () => { const handleChangeOrder = () => {
setIsPlaying(!isPlaying); // 修改 icon & hover
const orderArr: PlayOrder[] = ['list_loop', 'random', 'single'];
const index = orderArr.indexOf(order);
setOrder(orderArr[(index + 1) % 3]);
}; };
// 上一首 // 播放/暂停
const handlePlayPre = () => { const handlePlay = () => {
switchSongByDiff(-1); setIsPlaying((isPlaying) => !isPlaying);
}; };
// 下一首 /**
const handlePlayNext = () => { * @description
switchSongByDiff(1); * @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) => { const handleChangeProgress = (value: string) => {
audioRef.current.currentTime = Number(value); audioRef.current.currentTime = Number(value);
setTrackProgress(audioRef.current.currentTime); setTrackProgress(audioRef.current.currentTime);
setIsPlaying(true);
}; };
// 监听音频进度 // 监听音频进度
const onTimeUpdate = () => { const onTimeUpdate = () => {
if (audioRef.current.ended) handlePlayNext(); // 完成播放
setTrackProgress(audioRef.current.currentTime); if (audioRef.current.ended) {
}; handleSwitchAudio(1);
audioRef.current.play();
}
// 给 audio 绑定 timeupdate 监听 setTrackProgress(audioRef.current.currentTime);
const handleTimeUpdate = (status: boolean) => {
status
? audioRef.current.addEventListener('timeupdate', onTimeUpdate)
: audioRef.current.removeEventListener('timeupdate', onTimeUpdate);
}; };
// 播放/暂停 // 播放/暂停事件
useEffect(() => { useEffect(() => {
isPlaying ? audioRef.current.play() : audioRef.current.pause(); isPlaying ? audioRef.current.play() : audioRef.current.pause();
}, [isPlaying]); }, [isPlaying]);
// 切换歌曲 // 切换歌曲
useEffect(() => { useEffect(() => {
const handleLoadedMetadata = () => {
setDuration(audioRef.current.duration);
};
audioRef.current.pause(); audioRef.current.pause();
if (!audio) return;
audioRef.current = new Audio(audio.src); audioRef.current = new Audio(audio.src);
setTrackProgress(audioRef.current.currentTime); audioRef.current.addEventListener('timeupdate', onTimeUpdate);
console.log('id变化', { audio, isFirst: isFirst.current });
if (isFirst.current) { if (isReady.current) {
audioRef.current.addEventListener('loadedmetadata', handleLoadedMetadata); // 获取音频时长
audioRef.current.play(); audioRef.current.play();
setIsPlaying(true); setIsPlaying(true);
handleTimeUpdate(true);
} else { } else {
isFirst.current = true; isReady.current = true;
} }
}, [audio]);
// 卸载时的处理 return () => {
audioRef.current.removeEventListener('loadedmetadata', handleLoadedMetadata);
};
}, [audioId]);
useEffect(() => { useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.code === 'Space' || e.keyCode === 32) {
handlePlay();
}
};
document.addEventListener('keydown', handleKeyDown); // 监听快捷键
setIsPlaying(false); // 禁止自动播放
return () => { return () => {
audioRef.current.pause(); audioRef.current.pause();
handleTimeUpdate(false); audioRef.current.removeEventListener('timeupdate', onTimeUpdate);
document.removeEventListener('keydown', handleKeyDown);
}; };
}, []); }, []);
return ( return (
<div>
<PlayerControl <PlayerControl
playStatus={isPlaying} playStatus={isPlaying}
order={order}
audio={audio} audio={audio}
duration={duration} duration={duration}
onPlay={handlePlay} onPlay={handlePlay}
onPrev={handlePlayPre} onPrev={() => handleSwitchAudio(-1)}
onNext={handlePlayNext} onNext={() => handleSwitchAudio(1)}
onOrder={handleChangeOrder}
className={className} className={className}
trackProgress={trackProgress} trackProgress={trackProgress}
onChangeProgress={handleChangeProgress} onChangeProgress={handleChangeProgress}
/> />
</div>
); );
} }

@ -8,8 +8,10 @@ import { AutoScrollContainer } from '@/components';
interface Props { interface Props {
audio: SongInfo | null; audio: SongInfo | null;
order: PlayOrder;
playStatus: boolean; playStatus: boolean;
onPlay: () => void; onPlay: () => void;
onOrder: () => void;
onPrev: () => void; onPrev: () => void;
onNext: () => void; onNext: () => void;
onChangeProgress: (value: string) => void; onChangeProgress: (value: string) => void;
@ -20,8 +22,10 @@ interface Props {
export default function AudioPlayer({ export default function AudioPlayer({
audio, audio,
order,
playStatus, playStatus,
onPlay, onPlay,
onOrder,
onPrev, onPrev,
onNext, onNext,
onChangeProgress, 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))) -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<PlayOrder, { label: string; icon: string }> = {
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 ( return (
<div className={`flex flex-row w-[1200px] pt-[29px] ${className}`}> <div className={`flex flex-row w-[1200px] pt-[29px] ${className}`}>
{/* 专辑封面 */} {/* 专辑封面 */}
@ -45,7 +56,7 @@ export default function AudioPlayer({
{/* title & author */} {/* title & author */}
<div className="h-full ml-[27px] mr-[44px] py-[14px]"> <div className="h-full ml-[27px] mr-[44px] py-[14px]">
<AutoScrollContainer auto hover width="140px" speed={50}> <AutoScrollContainer key={audio?.id} auto hover width="140px" speed={50}>
<div className="w-auto h-auto"> <div className="w-auto h-auto">
<p className="text-[17px] leading-[23.8px] text-[rgba(0,0,0,0.95)]">{audio?.title}</p> <p className="text-[17px] leading-[23.8px] text-[rgba(0,0,0,0.95)]">{audio?.title}</p>
<p className="text-[13px] leading-[18.2px] text-[rgba(0,0,0,0.7)]">{`${audio?.artist}/${audio?.album}`}</p> <p className="text-[13px] leading-[18.2px] text-[rgba(0,0,0,0.7)]">{`${audio?.artist}/${audio?.album}`}</p>
@ -77,14 +88,14 @@ export default function AudioPlayer({
{/* control */} {/* control */}
<div className="flex flex-row items-center"> <div className="flex flex-row items-center">
<button> <button onClick={onOrder}>
{/* 随机播放 */} {/* 播放顺序 */}
<Image <Image
src={'/img/audio-player/random.svg'} src={orders[order].icon}
alt="random"
width={28} width={28}
height={28} height={28}
className="ml-[55px] mr-[60px]" className="ml-[55px] mr-[60px]"
alt={orders[order].label}
/> />
</button> </button>
{/* 上一首 */} {/* 上一首 */}

@ -15,30 +15,31 @@ interface Props {
} }
export default function JournalItem({ listId, songList, className }: Props) { export default function JournalItem({ listId, songList, className }: Props) {
const { setPlayList, audio, playList, playListId, setAudio } = useAudioStore( const { setPlayList, audioIndex, playListId, setAudioIndex } = useAudioStore(
useShallow((state) => ({ useShallow((state) => ({
setplayList: state.setPlayList, setplayList: state.setPlayList,
audio: state.audio, audioIndex: state.audioIndex,
setAudio: state.setAudio, setAudioIndex: state.setAudioIndex,
playList: state.playList, playList: state.playList,
setPlayList: state.setPlayList, setPlayList: state.setPlayList,
playListId: state.playListId, playListId: state.playListId,
})), })),
); );
const handlePlayList = (audioId: string) => { const handlePlayList = (index: number) => {
// 正在播放当前歌单 // 正在播放当前歌单
if (playListId !== listId) { if (playListId !== listId) {
setPlayList({ id: listId, list: songList }); setPlayList({ id: listId, list: songList });
} }
// 正在播放其他歌单 // 正在播放其他歌单
setAudio(audioId); setAudioIndex(index);
}; };
return ( return (
<div className={`${className}`}> <div className={`${className}`}>
{songList.map((song: SongInfo) => ( {songList.map((song: SongInfo, index: number) => (
<SongCard key={song.id} {...song} onPlay={(audioId: string) => handlePlayList(audioId)} /> <SongCard key={song.id} {...song} onPlay={() => handlePlayList(index)} />
))} ))}
</div> </div>
); );

@ -30,6 +30,7 @@ const TextScroll: React.FC<TextScrollProps> = ({
const [animation, setAnimation] = useState<string>(''); // 滚动动画 const [animation, setAnimation] = useState<string>(''); // 滚动动画
const handleMouseMove = (isMouseInside: boolean) => { const handleMouseMove = (isMouseInside: boolean) => {
if (auto) return;
if (!hover) return; // 未传入 'hover = true' 时不响应鼠标事件 if (!hover) return; // 未传入 'hover = true' 时不响应鼠标事件
if (!childrenRef.current) return; // 获取不到 children 时不响应 if (!childrenRef.current) return; // 获取不到 children 时不响应
if (isMouseInside && isHovered) return; // 鼠标已经进入时不响应 if (isMouseInside && isHovered) return; // 鼠标已经进入时不响应
@ -55,6 +56,16 @@ const TextScroll: React.FC<TextScrollProps> = ({
} }
}, [children, speed, hover, animation]); // 当children变化时重新计算 }, [children, speed, hover, animation]); // 当children变化时重新计算
useEffect(() => {
const timer = setTimeout(() => {
if (childrenRef.current) childrenRef.current.style.animation = animation;
}, 600);
return () => {
clearTimeout(timer);
};
}, [childrenRef.current]);
return ( return (
<div <div
ref={containerRef} ref={containerRef}
@ -63,11 +74,7 @@ const TextScroll: React.FC<TextScrollProps> = ({
onMouseEnter={() => handleMouseMove(true)} onMouseEnter={() => handleMouseMove(true)}
onMouseLeave={() => handleMouseMove(false)} onMouseLeave={() => handleMouseMove(false)}
> >
<div <div ref={childrenRef} className={`flex flex-row items-center relative w-[fit-content] h-auto ${className}`}>
ref={childrenRef}
className={`flex flex-row items-center relative w-[fit-content] h-auto ${className}`}
style={{ animation: auto ? animation : 'none' }}
>
{children} {children}
{isOverflowing && ( {isOverflowing && (
<> <>

@ -1,37 +1,29 @@
import { produce } from 'immer'; import { produce } from 'immer';
import cloneDeep from 'lodash/cloneDeep';
import { create } from 'zustand'; import { create } from 'zustand';
import { devtools, persist, createJSONStorage } from 'zustand/middleware'; import { devtools, persist, createJSONStorage } from 'zustand/middleware';
import { apiGetSongInfo } from '@/services';
interface AuioState { interface AuioState {
/** 播放状态 */ /** 播放状态 */
playStatus: boolean; playStatus: boolean;
/** 播放列表id */ /** 播放列表id */
playListId: string; playListId: string;
/** 播放顺序 */
order: PlayOrder;
/** 播放器显示状态 */ /** 播放器显示状态 */
show: boolean; show: boolean;
/** 正在播放的音频信息 */ audioId: string;
audio: SongInfo | null; /** 歌单列表 */
/** 播放列表 */ playList: SongInfo[];
playList: Array<SongInfo>; /** 播放队列 */
/** playQueue: SongInfo[];
* @description setOrder: (order: PlayOrder) => void;
* @param id id playList setAudioId: (id: string) => void;
*/ // 显示/隐藏播放器
setAudio: (id: string | null) => void;
/** @description 显示/隐藏播放器 */
setShow: (value: boolean) => void; setShow: (value: boolean) => void;
/** @description 设置播放列表 */ // 设置播放列表
setPlayList: (params: { id: string; list: SongInfo[] }) => void; setPlayList: (params: { id: string; list: SongInfo[] }) => void;
/** @description 获取歌曲信息 */ // 切换歌曲 -1: 上一首 1: 下一首
getSongInfoById: (id: string) => Promise<boolean>;
/** 根据id切换歌曲 */
switchSongById: (id: string) => Promise<boolean>;
/**
* @description
* @diff -1: 1:
*/
switchSongByDiff: (diff: number) => void; switchSongByDiff: (diff: number) => void;
} }
@ -39,73 +31,89 @@ const useAuioState = create<AuioState>()(
devtools( devtools(
persist( persist(
(set, get) => { (set, get) => {
const setAudio = (id: string | null) => { const setAudioId = (id: string) => {
console.log('触发setAudio'); if (id === get().audioId) return; // 当前歌曲
const audio = get().playList.find((item) => item?.id === id) || null; if (get().playQueue.findIndex((item) => item.id === id) < 0) return; // 不在歌单内
set( set(
produce((state) => { 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) => { const switchSongByDiff = (diff: number) => {
if (diff === 0) return; if (diff === 0) return;
const playList = get().playList; const playQueue = get().playQueue;
if (!playList) return; 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 const nextIndex = index + diff; // 计算后的 index
// 判断 index 是否越界,计算音频 id // 判断 index 是否越界,计算音频 id
let audioId = ''; let audioId = '';
if (nextIndex < 0) { if (nextIndex < 0) {
audioId = playList[playList.length - 1].id; audioId = playQueue[playQueue.length - 1].id;
} else if (nextIndex >= playList.length) { } else if (nextIndex >= playQueue.length) {
audioId = playList[0].id; audioId = playQueue[0].id;
} else { } 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 { return {
audioId: '',
order: 'list_loop',
playStatus: false, playStatus: false,
show: false, show: false,
audio: null, playListId: '',
playList: [], playList: [],
setAudio, playQueue: [],
setAudioId,
setOrder,
setShow: (value) => setShow: (value) =>
set( set(
produce((state) => { produce((state) => {
state.show = value; state.show = value;
}), }),
), ),
switchSongById: async (id: string) => {
const res = await getSongInfoById(id);
return res;
},
playListId: '',
setPlayList: (params) => { setPlayList: (params) => {
set( set(
produce((state) => { produce((state) => {
state.playListId = params.id; state.playListId = params.id;
state.playList = params.list; state.playList = params.list;
state.playQueue = params.list;
}), }),
); );
}, },
getSongInfoById,
switchSongByDiff, switchSongByDiff,
}; };
}, },

@ -0,0 +1 @@
declare type PlayOrder = 'list_loop' | 'random' | 'single';
Loading…
Cancel
Save