Merge branch 'dev'

feature/artists
mackt 7 months ago
commit 7def728351

@ -19,6 +19,7 @@
"@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-toast": "^1.1.5", "@radix-ui/react-toast": "^1.1.5",
"@types/qrcode": "^1.5.5", "@types/qrcode": "^1.5.5",
"@types/react-window": "^1.8.8",
"@types/uuid": "^9.0.8", "@types/uuid": "^9.0.8",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.1.0", "clsx": "^2.1.0",
@ -32,6 +33,7 @@
"qs": "^6.12.0", "qs": "^6.12.0",
"react": "^18", "react": "^18",
"react-dom": "^18", "react-dom": "^18",
"react-window": "^1.8.10",
"tailwind-merge": "^2.2.2", "tailwind-merge": "^2.2.2",
"uuid": "^9.0.1", "uuid": "^9.0.1",
"zustand": "^4.5.2" "zustand": "^4.5.2"

@ -5,7 +5,7 @@ import DownloadCard from '@/app/download/components/DownloadCard';
import type { Metadata } from 'next'; import type { Metadata } from 'next';
export const metadata: Metadata = { export const metadata: Metadata = {
title: '下载', title: 'APP下载',
}; };
const qrCodeList: Array<DownloadQrcodeCard> = [ const qrCodeList: Array<DownloadQrcodeCard> = [

@ -3,7 +3,7 @@ import Script from 'next/script';
import { Toaster } from '@/components/ui/toaster'; import { Toaster } from '@/components/ui/toaster';
import { Header, Footer, PlayerBar, ScrollTopButton } from '@/components'; import { Header, App, Footer, PlayerBar, ScrollTopButton } from '@/components';
import './globals.css'; import './globals.css';
@ -12,7 +12,8 @@ export const metadata: Metadata = {
template: '%s - 雀乐', template: '%s - 雀乐',
default: '雀乐', default: '雀乐',
}, },
description: '独立 不独于世', description:
'雀乐是由一群喜欢并热爱独立音乐的人共同创建的,我们致力于传播来自世界各地的独立音乐。我们秉着音乐里独立自主的精神表达内核,希望把雀乐建设成为泛华语地区最好的独立音乐传播平台。',
icons: { icons: {
icon: '/favicon.ico', icon: '/favicon.ico',
}, },
@ -31,6 +32,7 @@ export default function RootLayout({ children }: Readonly<{ children: React.Reac
return ( return (
<html lang="zn-ch" className="relative"> <html lang="zn-ch" className="relative">
<body> <body>
<App />
<Header className="absolute top-0" /> <Header className="absolute top-0" />
<div>{children}</div> <div>{children}</div>
<Footer /> <Footer />

@ -4,11 +4,17 @@ import { notFound } from 'next/navigation';
import { Category, JournalRecommendList, JournalItem, Pagination, RedirectCheck, ButtonFM } from '@/components'; import { Category, JournalRecommendList, JournalItem, Pagination, RedirectCheck, ButtonFM } from '@/components';
import { apiSearchCategoryList, apiGetJournalRecommendWithCollect, apiJournalList } from '@/services'; import { apiSearchCategoryList, apiGetJournalRecommendWithCollect, apiJournalList } from '@/services';
import type { Metadata } from 'next'; // export const metadata: Metadata = {
// title: '期刊',
export const metadata: Metadata = { // };
title: '期刊',
}; export async function generateMetadata({ params: { category } }: { params: { category?: string } }) {
if (category && category !== 'all') {
const categoryList = await getCategoryList();
const categoryInfo: Category | undefined = categoryList.find((item: Category) => item.nameEn === category);
return { title: categoryInfo?.nameCh, description: categoryInfo?.description };
}
}
const getCategoryList = async () => { const getCategoryList = async () => {
const result = await apiSearchCategoryList(); const result = await apiSearchCategoryList();

@ -6,8 +6,8 @@ import { apiGetJournalInfoById, apiGetSongsByJournalNo, apiGetJournalRecommendBy
export async function generateMetadata({ params: { journalId } }: { params: { journalId: string } }) { export async function generateMetadata({ params: { journalId } }: { params: { journalId: string } }) {
const res = await apiGetJournalInfoById({ id: journalId }); const res = await apiGetJournalInfoById({ id: journalId });
const title = res.code === 200 ? res.data.title : '期刊详情'; const journalInfo = res.data;
return { title }; return { title: journalInfo.title, description: journalInfo.summary };
} }
const getData = async (journalId: string) => { const getData = async (journalId: string) => {
@ -34,52 +34,45 @@ export default async function JournalDetail({
} }
return ( return (
<main className="w-[1200px] mx-auto flex flex-row justify-between pt-[145px]"> <main className="w-[1200px] mx-auto flex flex-row justify-between pt-[120px]">
<div className="w-[712px]"> <div className="w-[712px]">
{/* 封面 */} {/* 封面 */}
<VolDetailCoverCard journalInfo={journalInfo} songList={songList} /> <VolDetailCoverCard journalInfo={journalInfo} songList={songList} />
{/* 期刊号 & 标签 */} <div className="flex justify-between items-center w-full mt-[30px]">
<div className="flex flex-row items-center gap-[9px] mt-[30px]"> {/* 期刊号 & 标签 */}
<div className="text-[rgba(0,0,0,0.7)] text-[14px] leading-[19.6px]">{`VOL.${journalInfo?.journalNo.toString().padStart(4, '0')}`}</div> <div className="flex flex-row items-center gap-[9px]">
{journalInfo?.tags.length > 0 && <div className="text-[rgba(0,0,0,0.7)] text-14px leading-20px">{`VOL.${journalInfo?.journalNo.toString().padStart(4, '0')}`}</div>
journalInfo.tags.map((tag: string) => ( {journalInfo?.tags.length > 0 &&
// <Link journalInfo.tags.map((tag: string) => (
// href={`/music/${tag}`} <span
// key={tag} key={tag}
// className="block w-auto py-[3px] px-[10px] rounded-[15px] bg-[rgba(0,0,0,0.05)] text-[rgba(0,0,0,0.7)] text-[12px] leading-[12px]" className="block w-fit py-[3px] px-[10px] rounded-[15px] bg-[rgba(0,0,0,0.05)] text-[rgba(0,0,0,0.7)] text-[12px] leading-[12px]"
// > >
// {tag} {tag}
// </Link> </span>
))}
</div>
<span {/* 收藏期刊 */}
key={tag} <CollectButton
className="block w-fit py-[3px] px-[10px] rounded-[15px] bg-[rgba(0,0,0,0.05)] text-[rgba(0,0,0,0.7)] text-[12px] leading-[12px]" showText
> id={journalInfo.id}
{tag} active={journalInfo.haveCollect}
</span> count={journalInfo.userCollectCount}
))} text="人收藏"
collectType="1"
iconPosition="right"
gap={9}
/>
</div> </div>
<div className="flex flex-row justify-between w-full h-auto"> <div className="flex flex-row justify-between w-full h-auto">
{/* 标题 */} {/* 标题 */}
<div className="w-full"> <div className="w-full">
<div className="flex flex-row justify-between items-center w-full h-auto"> <div className="w-autp text-[24px] leading-[33.6px] my-[9px] text-base font-bold text-overflow">
<div className="w-autp text-[24px] leading-[33.6px] my-[9px] text-base font-bold text-overflow"> {journalInfo.title}
{journalInfo.title}
</div>
{/* 收藏期刊 */}
<CollectButton
showText
id={journalInfo.id}
active={journalInfo.haveCollect}
count={journalInfo.userCollectCount}
text="人收藏"
collectType="1"
iconPosition="right"
gap={9}
/>
</div> </div>
{/* 作者 & 时间 */} {/* 作者 & 时间 */}
<div className="flex flex-row item-center text-[rgba(0,0,0,0.4)] text-[12px] leading-[16.8px] gap-[12px]"> <div className="flex flex-row item-center text-[rgba(0,0,0,0.4)] text-[12px] leading-[16.8px] gap-[12px]">
<p>{journalInfo.editor}</p> <p>{journalInfo.editor}</p>

@ -0,0 +1,21 @@
'use client';
import { useEffect } from 'react';
import { useShallow } from 'zustand/react/shallow';
import { useUserStore } from '@/store';
export default function APP() {
const { getUserInfo } = useUserStore(
useShallow((state) => ({
getUserInfo: state.getUserInfo,
})),
);
useEffect(() => {
getUserInfo();
}, []);
return <div></div>;
}

@ -5,13 +5,20 @@
*/ */
import { useState, useRef, useEffect } from 'react'; import { useState, useRef, useEffect } from 'react';
import Image from 'next/image';
import { useShallow } from 'zustand/react/shallow'; import { useShallow } from 'zustand/react/shallow';
import { useToast } from '@/components/ui/use-toast'; 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 { useAudioStore } from '@/store';
import { secondToDate } from '@/utils';
export default function AudioPlayer({ export default function AudioPlayer({
className, className,
@ -46,6 +53,7 @@ export default function AudioPlayer({
const isReady = useRef<boolean>(false); // 是否加载过组件 const isReady = useRef<boolean>(false); // 是否加载过组件
const [trackProgress, setTrackProgress] = useState<number>(0); // 音频进度(s) const [trackProgress, setTrackProgress] = useState<number>(0); // 音频进度(s)
const [duration, setDuration] = useState<number>(0); // 音频总时长(s) const [duration, setDuration] = useState<number>(0); // 音频总时长(s)
const [volume, setVolume] = useState<number>(50); // 音频进度(s)
/** /**
* @description * @description
@ -96,8 +104,8 @@ export default function AudioPlayer({
}; };
// 调整播放进度 // 调整播放进度
const handleChangeProgress = (value: string) => { const handleChangeProgress = (value: number) => {
audioRef.current.currentTime = Number(value); audioRef.current.currentTime = value;
setTrackProgress(audioRef.current.currentTime); setTrackProgress(audioRef.current.currentTime);
setPlayState(true); setPlayState(true);
}; };
@ -113,6 +121,16 @@ export default function AudioPlayer({
setTrackProgress(audioRef.current.currentTime); setTrackProgress(audioRef.current.currentTime);
}; };
// 调节音量
const handleChangeVolumn = (value: number) => {
setVolume(value);
};
// 播放/暂停事件
useEffect(() => {
audioRef.current.volume = volume / 100;
}, [volume]);
// 播放/暂停事件 // 播放/暂停事件
useEffect(() => { useEffect(() => {
playState ? audioRef.current.play() : audioRef.current.pause(); playState ? audioRef.current.play() : audioRef.current.pause();
@ -159,21 +177,73 @@ export default function AudioPlayer({
}, []); }, []);
return ( return (
<PlayerControl <div className={`flex justify-between w-1200px h-full pt-6px ${className}`}>
showOrder={playListInfo.type !== 'fm'} {/* left */}
playStatus={playState} <div className="flex flex-row w-fit h-fit mt-22px">
order={playOrder} {/* 专辑封面 */}
audio={audio} <div
duration={duration} 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}`}
showCard={showCard} onClick={onSwitchShowCard}
onPlay={handlePlay} >
onPrev={() => handleSwitchAudio(-1)} {audio?.pic && (
onNext={() => handleSwitchAudio(1)} <Image src={audio.pic} alt="music" width={48} height={48} unoptimized className="w-full h-full" />
onOrder={handleChangeOrder} )}
className={className} </div>
trackProgress={trackProgress}
onChangeProgress={handleChangeProgress} {/* 歌曲信息 */}
onSwitchShowCard={onSwitchShowCard} <div className="w-183px ml-15px cursor-default">
/> {/* <AutoScrollContainer key={audio?.id} auto hover width="140px" speed={50}> */}
{/* <div className="w-auto h-auto"> */}
<p className="mt-2px text-15px leading-24px text-base text-overflow">{audio?.title ?? ''}</p>
<p className="text-12px leading-18px mt-2px text-[rgba(0,0,0,0.7)] text-overflow">{`${audio?.artist}/${audio?.album}`}</p>
{/* </div> */}
{/* </AutoScrollContainer> */}
</div>
</div>
{/* center */}
<div className="flex flex-col items-center h-full">
{/* 按钮 */}
<div className="flex items-center gap-18px h-fit mt-9px">
<NextButton className="rotate-180" onClick={() => handleSwitchAudio(-1)} />
<PlayButton playState={playState} size={38} onClick={handlePlay} />
<NextButton onClick={() => handleSwitchAudio(1)} />
</div>
{/* 播放进度 */}
<div className="flex items-center justify-between h-fit mt-6px gap-6px">
{/* 播放时长 */}
<span className="block w-27px text-11px leading-14px text-right text-#000">
{secondToDate(trackProgress)}
</span>
{/* 进度条 */}
<ProgressBar
value={trackProgress}
duration={duration}
onChange={(value: number) => handleChangeProgress(value)}
className="w-710px"
/>
{/* 总时长 */}
<span className="block w-27px text-11px leading-14px text-left text-#000">{secondToDate(duration || 0)}</span>
</div>
</div>
{/* right */}
<div className="flex items-end h-full">
<div className="flex h-full items-end h-fit mb-21px gap-18px">
{/* 收藏歌曲 */}
{<CollectButton id={audio?.id ?? '0'} active={audio?.haveCollect ?? false} collectType="0" color="dark" />}
{/* 播放顺序 */}
<OrderButton
order={playOrder}
onClick={handleChangeOrder}
className={`mb-1px ${playListInfo.type === 'fm' && 'hidden'}`}
/>
<VolumeButton value={volume} onChange={handleChangeVolumn} className="mb-1px" />
</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-auto z-10 ${className}`}>
{/* 播放器 */} {/* 播放器 */}
<div <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-[88px] 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 }} style={{ bottom: audioId || showCard ? 0 : -80 }}
> >
<Player className="mx-auto" onSwitchShowCard={() => setShowCard(!showCard)} /> <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,4 +1,4 @@
import { useState, useEffect } from 'react'; import { useState, useEffect, useRef } from 'react';
import Image from 'next/image'; import Image from 'next/image';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
@ -8,8 +8,9 @@ import clientHttp from '@/utils/request/client';
import styles from './index.module.css'; import styles from './index.module.css';
import Lrc from './Lrc'; import Lrc from './Lrc';
import SongCardList from './widget/SongList';
import { Header, SongCardList } from '@/components'; import { Header } from '@/components';
import { useAudioStore } from '@/store'; import { useAudioStore } from '@/store';
interface Prop { interface Prop {
@ -32,8 +33,10 @@ export default function PlayerCard({ show, className }: Prop) {
}; };
}), }),
); );
const rightRef = useRef<HTMLDivElement>(null);
const [lrc, setLrc] = useState<string>(''); const [lrc, setLrc] = useState<string>('');
const [height, setHeight] = useState<number>(100);
const miniClass = 'translate-y-100vh'; // 缩小时的样式 const miniClass = 'translate-y-100vh'; // 缩小时的样式
const largeClass = 'translate-y-0'; // 放大后的样式 const largeClass = 'translate-y-0'; // 放大后的样式
@ -69,13 +72,17 @@ export default function PlayerCard({ show, className }: Prop) {
getLrc(audioInfo?.lrc ?? ''); getLrc(audioInfo?.lrc ?? '');
}, [audioInfo]); }, [audioInfo]);
useEffect(() => {
if (rightRef.current?.offsetHeight) setHeight(rightRef.current?.offsetHeight - 24 - 15);
}, []);
return ( return (
<div <div
className={`w-100vw h-100vh bg-[#fff] transition-all duration-300 z-9 ${show ? largeClass : miniClass} ${className}`} className={`w-100vw h-100vh pb-150px bg-[#fff] transition-all duration-300 z-9 ${show ? largeClass : miniClass} ${className}`}
> >
<Header type="client" /> <Header type="client" />
<div className="flex justify-between w-[1200px] mx-auto "> <div className="flex justify-between w-1200px h-[calc(100vh_-_80px_-_88px)] pb-60px mx-auto ">
{/* 单曲信息 */} {/* 单曲信息 */}
<div className="mt-68px"> <div className="mt-68px">
{/* 专辑封面区域 */} {/* 专辑封面区域 */}
@ -127,7 +134,7 @@ export default function PlayerCard({ show, className }: Prop) {
</div> </div>
{/* 歌词 */} {/* 歌词 */}
<div className={`mt-[30px] w-[340px]`} style={{ height: 'calc(100vh - 80px - 460px - 185px)' }}> <div className={`mt-[30px] w-[340px]`} style={{ height: 'calc(100vh - 540px - 60px - 88px)' }}>
{lrc ? ( {lrc ? (
<Lrc lrc={lrc} className="h-full" /> <Lrc lrc={lrc} className="h-full" />
) : ( ) : (
@ -137,17 +144,13 @@ export default function PlayerCard({ show, className }: Prop) {
</div> </div>
{/* 播放列表 */} {/* 播放列表 */}
<div className="w-728px mt-49px"> <div className="flex flex-col items-start w-728px h-auto mt-49px" ref={rightRef}>
<button onClick={handleClearPlayQueue}></button> <button onClick={handleClearPlayQueue} className="h-24px text-17px leading-24px text-#000/95">
<div
className={`w-full mt-[15px] ml-[-18px] pr-14px !overflow-x-hidden overflow-y-auto ${styles.playQueue}`} </button>
style={{ height: 'calc(100vh - 168px - 140px - 80px)' }}
> <div className="w-full mt-15px ml-[-18px] overflow-hidden">
<SongCardList <SongCardList height={height} songList={playQueue} listInfo={{ type: 'playerCard', id: null }} />
className="w-[712px] mt-11px"
songList={playQueue}
listInfo={{ type: 'playerCard', id: null }}
/>
</div> </div>
</div> </div>
</div> </div>

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

@ -31,42 +31,8 @@
border-radius: 3px; border-radius: 3px;
} }
.range-input {
-webkit-appearance: none;
width: 100%;
height: 2px;
border-radius: 5px; /* 圆角 */
background: ##181818; /* 背景颜色 */
outline: none; /* 去除默认的外边框 */
opacity: 1; /* 不选中时的透明度 */
-webkit-transition: 0.2s; /* 过渡效果 */
transition: opacity 0.2s;
}
.range-input:hover {
opacity: 0.9; /* 鼠标悬停时的透明度 */
}
.range-input::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 10px; /* 拇指的宽度 */
height: 10px; /* 拇指的高度 */
border-radius: 50%; /* 将拇指变为圆形 */
background: #181818; /* 拇指的颜色 */
cursor: pointer; /* 鼠标样式 */
}
/** 滑块 样式 */
.range-input::-moz-range-thumb {
width: 10px;
height: 10px;
border-radius: 50%;
background: #181818;
cursor: pointer;
}
/* 歌词滚动样式 */ /* 歌词滚动样式 */
/****************************************/
.lrc::-webkit-scrollbar { .lrc::-webkit-scrollbar {
width: 4px; /* 滚动条宽度 */ width: 4px; /* 滚动条宽度 */
} }
@ -94,6 +60,7 @@
} }
/* 播放列表滚动样式 */ /* 播放列表滚动样式 */
/****************************************/
.playQueue::-webkit-scrollbar { .playQueue::-webkit-scrollbar {
width: 4px; /* 滚动条宽度 */ width: 4px; /* 滚动条宽度 */
} }

@ -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={`group ${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">
<path
className="fill-#000/70 group-hover:fill-#000/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 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-#000/70 group-hover:fill-#000/95 transition-all"
fillRule="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">
<path
className="stroke-[rgba(0,0,0,0.7)] group-hover:stroke-#000/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-#000/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-#000/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-#000/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-#000/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">
<path
className="fill-#000/70 group-hover:fill-#000/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-#000/70 group-hover:fill-#000/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,97 @@
/**
*
*/
'use client';
import React from 'react';
import { VariableSizeList as List } from 'react-window';
import { useShallow } from 'zustand/react/shallow';
import styles from './index.module.css';
import { SongCard } from '@/components';
import { useAudioStore } from '@/store';
interface Props {
height: number;
/**
* vol:
* collectSingle:
* playerCard:
*/
listInfo: { type: 'vol' | 'collectSingle' | 'playerCard'; id: string | null };
songList: SongInfo[];
/** 收藏按钮的显示逻辑 always: 总是显示 playing: 播放时显示 */
collectType?: 'always' | 'playing';
className?: string;
style?: React.CSSProperties;
}
export default function SongCardList({ height, listInfo, songList, className, collectType = 'always', style }: Props) {
const { playState, audioId, playListInfo, playQueue, setPlayState, setPlayListInfo, setPlayList, setAudioId } =
useAudioStore(
useShallow((state) => ({
playState: state.playState,
audioId: state.audioId,
playListInfo: state.playListInfo,
playQueue: state.playQueue,
setPlayState: state.setPlayState,
setPlayListInfo: state.setPlayListInfo,
setPlayList: state.setPlayList,
setAudioId: state.setAudioId,
})),
);
const handlePlayList = (id: string) => {
if (!id) return;
// 如果是播放中的歌曲,改变播放状态
if (id === audioId) {
setPlayState(!playState);
return;
}
const isPlayingList = listInfo.type === playListInfo.type && listInfo.id === playListInfo.id; // 正在播放当前歌单
const isInQueue = playQueue.some((item) => item.id === id); // 所选的歌在队列中
if ((isPlayingList && isInQueue) || listInfo.type === 'playerCard') {
setAudioId(id);
return;
}
// 重新设置播放列表
const res = setPlayListInfo(listInfo as IPlayListInfo);
if (!res) return;
setPlayList(songList);
setAudioId(id);
};
function Item({ index }: { index: number }) {
const item = songList[index];
return (
<div>
<SongCard
key={item?.id}
{...item}
playState={playState}
showEq={item?.id === audioId}
onPlay={handlePlayList}
showCollect={collectType === 'always' || (collectType === 'playing' && item.id === audioId)}
/>
</div>
);
}
return (
<List
itemCount={playQueue.length}
height={height}
itemSize={() => 72}
width={712}
className={`pr-14px ${styles.SongList} ${className}`}
style={style}
>
{Item}
</List>
);
}

@ -0,0 +1,114 @@
'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-148px' : '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,35 @@
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 (
<div
className={`relative flex items-center w-18px h-118px rounded-18px bg-#fff ${styles.VolumeBarContainer} ${className}`}
>
<SliderPrimitive.Root
value={[value]}
orientation="vertical"
className={`flex flex-col items-center w-full h-107px ${styles.VolumeBarRoot}`}
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>
</div>
);
}

@ -0,0 +1,128 @@
/* 播放进度条 */
.ProgressBarRoot {
position: relative;
display: flex;
align-items: center;
user-select: none;
touch-action: none;
height: 11px;
cursor: pointer;
}
.ProgressBarTrack {
background-color: rgba(0, 0, 0, 0.1);
position: relative;
flex-grow: 1;
height: 2px;
border-radius: 2px;
overflow: hidden;
}
.ProgressBarRange {
position: absolute;
background-color: #181818;
height: 2px;
}
.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;
height: 102px;
border-radius: 37px;
}
.VolumeBarTrack {
position: relative;
flex-grow: 1;
background-color: rgba(0, 0, 0, 0.1);
}
.VolumeBarTrack[data-orientation='vertical'] {
width: 2px;
border-radius: 2px;
}
/* 激活区域 */
.VolumeBarRange {
position: absolute;
background-color: #646464;
}
.VolumeBarRange[data-orientation='vertical'] {
width: 100%;
border-radius: 2px;
}
/* 滑块 */
.VolumeBarThumb {
display: block;
width: 10px;
height: 10px;
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);
}
.VolumeBarContainer {
box-shadow: 0px 6px 34px rgba(0, 0, 0, 0.1);
/* overflow: hidden; */
}
.VolumeBarContainer::before {
content: '';
display: block;
position: absolute;
left: calc(-50% - 0.7px);
top: calc(-50% - 0.7px);
border-radius: 24px;
width: calc(200% + 1.4px); /* 考虑到阴影会占用额外空间,增加 1.4px */
height: calc(200% + 1.4px); /* 考虑到阴影会占用额外空间,增加 1.4px */
box-shadow: 0 0 0 0.7px #e1e1e1; /* 使用阴影模拟边框 */
transform: scale(0.5);
pointer-events: none; /** 防止 userCard 点击事件失效 */
overflow: hidden;
}
.SongList::-webkit-scrollbar {
width: 4px;
}
.SongList::-webkit-scrollbar-track {
opacity: 1;
}
.SongList::-webkit-scrollbar-thumb {
border-radius: 3px;
background: rgba(0, 0, 0, 0.05);
transition: all 0.3s;
}
.SongList::-webkit-scrollbar-thumb:hover {
background: rgba(0, 0, 0, 0.3);
}

@ -43,7 +43,7 @@ export default function ButtonFM() {
if (res.code === 200 && res.data.length > 0) { if (res.code === 200 && res.data.length > 0) {
const setRes = setPlayListInfo({ const setRes = setPlayListInfo({
type: 'fm', type: 'fm',
id: null, id: 'fm',
}); });
if (!setRes) return; if (!setRes) return;
setPlayList(res.data); setPlayList(res.data);

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

@ -13,8 +13,6 @@ export default function Auth() {
const { userInfo } = useUserStore( const { userInfo } = useUserStore(
useShallow((state) => ({ useShallow((state) => ({
userInfo: state.userInfo, userInfo: state.userInfo,
showLogin: state.showLogin,
setShowLogin: state.setShowLogin,
})), })),
); );

@ -8,7 +8,7 @@
position: absolute; position: absolute;
left: calc(-50% - 0.7px); left: calc(-50% - 0.7px);
top: calc(-50% - 0.7px); top: calc(-50% - 0.7px);
border-radius: 24px; border-radius: 18px;
width: calc(200% + 1.4px); /* 考虑到阴影会占用额外空间,增加 1.4px */ width: calc(200% + 1.4px); /* 考虑到阴影会占用额外空间,增加 1.4px */
height: calc(200% + 1.4px); /* 考虑到阴影会占用额外空间,增加 1.4px */ height: calc(200% + 1.4px); /* 考虑到阴影会占用额外空间,增加 1.4px */
box-shadow: 0 0 0 0.7px #e1e1e1; /* 使用阴影模拟边框 */ box-shadow: 0 0 0 0.7px #e1e1e1; /* 使用阴影模拟边框 */

@ -9,12 +9,16 @@
.color-default { .color-default {
--default-color: rgb(197, 197, 197); --default-color: rgb(197, 197, 197);
--active-color: #c43737; --hover-color: var(--primary-color);
--active-color: var(--primary-color);
--active-hover-color: var(--primary-color);
} }
.color-dark { .color-dark {
--default-color: rgba(0, 0, 0, 0.95); --default-color: rgba(0, 0, 0, 0.7);
--active-color: #c43737; --hover-color: rgba(0, 0, 0, 0.95);
--active-color: var(--primary-color);
--active-hover-color: var(--primary-color);
} }
.heart-svg { .heart-svg {
@ -47,8 +51,12 @@
fill: var(--active-color); fill: var(--active-color);
} }
.checkbox[type='checkbox']:checked:hover + svg .heart-path {
stroke: var(--active-hover-color);
}
.checkbox[type='checkbox']:hover + svg .heart-path { .checkbox[type='checkbox']:hover + svg .heart-path {
stroke: var(--active-color); stroke: var(--hover-color);
transition: stroke 0.2s ease-in-out; transition: stroke 0.2s ease-in-out;
} }

@ -19,13 +19,15 @@ export default function JournalCard({
title, title,
journalNo, journalNo,
showPlay = false, showPlay = false,
scale = false,
children, children,
className, className,
}: { }: {
image: string; image: string;
title?: string;
journalNo: string; journalNo: string;
title?: string;
showPlay?: boolean; showPlay?: boolean;
scale?: boolean;
children?: React.ReactNode; children?: React.ReactNode;
className?: string; className?: string;
}) { }) {
@ -41,7 +43,7 @@ export default function JournalCard({
src={image} src={image}
unoptimized unoptimized
alt={`Vol${journalNo}`} alt={`Vol${journalNo}`}
className={`absolute bottom-0 group-hover:transform-scale-103 transition-transform-300`} className={`absolute bottom-0 ${scale && 'group-hover:transform-scale-103 transition-transform-300'}`}
/> />
{/* 标题 */} {/* 标题 */}
{!!title && ( {!!title && (

@ -8,7 +8,7 @@ export default function CoverCard({ journalInfo, songList }: { journalInfo: Jour
return ( return (
<div className="relative"> <div className="relative">
<JournalCard image={image} journalNo={journalNo} /> <JournalCard image={image} journalNo={journalNo} />
<VolPlayButton className="absolute right-[30px] bottom-[-26px]" journalInfo={journalInfo} songList={songList} /> <VolPlayButton className="absolute left-30px bottom-30px" journalInfo={journalInfo} songList={songList} />
</div> </div>
); );
} }

@ -4,7 +4,7 @@ import { JournalCard } from '@/components';
export default function CoverCard({ title, img, no }: { title: string; img: string; no: string }) { export default function CoverCard({ title, img, no }: { title: string; img: string; no: string }) {
return ( return (
<JournalCard image={img} journalNo={no} className="cursor-pointer"> <JournalCard image={img} journalNo={no} scale className="cursor-pointer">
<div className={`absolute bottom-0 w-full h-[126px] ${styles.title_background}`}> <div className={`absolute bottom-0 w-full h-[126px] ${styles.title_background}`}>
<p className="absolute bottom-[23px] left-[23px] text-[#fff] text-[20px] leading-[33.6px] font-500">{title}</p> <p className="absolute bottom-[23px] left-[23px] text-[#fff] text-[20px] leading-[33.6px] font-500">{title}</p>
</div> </div>

@ -2,7 +2,7 @@
import { useShallow } from 'zustand/react/shallow'; import { useShallow } from 'zustand/react/shallow';
import { PlayerButton } from '@/components'; import { PlayButton } from '@/components';
import { useAudioStore } from '@/store'; import { useAudioStore } from '@/store';
export default function VolPlayButton({ export default function VolPlayButton({
@ -44,14 +44,12 @@ export default function VolPlayButton({
setPlayState(!playState); setPlayState(!playState);
}; };
return isCurrentVol && playState ? ( return (
<PlayerButton <PlayButton
size={60}
img={'/img/audio-player/pause.svg'}
onClick={() => handlePlay(false)}
className={className} 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} />
); );
} }

@ -71,7 +71,7 @@ export default function LoginForm({ className }: { className?: string }) {
// 登录 // 登录
const handleLogin = async () => { const handleLogin = async () => {
setBtnLoading(true); setBtnLoading(true);
const uuid = localStorage.getItem('deviceId') || ''; const uuid = localStorage.getItem('deviceId') || 'error';
const result = await SmsLogin({ phone, authCode: authCode, deviceId: uuid }); const result = await SmsLogin({ phone, authCode: authCode, deviceId: uuid });
if (result.code === 200) { if (result.code === 200) {
setShowLogin(false); setShowLogin(false);

@ -9,6 +9,11 @@ import { SongCard } from '@/components';
import { useAudioStore } from '@/store'; import { useAudioStore } from '@/store';
interface Props { interface Props {
/**
* vol:
* collectSingle:
* playerCard:
*/
listInfo: { type: 'vol' | 'collectSingle' | 'playerCard'; id: string | null }; listInfo: { type: 'vol' | 'collectSingle' | 'playerCard'; id: string | null };
songList: SongInfo[]; songList: SongInfo[];
/** 收藏按钮的显示逻辑 always: 总是显示 playing: 播放时显示 */ /** 收藏按钮的显示逻辑 always: 总是显示 playing: 播放时显示 */
@ -42,7 +47,7 @@ export default function SongCardList({ listInfo, songList, className, collectTyp
const isPlayingList = listInfo.type === playListInfo.type && listInfo.id === playListInfo.id; // 正在播放当前歌单 const isPlayingList = listInfo.type === playListInfo.type && listInfo.id === playListInfo.id; // 正在播放当前歌单
const isInQueue = playQueue.some((item) => item.id === id); // 所选的歌在队列中 const isInQueue = playQueue.some((item) => item.id === id); // 所选的歌在队列中
if (isPlayingList && isInQueue) { if ((isPlayingList && isInQueue) || listInfo.type === 'playerCard') {
setAudioId(id); setAudioId(id);
return; return;
} }

@ -1,3 +1,4 @@
export { default as App } from './App';
export { default as Logo } from './Logo'; export { default as Logo } from './Logo';
export { default as Header } from './Header/Header'; export { default as Header } from './Header/Header';
export { default as Footer } from './Footer/Footer'; export { default as Footer } from './Footer/Footer';
@ -22,6 +23,7 @@ export { default as RedirectCheck } from './Login/RedirectCheck';
// Button // Button
export { default as CollectButton } from './Button/CollectButton'; export { default as CollectButton } from './Button/CollectButton';
export { default as ScrollTopButton } from './Button/ScrollTopButton'; export { default as ScrollTopButton } from './Button/ScrollTopButton';
export { default as PlayButton } from './Button/PlayButton';
export { default as ThumbButton } from './Button/ThumbButton'; export { default as ThumbButton } from './Button/ThumbButton';
export { default as ButtonFM } from './Button/ButtonFM'; export { default as ButtonFM } from './Button/ButtonFM';
@ -39,7 +41,7 @@ export { default as VolListCoverCard } from './Journal/JournalList/VolListCoverC
// Audio Player // Audio Player
export { default as PlayerBar } from './AudioPlayer/PlayerBar'; export { default as PlayerBar } from './AudioPlayer/PlayerBar';
export { default as AudioPlayer } from './AudioPlayer/Player'; export { default as AudioPlayer } from './AudioPlayer/Player';
export { default as PlayerButton } from './AudioPlayer/PlayerButton'; export { default as PlayerButton } from './AudioPlayer/widget/ButtonProvide';
// SongCard // SongCard
export { default as SongCard } from './SongCard/SongCard'; export { default as SongCard } from './SongCard/SongCard';

@ -245,7 +245,7 @@ const useAudioState = create<AuioState>()(
}; };
}, },
{ {
name: 'audioStore', name: 'audio',
storage: createJSONStorage(() => localStorage), storage: createJSONStorage(() => localStorage),
}, },
), ),

@ -1,3 +1,8 @@
/**
* UserInfo
* https://github.com/pmndrs/zustand/blob/main/docs/integrations/persisting-store-data.md#usage-in-nextjs
*/
import { produce } from 'immer'; import { produce } from 'immer';
// import { setCookie } from 'nookies'; // import { setCookie } from 'nookies';
import { create } from 'zustand'; import { create } from 'zustand';

@ -48,22 +48,5 @@ export default defineConfig({
colors: { colors: {
brand: 'var(--primary-color)', brand: 'var(--primary-color)',
}, },
extend: {
'border-half': {
position: 'relative',
'::after': {
content: '',
display: 'block',
position: 'absolute',
left: '-50%',
top: '-50%',
width: '200%',
height: '200%',
border: '1px solid red',
transform: 'scale(0.5)',
'-webkit-transform': 'scale(0.5)',
},
},
},
}, },
}); });

@ -275,7 +275,7 @@
"@babel/plugin-transform-modules-commonjs" "^7.24.1" "@babel/plugin-transform-modules-commonjs" "^7.24.1"
"@babel/plugin-transform-typescript" "^7.24.1" "@babel/plugin-transform-typescript" "^7.24.1"
"@babel/runtime@^7.13.10", "@babel/runtime@^7.24.0": "@babel/runtime@^7.0.0", "@babel/runtime@^7.13.10", "@babel/runtime@^7.24.0":
version "7.24.4" version "7.24.4"
resolved "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.24.4.tgz#de795accd698007a66ba44add6cc86542aff1edd" resolved "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.24.4.tgz#de795accd698007a66ba44add6cc86542aff1edd"
integrity sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA== integrity sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA==
@ -1413,6 +1413,13 @@
dependencies: dependencies:
"@types/react" "*" "@types/react" "*"
"@types/react-window@^1.8.8":
version "1.8.8"
resolved "https://r.cnpmjs.org/@types/react-window/-/react-window-1.8.8.tgz#c20645414d142364fbe735818e1c1e0a145696e3"
integrity sha512-8Ls660bHR1AUA2kuRvVG9D/4XpRC6wjAaPT9dil7Ckc76eP9TKWZwwmgfq8Q1LANX3QNDnoU4Zp48A3w+zK69Q==
dependencies:
"@types/react" "*"
"@types/react@*", "@types/react@^18": "@types/react@*", "@types/react@^18":
version "18.2.65" version "18.2.65"
resolved "https://registry.npmmirror.com/@types/react/-/react-18.2.65.tgz" resolved "https://registry.npmmirror.com/@types/react/-/react-18.2.65.tgz"
@ -3830,6 +3837,11 @@ mdn-data@2.0.30:
resolved "https://registry.npmmirror.com/mdn-data/-/mdn-data-2.0.30.tgz" resolved "https://registry.npmmirror.com/mdn-data/-/mdn-data-2.0.30.tgz"
integrity sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA== integrity sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==
"memoize-one@>=3.1.1 <6":
version "5.2.1"
resolved "https://r2.cnpmjs.org/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e"
integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==
meow@^12.0.1: meow@^12.0.1:
version "12.1.1" version "12.1.1"
resolved "https://registry.npmmirror.com/meow/-/meow-12.1.1.tgz" resolved "https://registry.npmmirror.com/meow/-/meow-12.1.1.tgz"
@ -4632,6 +4644,14 @@ react-style-singleton@^2.2.1:
invariant "^2.2.4" invariant "^2.2.4"
tslib "^2.0.0" tslib "^2.0.0"
react-window@^1.8.10:
version "1.8.10"
resolved "https://r.cnpmjs.org/react-window/-/react-window-1.8.10.tgz#9e6b08548316814b443f7002b1cf8fd3a1bdde03"
integrity sha512-Y0Cx+dnU6NLa5/EvoHukUD0BklJ8qITCtVEPY1C/nL8wwoZ0b5aEw8Ff1dOVHw7fCzMt55XfJDd8S8W8LCaUCg==
dependencies:
"@babel/runtime" "^7.0.0"
memoize-one ">=3.1.1 <6"
react@^18: react@^18:
version "18.2.0" version "18.2.0"
resolved "https://registry.npmmirror.com/react/-/react-18.2.0.tgz" resolved "https://registry.npmmirror.com/react/-/react-18.2.0.tgz"

Loading…
Cancel
Save