feat(Player):

1. 新增音量条
2. 更改布局
feature/artists
mackt 7 months ago
parent e2d65c89c9
commit 9e737bcb4b

@ -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<boolean>(false); // 是否加载过组件
const [trackProgress, setTrackProgress] = useState<number>(0); // 音频进度(s)
const [duration, setDuration] = useState<number>(0); // 音频总时长(s)
const [volume, setVolume] = useState<number>(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 (
<PlayerControl
showOrder={playListInfo.type !== 'fm'}
playStatus={playState}
order={playOrder}
audio={audio}
duration={duration}
showCard={showCard}
onPlay={handlePlay}
onPrev={() => handleSwitchAudio(-1)}
onNext={() => handleSwitchAudio(1)}
onOrder={handleChangeOrder}
className={className}
trackProgress={trackProgress}
onChangeProgress={handleChangeProgress}
onSwitchShowCard={onSwitchShowCard}
/>
<div className={`flex justify-between w-1200px h-full ${className}`}>
{/* left */}
<div className="flex flex-row w-fit h-fit mt-22px">
{/* 专辑封面 */}
<div
className={`relative flex-shrink-0 w-48px h-48px rounded-3px bg-[#000] overflow-hidden cursor-pointer ${showCard ? styles.album_pic_overlay_collapse : styles.album_pic_overlay_expand}`}
onClick={onSwitchShowCard}
>
{audio?.pic && (
<Image src={audio.pic} alt="music" width={48} height={48} unoptimized className="w-full h-full" />
)}
</div>
{/* 歌曲信息 */}
<div className="w-158px ml-15px mr-36px cursor-default">
{/* <AutoScrollContainer key={audio?.id} auto hover width="140px" speed={50}> */}
{/* <div className="w-auto h-auto"> */}
<p className="mt-2px text-17px leading-24px text-base text-overflow">{audio?.title ?? ''}</p>
<p className="text-13px leading-18px mt-2px text-[rgba(0,0,0,0.7)] text-overflow">{`${audio?.artist}/${audio?.album}`}</p>
{/* </div> */}
{/* </AutoScrollContainer> */}
</div>
</div>
<div className="flex h-full">
{/* center */}
<div className="flex flex-col items-center h-full">
{/* 按钮 */}
<div className="flex items-center gap-24px h-fit mt-11px">
<NextButton className="rotate-180" onClick={() => handleSwitchAudio(-1)} />
<PlayButton playState={playState} size={40} onClick={handlePlay} />
<NextButton onClick={() => handleSwitchAudio(1)} />
</div>
{/* 播放进度 */}
<div className="flex items-end justify-between h-fit mt-3px gap-6px">
{/* 播放时长 */}
<span className="block w-27px mt-1px text-10px leading-14px text-center text-#000">
{secondToDate(trackProgress)}
</span>
{/* 进度条 */}
<ProgressBar
value={trackProgress}
duration={duration}
onChange={(value: number) => handleChangeProgress(value)}
/>
{/* 总时长 */}
<span className="block w-27px mt-1px text-10px leading-14px text-center text-#000">
{secondToDate(duration || 0)}
</span>
</div>
</div>
{/* right */}
<div className="flex items-end">
<div className="flex h-full items-end h-fit ml-45px mb-29px gap-18px">
{/* 收藏歌曲 */}
<CollectButton id={audio.id} active={audio.haveCollect} collectType="0" color="dark" />
{/* 播放顺序 */}
{playListInfo.type !== 'fm' && <OrderButton order={playOrder} onClick={handleChangeOrder} />}
<VolumeButton value={volume} onChange={handleChangeVolumn} />
</div>
</div>
</div>
</div>
);
}

@ -59,7 +59,7 @@ const PlayerBar = ({ className }: { className?: string }) => {
<div className={`fixed bottom-0 w-full h-auto z-10 ${className}`}>
{/* 播放器 */}
<div
className="fixed bottom-0 w-full h-[80px] bg-[#fff] bg-op-80 backdrop-blur-[20px] shadow-lg shadow-black-[0_0_10px] z-10 transition-bottom duration-600`"
className="fixed bottom-0 w-full h-[90px] bg-[#fff] bg-op-80 backdrop-blur-[20px] shadow-lg shadow-black-[0_0_10px] z-10 transition-bottom duration-600`"
style={{ bottom: audioId || showCard ? 0 : -80 }}
>
<Player className="mx-auto" onSwitchShowCard={() => setShowCard(!showCard)} />

@ -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 (
<a
href="#"
className={`block bg-cover bg-center bg-no-repeat ${className}`}
style={{ width: `${size}px`, height: `${size}px` }}
onClick={(e) => {
e.preventDefault();
onClick();
}}
>
<Image width={size} height={size} src={img} alt="Pause" priority className={`w-[${size}px] h-[${size}px]`} />
</a>
);
}

@ -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 (
<div className={`flex justify-between items-center w-1200px h-full ${className}`}>
<div className="flex flex-row items-center w-fit h-full">
{/* 专辑封面 */}
<div
className={`relative flex-shrink-0 w-48px h-48px rounded-3px bg-[#000] overflow-hidden cursor-pointer ${showCard ? styles.album_pic_overlay_collapse : styles.album_pic_overlay_expand}`}
onClick={onSwitchShowCard}
>
{audio?.pic && (
<Image src={audio.pic} alt="music" width={48} height={48} unoptimized className="w-full h-full" />
)}
</div>
{/* 歌曲信息 */}
<div className="w-158px ml-15px mr-36px cursor-default">
{/* <AutoScrollContainer key={audio?.id} auto hover width="140px" speed={50}> */}
{/* <div className="w-auto h-auto"> */}
<p className="text-17px leading-24px text-base text-overflow">{audio?.title ?? ''}</p>
<p className="text-13px leading-18px mt-2px text-[rgba(0,0,0,0.7)] text-overflow">{`${audio?.artist}/${audio?.album}`}</p>
{/* </div> */}
{/* </AutoScrollContainer> */}
</div>
<div className="h-fit mt-2px">
{/* 进度条 */}
<div className="w-686px h-fit cursor-pointer">
<input
type="range"
value={trackProgress}
step="1"
min="0"
max={duration ? duration : `${duration}`}
className={styles['range-input']}
onChange={(e) => onChangeProgress(e.target.value)}
style={{ background: trackStyling }}
/>
</div>
{/* 时间 */}
<p className="mt-1px text-11px leading-14px">
<span>{`${secondToDate(trackProgress)}`}</span>
<span className="text-[rgba(0,0,0,0.4)]">{` / ${secondToDate(duration || 0)}`}</span>
</p>
</div>
</div>
{/* control */}
<div className="flex items-center h-fit pb-4px">
{/* 收藏歌曲 */}
{!!audio?.id && (
<CollectButton id={audio.id} active={audio.haveCollect} collectType="0" color="dark" className="ml-24px" />
)}
{/* 播放顺序 */}
{showOrder &&
orderList.map((item) => (
<PlayerButton
className={`${item.value === order ? 'block' : 'hidden'} ml-24px`}
key={item.value}
size={24}
img={item.icon}
onClick={onOrder}
/>
))}
{/* 上一首 */}
<PlayerButton size={24} img={'/img/audio-player/next.svg'} onClick={onPrev} className="rotate-180 ml-24px" />
{/* 播放/暂停 */}
<div className="mx-24px">
<PlayerButton
className={playStatus ? 'block' : 'hidden'}
size={54}
img={'/img/audio-player/pause.svg'}
onClick={onPlay}
/>
<PlayerButton
className={!playStatus ? 'block' : 'hidden'}
size={54}
img={'/img/audio-player/play.svg'}
onClick={onPlay}
/>
</div>
{/* 下一首 */}
<PlayerButton size={24} img="/img/audio-player/next.svg" onClick={onNext} />
</div>
</div>
);
}

@ -0,0 +1,24 @@
// 播放器按钮
'use client';
interface Props {
onClick: () => void;
className?: string;
children?: React.ReactNode;
}
export default function ControlButton({ children, className, onClick }: Props) {
return (
<a
href="#"
className={`block w-fit h-fit ${className}`}
onClick={(e) => {
e.preventDefault();
onClick();
}}
>
{!!children && children}
</a>
);
}

@ -0,0 +1,20 @@
import ButtonProvide from './ButtonProvide';
export default function Next({ className, onClick }: { className?: string; onClick: () => void }) {
return (
<ButtonProvide className={className} onClick={onClick}>
<svg width="22" height="22" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg" className="group">
<path
className="fill-#000/70 group-hover:fill-#000/95 transition-all"
d="M19.8333 5.36009C19.8333 5.02405 19.8333 4.85604 19.8987 4.72769C19.9562 4.61479 20.048 4.52301 20.1609 4.46548C20.2893 4.40009 20.4573 4.40009 20.7933 4.40009H22.3733C22.7094 4.40009 22.8774 4.40009 23.0057 4.46548C23.1186 4.52301 23.2104 4.61479 23.2679 4.72769C23.3333 4.85604 23.3333 5.02405 23.3333 5.36009V22.6401C23.3333 22.9761 23.3333 23.1441 23.2679 23.2725C23.2104 23.3854 23.1186 23.4772 23.0057 23.5347C22.8774 23.6001 22.7094 23.6001 22.3733 23.6001H20.7933C20.4573 23.6001 20.2893 23.6001 20.1609 23.5347C20.048 23.4772 19.9562 23.3854 19.8987 23.2725C19.8333 23.1441 19.8333 22.9761 19.8333 22.6401V5.36009Z"
/>
<path
className="fill-#000/70 group-hover:fill-#000/95 transition-all"
fillRule="evenodd"
clipRule="evenodd"
d="M17.2415 13.0134C17.9308 13.4906 17.9308 14.5095 17.2415 14.9867L5.38306 23.1964C4.58718 23.7474 3.50001 23.1778 3.50001 22.2098L3.50001 5.79034C3.50001 4.82235 4.58718 4.25272 5.38306 4.80371L17.2415 13.0134Z"
/>
</svg>
</ButtonProvide>
);
}

@ -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: <ListLoop /> },
{ value: 'random', label: '随机播放', icon: <Random /> },
{ value: 'single_loop', label: '单曲循环', icon: <SingleLoop /> },
];
// return orderList.map((item) => (
// <ButtonProvide className={className} key={item.value} onClick={onClick}>
// {item.icon}
// </ButtonProvide>
// ));
return (
<ButtonProvide className={className} onClick={onClick}>
{orderList.find((item) => item.value === order)?.icon ?? null}
</ButtonProvide>
);
}
const ListLoop = () => (
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className="group">
<path
className="fill-[rgba(0,0,0,70)] group-hover:fill-[rgba(0,0,0,0.95)] transition-all"
fill-rule="evenodd"
clipRule="evenodd"
d="M18.5303 1.46967C18.2374 1.17678 17.7626 1.17678 17.4697 1.46967C17.1768 1.76256 17.1768 2.23744 17.4697 2.53033L19.1893 4.25H8C4.82436 4.25 2.25 6.82436 2.25 10V12C2.25 12.4142 2.58579 12.75 3 12.75C3.41421 12.75 3.75 12.4142 3.75 12V10C3.75 7.65279 5.65279 5.75 8 5.75H19.1893L17.4697 7.46967C17.1768 7.76256 17.1768 8.23744 17.4697 8.53033C17.7626 8.82322 18.2374 8.82322 18.5303 8.53033L21.5303 5.53033C21.6768 5.38388 21.75 5.19194 21.75 5C21.75 4.89831 21.7298 4.80134 21.6931 4.71291C21.6565 4.62445 21.6022 4.54158 21.5303 4.46967L18.5303 1.46967Z"
/>
<path
className="fill-[rgba(0,0,0,70)] group-hover:fill-[rgba(0,0,0,0.95)] transition-all"
fill-rule="evenodd"
clipRule="evenodd"
d="M5.46967 22.5303C5.76256 22.8232 6.23744 22.8232 6.53033 22.5303C6.82322 22.2374 6.82322 21.7626 6.53033 21.4697L4.81066 19.75H16C19.1756 19.75 21.75 17.1756 21.75 14V12C21.75 11.5858 21.4142 11.25 21 11.25C20.5858 11.25 20.25 11.5858 20.25 12V14C20.25 16.3472 18.3472 18.25 16 18.25H4.81066L6.53033 16.5303C6.82322 16.2374 6.82322 15.7626 6.53033 15.4697C6.23744 15.1768 5.76256 15.1768 5.46967 15.4697L2.46967 18.4697C2.32322 18.6161 2.25 18.8081 2.25 19C2.25 19.1017 2.27024 19.1987 2.30691 19.2871C2.34351 19.3755 2.39776 19.4584 2.46967 19.5303L5.46967 22.5303Z"
/>
</svg>
);
const Random = () => (
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className="group">
<path
className="stroke-[rgba(0,0,0,0.7)] group-hover:stroke-[rgba(0,0,0,0.95)] transition-all"
d="M15.5 3H20.5V8"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
className="stroke-[rgba(0,0,0,0.7)] group-hover:stroke-[rgba(0,0,0,0.95)] transition-all"
d="M3.5 20L20.5 3"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
className="stroke-[rgba(0,0,0,0.7)] group-hover:stroke-[rgba(0,0,0,0.95)] transition-all"
d="M20.5 16V21H15.5"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
className="stroke-[rgba(0,0,0,0.7)] group-hover:stroke-[rgba(0,0,0,0.95)] transition-all"
d="M14.5 15L20.5 21"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
className="stroke-[rgba(0,0,0,0.7)] group-hover:stroke-[rgba(0,0,0,0.95)] transition-all"
d="M3.5 4L8.5 9"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
const SingleLoop = () => (
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className="group">
<path
className="fill-[rgba(0,0,0,70)] group-hover:fill-[rgba(0,0,0,0.95)] transition-all"
fillRule="evenodd"
clipRule="evenodd"
d="M18.5303 1.46967C18.2374 1.17678 17.7626 1.17678 17.4697 1.46967C17.1768 1.76256 17.1768 2.23744 17.4697 2.53033L19.1893 4.25H8C4.82436 4.25 2.25 6.82436 2.25 10V16C2.25 19.1756 4.82436 21.75 8 21.75H16C19.1757 21.75 21.75 19.1755 21.75 15.9999V14.5C21.75 14.0858 21.4142 13.75 21 13.75C20.5858 13.75 20.25 14.0858 20.25 14.5V15.9999C20.25 18.3471 18.3472 20.25 16 20.25H8C5.65279 20.25 3.75 18.3472 3.75 16V10C3.75 7.65279 5.65279 5.75 8 5.75H19.1893L17.4697 7.46967C17.1768 7.76256 17.1768 8.23744 17.4697 8.53033C17.7626 8.82322 18.2374 8.82322 18.5303 8.53033L21.5303 5.53033C21.6768 5.38388 21.75 5.19194 21.75 5C21.75 4.89831 21.7298 4.80134 21.6931 4.71291C21.6565 4.62445 21.6022 4.54158 21.5303 4.46967L18.5303 1.46967Z"
/>
<path
className="fill-[rgba(0,0,0,70)] group-hover:fill-[rgba(0,0,0,0.95)] transition-all"
fillRule="evenodd"
clipRule="evenodd"
d="M12.6707 9.12895C12.8768 9.26848 13.0002 9.50114 13.0002 9.75V16.25C13.0002 16.6642 12.6644 17 12.2502 17C11.836 17 11.5002 16.6642 11.5002 16.25V10.8578L10.0287 11.4464C9.64415 11.6002 9.20768 11.4131 9.05384 11.0285C8.90001 10.6439 9.08707 10.2075 9.47166 10.0536L11.9717 9.05365C12.2027 8.96122 12.4646 8.98943 12.6707 9.12895Z"
/>
</svg>
);

@ -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 (
<Slider.Root
value={[value]}
max={duration}
className={`${styles.ProgressBarRoot} ${className}`}
onValueChange={(value: number[]) => handleChange(value[0])}
>
<Slider.Track className={`${styles.ProgressBarTrack}`}>
<Slider.Range className={`${styles.ProgressBarRange}`} />
</Slider.Track>
<Slider.Thumb className={`${styles.ProgressBarThumb}`} />
</Slider.Root>
);
}

@ -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<boolean>(false);
const handleClick = () => {
onChange(value > 0 ? 0 : 50);
};
return (
<div
className={`flex flex-col justify-between relative w-22px cursor-pointer ${hover ? 'h-173px' : 'h-22px'} ${className}`}
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
>
<VolumeBar value={value} className={`${!hover && '!hidden'} transition-all`} onChange={onChange} />
<ButtonProvide className="group" onClick={handleClick}>
{value >= 50 ? (
<VolumeFull hover={hover} />
) : value === 0 ? (
<VolumeOff hover={hover} />
) : (
<VolumeLow hover={hover} />
)}
</ButtonProvide>
</div>
);
}
const VolumeLow = ({ hover }: { hover: boolean }) => (
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
className="transition-all"
d="M11 4L6 8.57143H2V15.4286H6L11 20V4Z"
stroke={hover ? 'rgba(0,0,0,0.95)' : 'rgba(0,0,0,0.7)'}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
className="transition-all"
d="M15.54 8.46C16.4773 9.39764 17.0039 10.6692 17.0039 11.995C17.0039 13.3208 16.4773 14.5924 15.54 15.53"
stroke="black"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
const VolumeOff = ({ hover }: { hover: boolean }) => (
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
className="transition-all"
d="M11 4L6 8.57143H2V15.4286H6L11 20V4Z"
stroke={hover ? 'rgba(0,0,0,0.95)' : 'rgba(0,0,0,0.7)'}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
className="transition-all"
d="M23 9L17 15"
stroke={hover ? 'rgba(0,0,0,0.95)' : 'rgba(0,0,0,0.7)'}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
className="transition-all"
d="M17 9L23 15"
stroke={hover ? 'rgba(0,0,0,0.95)' : 'rgba(0,0,0,0.7)'}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
const VolumeFull = ({ hover }: { hover: boolean }) => (
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
className="transition-all"
d="M11 4L6 8.57143H2V15.4286H6L11 20V4Z"
stroke={hover ? 'rgba(0,0,0,0.95)' : 'rgba(0,0,0,0.7)'}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
className="transition-all"
d="M19.07 4.93C20.9447 6.80528 21.9979 9.34836 21.9979 12C21.9979 14.6516 20.9447 17.1947 19.07 19.07M15.54 8.46C16.4774 9.39764 17.0039 10.6692 17.0039 11.995C17.0039 13.3208 16.4774 14.5924 15.54 15.53"
stroke={hover ? 'rgba(0,0,0,0.95)' : 'rgba(0,0,0,0.7)'}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);

@ -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 (
<SliderPrimitive.Root
value={[value]}
orientation="vertical"
className={`${styles.VolumeBarRoot} ${className}`}
onValueChange={(value: number[]) => handleChange(value[0])}
>
<SliderPrimitive.Track className={`${styles.VolumeBarTrack}`}>
<SliderPrimitive.Range className={`${styles.VolumeBarRange}`} />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb className={`${styles.VolumeBarThumb}`} />
</SliderPrimitive.Root>
);
}

@ -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);
}

@ -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 (
<ButtonProvide className={`group ${className}`} onClick={onClick}>
{playState ? (
<svg width={size} height={size} viewBox="0 0 54 54" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="27" cy="27" r="27" className={`fill-brand group-hover:fill-#B13232 transition-all`} />
<rect x="20.7" y="18.9" width="4.5" height="16.2" rx="0.736" fill="white" />
<rect x="28.8" y="18.9" width="4.5" height="16.2" rx="0.736" fill="white" />
</svg>
) : (
<svg width={size} height={size} viewBox="0 0 54 54" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="27" cy="27" r="27" className={`fill-brand group-hover:fill-#B13232 transition-all`} />
<path
fillRule="evenodd"
clipRule="evenodd"
d="M20.0103 10.2691C21.3409 11.0395 21.3409 12.9605 20.0103 13.7309L7.00207 21.262C5.66874 22.0339 4 21.0718 4 19.5311L4 4.4689C4 2.92823 5.66874 1.96611 7.00207 2.73804L20.0103 10.2691Z"
fill="white"
fillOpacity="0.95"
transform="translate(15, 15)"
/>
</svg>
)}
</ButtonProvide>
);
}

@ -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 ? (
<PlayerButton
size={60}
img={'/img/audio-player/pause.svg'}
onClick={() => handlePlay(false)}
return (
<PlayButton
className={className}
playState={isCurrentVol && playState}
size={60}
onClick={() => handlePlay(!(isCurrentVol && playState))}
/>
) : (
<PlayerButton size={60} img={'/img/audio-player/play.svg'} onClick={() => handlePlay(true)} className={className} />
);
}

@ -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';

Loading…
Cancel
Save