diff --git a/src/components/AudioPlayer/Player.tsx b/src/components/AudioPlayer/Player.tsx index 4388f8f..6694f8e 100644 --- a/src/components/AudioPlayer/Player.tsx +++ b/src/components/AudioPlayer/Player.tsx @@ -5,13 +5,20 @@ */ import { useState, useRef, useEffect } from 'react'; +import Image from 'next/image'; import { useShallow } from 'zustand/react/shallow'; import { useToast } from '@/components/ui/use-toast'; -import PlayerControl from './PlayerControl'; +import styles from './index.module.css'; +import NextButton from './widget/Next'; +import OrderButton from './widget/Order'; +import ProgressBar from './widget/ProgressBar'; +import VolumeButton from './widget/Volume'; +import { CollectButton, PlayButton } from '@/components'; import { useAudioStore } from '@/store'; +import { secondToDate } from '@/utils'; export default function AudioPlayer({ className, @@ -46,6 +53,7 @@ export default function AudioPlayer({ const isReady = useRef(false); // 是否加载过组件 const [trackProgress, setTrackProgress] = useState(0); // 音频进度(s) const [duration, setDuration] = useState(0); // 音频总时长(s) + const [volume, setVolume] = useState(50); // 音频进度(s) /** * @description 切换播放顺序 @@ -96,8 +104,8 @@ export default function AudioPlayer({ }; // 调整播放进度 - const handleChangeProgress = (value: string) => { - audioRef.current.currentTime = Number(value); + const handleChangeProgress = (value: number) => { + audioRef.current.currentTime = value; setTrackProgress(audioRef.current.currentTime); setPlayState(true); }; @@ -113,6 +121,16 @@ export default function AudioPlayer({ setTrackProgress(audioRef.current.currentTime); }; + // 调节音量 + const handleChangeVolumn = (value: number) => { + setVolume(value); + }; + + // 播放/暂停事件 + useEffect(() => { + audioRef.current.volume = volume / 100; + }, [volume]); + // 播放/暂停事件 useEffect(() => { playState ? audioRef.current.play() : audioRef.current.pause(); @@ -159,21 +177,74 @@ export default function AudioPlayer({ }, []); return ( - handleSwitchAudio(-1)} - onNext={() => handleSwitchAudio(1)} - onOrder={handleChangeOrder} - className={className} - trackProgress={trackProgress} - onChangeProgress={handleChangeProgress} - onSwitchShowCard={onSwitchShowCard} - /> +
+ {/* left */} +
+ {/* 专辑封面 */} +
+ {audio?.pic && ( + music + )} +
+ + {/* 歌曲信息 */} +
+ {/* */} + {/*
*/} +

{audio?.title ?? ''}

+

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

+ {/*
*/} + {/*
*/} +
+
+ +
+ {/* center */} +
+ {/* 按钮 */} +
+ handleSwitchAudio(-1)} /> + + handleSwitchAudio(1)} /> +
+ + {/* 播放进度 */} +
+ {/* 播放时长 */} + + {secondToDate(trackProgress)} + + + {/* 进度条 */} + handleChangeProgress(value)} + /> + + {/* 总时长 */} + + {secondToDate(duration || 0)} + +
+
+ + {/* right */} +
+
+ {/* 收藏歌曲 */} + + + {/* 播放顺序 */} + {playListInfo.type !== 'fm' && } + + +
+
+
+
); } diff --git a/src/components/AudioPlayer/PlayerBar.tsx b/src/components/AudioPlayer/PlayerBar.tsx index cfeca8b..30b388a 100644 --- a/src/components/AudioPlayer/PlayerBar.tsx +++ b/src/components/AudioPlayer/PlayerBar.tsx @@ -59,7 +59,7 @@ const PlayerBar = ({ className }: { className?: string }) => {
{/* 播放器 */}
setShowCard(!showCard)} /> diff --git a/src/components/AudioPlayer/PlayerButton.tsx b/src/components/AudioPlayer/PlayerButton.tsx deleted file mode 100644 index 5c711d6..0000000 --- a/src/components/AudioPlayer/PlayerButton.tsx +++ /dev/null @@ -1,28 +0,0 @@ -// 播放器按钮 - -'use client'; - -import Image from 'next/image'; - -interface Props { - size: number; - img: string; - onClick: () => void; - className?: string; -} - -export default function ControlButton({ size, img, onClick, className = '' }: Props) { - return ( - { - e.preventDefault(); - onClick(); - }} - > - Pause - - ); -} diff --git a/src/components/AudioPlayer/PlayerControl.tsx b/src/components/AudioPlayer/PlayerControl.tsx deleted file mode 100644 index fca8d40..0000000 --- a/src/components/AudioPlayer/PlayerControl.tsx +++ /dev/null @@ -1,144 +0,0 @@ -'use client'; - -// 样式层 - -import Image from 'next/image'; - -import styles from './index.module.css'; - -import { PlayerButton, CollectButton } from '@/components'; -import { secondToDate } from '@/utils'; - -interface Props { - audio: SongInfo | null; - order: PlayOrder; - showOrder: boolean; - playStatus: boolean; - showCard: boolean; - onSwitchShowCard: () => void; - onPlay: () => void; - /** 切换播放顺序 */ - onOrder: () => void; - onPrev: () => void; - onNext: () => void; - /** 改变播放进度 */ - onChangeProgress: (value: string) => void; - trackProgress: number; - duration: number; - className?: string; -} - -export default function AudioPlayer({ - audio, - order, - playStatus, - showCard, - showOrder, - onPlay, - onOrder, - onPrev, - onNext, - onChangeProgress, - onSwitchShowCard, - trackProgress, - duration, - className, -}: Props) { - const currentPercentage = duration ? `${(trackProgress / duration) * 100}%` : '0%'; - const trackStyling: string = ` - -webkit-gradient(linear, 0% 0%, 100% 0%, color-stop(${currentPercentage}, rgb(24,24,24)), color-stop(${currentPercentage}, rgba(0,0,0,0.1))) - `; // 进度条样式 - - // 播放顺序 - const orderList: Array<{ value: string; label: string; icon: string }> = [ - { value: 'list_loop', label: '列表循环', icon: '/img/audio-player/order-list_loop.svg' }, - { value: 'random', label: '随机播放', icon: '/img/audio-player/order-random.svg' }, - { value: 'single_loop', label: '单曲循环', icon: '/img/audio-player/order-single.svg' }, - ]; - - return ( -
-
- {/* 专辑封面 */} -
- {audio?.pic && ( - music - )} -
- - {/* 歌曲信息 */} -
- {/* */} - {/*
*/} -

{audio?.title ?? ''}

-

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

- {/*
*/} - {/*
*/} -
- -
- {/* 进度条 */} -
- onChangeProgress(e.target.value)} - style={{ background: trackStyling }} - /> -
- - {/* 时间 */} -

- {`${secondToDate(trackProgress)}`} - {` / ${secondToDate(duration || 0)}`} -

-
-
- - {/* control */} -
- {/* 收藏歌曲 */} - {!!audio?.id && ( - - )} - {/* 播放顺序 */} - {showOrder && - orderList.map((item) => ( - - ))} - {/* 上一首 */} - - {/* 播放/暂停 */} -
- - -
- {/* 下一首 */} - -
-
- ); -} diff --git a/src/components/AudioPlayer/widget/ButtonProvide.tsx b/src/components/AudioPlayer/widget/ButtonProvide.tsx new file mode 100644 index 0000000..2be0595 --- /dev/null +++ b/src/components/AudioPlayer/widget/ButtonProvide.tsx @@ -0,0 +1,24 @@ +// 播放器按钮 + +'use client'; + +interface Props { + onClick: () => void; + className?: string; + children?: React.ReactNode; +} + +export default function ControlButton({ children, className, onClick }: Props) { + return ( + { + e.preventDefault(); + onClick(); + }} + > + {!!children && children} + + ); +} diff --git a/src/components/AudioPlayer/widget/Next.tsx b/src/components/AudioPlayer/widget/Next.tsx new file mode 100644 index 0000000..d43e632 --- /dev/null +++ b/src/components/AudioPlayer/widget/Next.tsx @@ -0,0 +1,20 @@ +import ButtonProvide from './ButtonProvide'; + +export default function Next({ className, onClick }: { className?: string; onClick: () => void }) { + return ( + + + + + + + ); +} diff --git a/src/components/AudioPlayer/widget/Order.tsx b/src/components/AudioPlayer/widget/Order.tsx new file mode 100644 index 0000000..6c4044c --- /dev/null +++ b/src/components/AudioPlayer/widget/Order.tsx @@ -0,0 +1,105 @@ +import { ReactElement } from 'react'; + +import ButtonProvide from './ButtonProvide'; + +export default function Order({ + order, + className, + onClick, +}: { + order: PlayOrder; + className?: string; + onClick: () => void; +}) { + const orderList: Array<{ value: string; label: string; icon: ReactElement }> = [ + { value: 'list_loop', label: '列表循环', icon: }, + { value: 'random', label: '随机播放', icon: }, + { value: 'single_loop', label: '单曲循环', icon: }, + ]; + + // return orderList.map((item) => ( + // + // {item.icon} + // + // )); + + return ( + + {orderList.find((item) => item.value === order)?.icon ?? null} + + ); +} + +const ListLoop = () => ( + + + + +); + +const Random = () => ( + + + + + + + +); + +const SingleLoop = () => ( + + + + +); diff --git a/src/components/AudioPlayer/widget/ProgressBar.tsx b/src/components/AudioPlayer/widget/ProgressBar.tsx new file mode 100644 index 0000000..3383964 --- /dev/null +++ b/src/components/AudioPlayer/widget/ProgressBar.tsx @@ -0,0 +1,35 @@ +import React from 'react'; + +import * as Slider from '@radix-ui/react-slider'; + +import styles from './index.module.css'; + +export default function ProgressBar({ + value, + duration, + className, + onChange, +}: { + value: number; + duration: number; + className?: string; + onChange: (value: number) => void; +}) { + const handleChange = (value: number) => { + onChange(value); + }; + + return ( + handleChange(value[0])} + > + + + + + + ); +} diff --git a/src/components/AudioPlayer/widget/Volume.tsx b/src/components/AudioPlayer/widget/Volume.tsx new file mode 100644 index 0000000..3ebb64f --- /dev/null +++ b/src/components/AudioPlayer/widget/Volume.tsx @@ -0,0 +1,113 @@ +'use client'; + +import { useState } from 'react'; + +import ButtonProvide from './ButtonProvide'; +import VolumeBar from './VolumeBar'; + +export default function Volume({ + value, + className, + onChange, +}: { + value: number; + className?: string; + onChange: (value: number) => void; +}) { + const [hover, setHover] = useState(false); + + const handleClick = () => { + onChange(value > 0 ? 0 : 50); + }; + + return ( +
setHover(true)} + onMouseLeave={() => setHover(false)} + > + + + + {value >= 50 ? ( + + ) : value === 0 ? ( + + ) : ( + + )} + +
+ ); +} + +const VolumeLow = ({ hover }: { hover: boolean }) => ( + + + + +); + +const VolumeOff = ({ hover }: { hover: boolean }) => ( + + + + + +); + +const VolumeFull = ({ hover }: { hover: boolean }) => ( + + + + +); diff --git a/src/components/AudioPlayer/widget/VolumeBar.tsx b/src/components/AudioPlayer/widget/VolumeBar.tsx new file mode 100644 index 0000000..135ba5b --- /dev/null +++ b/src/components/AudioPlayer/widget/VolumeBar.tsx @@ -0,0 +1,31 @@ +import * as SliderPrimitive from '@radix-ui/react-slider'; + +import styles from './index.module.css'; + +export default function Volumne({ + value, + className, + onChange, +}: { + value: number; + className?: string; + onChange: (value: number) => void; +}) { + const handleChange = (value: number) => { + onChange(value); + }; + + return ( + handleChange(value[0])} + > + + + + + + ); +} diff --git a/src/components/AudioPlayer/widget/index.module.css b/src/components/AudioPlayer/widget/index.module.css new file mode 100644 index 0000000..6d0d7e5 --- /dev/null +++ b/src/components/AudioPlayer/widget/index.module.css @@ -0,0 +1,86 @@ +/* 播放进度条 */ +.ProgressBarRoot { + position: relative; + display: flex; + align-items: center; + user-select: none; + touch-action: none; + width: 718px; + height: 11px; + cursor: pointer; +} + +.ProgressBarTrack { + background-color: rgba(0, 0, 0, 0.1); + position: relative; + flex-grow: 1; + height: 3px; +} + +.ProgressBarRange { + position: absolute; + background-color: #181818; + height: 3px; +} + +.ProgressBarThumb { + display: block; + width: 11px; + height: 11px; + background-color: #181818; + border-radius: 50%; + outline: none; +} +.ProgressBarThumb:hover { + /* background-color: var(--violet-3); */ +} + +/* 音量条 */ +.VolumeBarRoot { + display: flex; + align-items: center; + position: relative; +} +.VolumeBarRoot[data-orientation='vertical'] { + flex-direction: column; + width: 20px; + height: 100px; + border-radius: 37px; +} + +/* 底色 */ +.VolumeBarTrack { + position: relative; + flex-grow: 1; + background-color: rgba(0, 0, 0, 0.1); +} +.VolumeBarTrack[data-orientation='vertical'] { + width: 3px; +} + +/* 激活区域 */ +.VolumeBarRange { + position: absolute; + background-color: #646464; +} +.VolumeBarRange[data-orientation='vertical'] { + width: 100%; +} + +/* 滑块 */ +.VolumeBarThumb { + display: block; + width: 11px; + height: 11px; + border-radius: 50%; + background-color: #646464; + outline: none; +} + +.VolumeBarRoot:hover .VolumeBarRange { + background-color: rgba(0, 0, 0, 0.95); +} + +.VolumeBarRoot:hover .VolumeBarThumb { + background-color: rgba(0, 0, 0, 0.95); +} diff --git a/src/components/Button/PlayButton.tsx b/src/components/Button/PlayButton.tsx new file mode 100644 index 0000000..2795121 --- /dev/null +++ b/src/components/Button/PlayButton.tsx @@ -0,0 +1,37 @@ +import ButtonProvide from '../AudioPlayer/widget/ButtonProvide'; + +export default function PlayButton({ + playState, + size, + className, + onClick, +}: { + playState: boolean; + size: number; + className?: string; + onClick: () => void; +}) { + return ( + + {playState ? ( + + + + + + ) : ( + + + + + )} + + ); +} diff --git a/src/components/Journal/VolPlayButton.tsx b/src/components/Journal/VolPlayButton.tsx index 675f197..0a52e99 100644 --- a/src/components/Journal/VolPlayButton.tsx +++ b/src/components/Journal/VolPlayButton.tsx @@ -2,7 +2,7 @@ import { useShallow } from 'zustand/react/shallow'; -import { PlayerButton } from '@/components'; +import { PlayButton } from '@/components'; import { useAudioStore } from '@/store'; export default function VolPlayButton({ @@ -44,14 +44,12 @@ export default function VolPlayButton({ setPlayState(!playState); }; - return isCurrentVol && playState ? ( - handlePlay(false)} + return ( + handlePlay(!(isCurrentVol && playState))} /> - ) : ( - handlePlay(true)} className={className} /> ); } diff --git a/src/components/index.ts b/src/components/index.ts index f217bd5..c08188e 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -22,6 +22,7 @@ export { default as RedirectCheck } from './Login/RedirectCheck'; // Button export { default as CollectButton } from './Button/CollectButton'; export { default as ScrollTopButton } from './Button/ScrollTopButton'; +export { default as PlayButton } from './Button/PlayButton'; export { default as ThumbButton } from './Button/ThumbButton'; export { default as ButtonFM } from './Button/ButtonFM'; @@ -39,7 +40,7 @@ export { default as VolListCoverCard } from './Journal/JournalList/VolListCoverC // Audio Player export { default as PlayerBar } from './AudioPlayer/PlayerBar'; export { default as AudioPlayer } from './AudioPlayer/Player'; -export { default as PlayerButton } from './AudioPlayer/PlayerButton'; +export { default as PlayerButton } from './AudioPlayer/widget/ButtonProvide'; // SongCard export { default as SongCard } from './SongCard/SongCard';