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.
* 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<HTMLAudioElement | undefined>(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<boolean>(false); // 是否加载过组件
const [isPlaying, setIsPlaying] = useState(false); // 播放状态
const [trackProgress, setTrackProgress] = useState<number>(0); // 音频进度(s)
const isFirst = useRef<boolean>(false); // 第一次进入
const { duration } = audioRef.current; // 音频总时长(s)
const [duration, setDuration] = useState<number>(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 (
<div>
<PlayerControl
playStatus={isPlaying}
audio={audio}
duration={duration}
onPlay={handlePlay}
onPrev={handlePlayPre}
onNext={handlePlayNext}
className={className}
trackProgress={trackProgress}
onChangeProgress={handleChangeProgress}
/>
</div>
<PlayerControl
playStatus={isPlaying}
order={order}
audio={audio}
duration={duration}
onPlay={handlePlay}
onPrev={() => handleSwitchAudio(-1)}
onNext={() => handleSwitchAudio(1)}
onOrder={handleChangeOrder}
className={className}
trackProgress={trackProgress}
onChangeProgress={handleChangeProgress}
/>
);
}

@ -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<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 (
<div className={`flex flex-row w-[1200px] pt-[29px] ${className}`}>
{/* 专辑封面 */}
@ -45,7 +56,7 @@ export default function AudioPlayer({
{/* title & author */}
<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">
<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>
@ -77,14 +88,14 @@ export default function AudioPlayer({
{/* control */}
<div className="flex flex-row items-center">
<button>
{/* 随机播放 */}
<button onClick={onOrder}>
{/* 播放顺序 */}
<Image
src={'/img/audio-player/random.svg'}
alt="random"
src={orders[order].icon}
width={28}
height={28}
className="ml-[55px] mr-[60px]"
alt={orders[order].label}
/>
</button>
{/* 上一首 */}

@ -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 (
<div className={`${className}`}>
{songList.map((song: SongInfo) => (
<SongCard key={song.id} {...song} onPlay={(audioId: string) => handlePlayList(audioId)} />
{songList.map((song: SongInfo, index: number) => (
<SongCard key={song.id} {...song} onPlay={() => handlePlayList(index)} />
))}
</div>
);

@ -30,6 +30,7 @@ const TextScroll: React.FC<TextScrollProps> = ({
const [animation, setAnimation] = useState<string>(''); // 滚动动画
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<TextScrollProps> = ({
}
}, [children, speed, hover, animation]); // 当children变化时重新计算
useEffect(() => {
const timer = setTimeout(() => {
if (childrenRef.current) childrenRef.current.style.animation = animation;
}, 600);
return () => {
clearTimeout(timer);
};
}, [childrenRef.current]);
return (
<div
ref={containerRef}
@ -63,11 +74,7 @@ const TextScroll: React.FC<TextScrollProps> = ({
onMouseEnter={() => handleMouseMove(true)}
onMouseLeave={() => handleMouseMove(false)}
>
<div
ref={childrenRef}
className={`flex flex-row items-center relative w-[fit-content] h-auto ${className}`}
style={{ animation: auto ? animation : 'none' }}
>
<div ref={childrenRef} className={`flex flex-row items-center relative w-[fit-content] h-auto ${className}`}>
{children}
{isOverflowing && (
<>

@ -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<SongInfo>;
/**
* @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<boolean>;
/** 根据id切换歌曲 */
switchSongById: (id: string) => Promise<boolean>;
/**
* @description
* @diff -1: 1:
*/
// 切换歌曲 -1: 上一首 1: 下一首
switchSongByDiff: (diff: number) => void;
}
@ -39,73 +31,89 @@ const useAuioState = create<AuioState>()(
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,
};
},

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