feat(feature): Add Search.

feature/artists
mackt 7 months ago
parent 14a5be4f00
commit 58cf24abd3

@ -31,7 +31,6 @@
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
z-index: 30;
opacity: 0; opacity: 0;
} }

@ -3,14 +3,17 @@
import Auth from './Auth'; import Auth from './Auth';
import Nav from './Nav'; import Nav from './Nav';
import { Logo } from '@/components'; import { Logo, Search } from '@/components';
export default function Header({ type = 'server', className }: { type?: NextComponentType; className?: string }) { export default function Header({ type = 'server', className }: { type?: NextComponentType; className?: string }) {
return ( return (
<header className={`w-full h-[80px] z-2 ${className}`}> <header className={`w-full h-80px z-2 ${className}`}>
<div className="relative w-[1200px] h-full mx-auto flex items-center justify-between"> <div className="relative w-1200px h-full mx-auto flex items-center justify-between">
<Logo type={type} /> <div className="relative flex items-center h-full">
<div className="h-full flex items-center"> <Logo type={type} />
<Search className="ml-40px" />
</div>
<div className="flex items-center h-full">
<Nav type={type} /> <Nav type={type} />
<Auth /> <Auth />
</div> </div>

@ -42,7 +42,6 @@
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
z-index: 99;
opacity: 0; opacity: 0;
} }

@ -5,13 +5,15 @@ export default function MiniJournalCard({
image, image,
journalNo, journalNo,
title, title,
onClick,
}: { }: {
image: string; image: string;
journalNo: string; journalNo: string;
title: string; title: string;
onClick?: () => void;
}) { }) {
return ( return (
<Link href={`/vol/${journalNo}`}> <Link href={`/vol/${journalNo}`} onClick={onClick}>
<div className="w-162px h-147px group"> <div className="w-162px h-147px group">
<Image <Image
width={162} width={162}

@ -1,10 +1,22 @@
import MiniJournalCard from './MiniJournalCard'; import MiniJournalCard from './MiniJournalCard';
export default function MiniJournalCardList({ list, className }: { list: JournalInfo[]; className?: string }) { interface IProps {
list: JournalInfo[];
className?: string;
onClick: () => void;
}
export default function MiniJournalCardList({ list, className, onClick }: IProps) {
return ( return (
<div className={`grid grid-cols-4 gap-15px ${className}`}> <div className={`grid grid-cols-4 gap-15px ${className}`}>
{list.map((item) => ( {list.map((item) => (
<MiniJournalCard key={item.id} image={item.image} journalNo={item.journalNo} title={item.title} /> <MiniJournalCard
key={item.id}
image={item.image}
journalNo={item.journalNo}
title={item.title}
onClick={onClick}
/>
))} ))}
</div> </div>
); );

@ -0,0 +1,30 @@
import style from './index.module.css';
import Journal from './widget/Journal';
import NoData from './widget/NoData';
import Single from './widget/Single';
interface IProps {
journalData: JournalInfo[];
songData: SongInfo[];
className?: string;
onClose: () => void;
}
export default function SearchDropdown({ journalData, songData, className, onClose }: IProps) {
const haveData = journalData.length || songData.length;
return (
<div
className={`relative w-747px h-fit min-h-162px max-h-747px rounded-10px mt-10px pt-24px pb-40px pl-36px pr-18px border-1px border-#e1e1e1 bg-base z-11 overflow-auto ${style.dropdownScrollbar} ${className}`}
>
{haveData ? (
<div className="h-fit">
{!!songData.length && <Single data={songData} />}
{!!journalData.length && <Journal data={journalData} onClose={onClose} />}
</div>
) : (
<NoData className="w-full h-160px" />
)}
</div>
);
}

@ -0,0 +1,22 @@
interface IProps {
className?: string;
onInput: (value: string) => void;
onFocus: () => void;
}
import IconSearch from './widget/IconSearch';
export default function SearchInput({ className, onInput, onFocus }: IProps) {
return (
<div className={`relative w-208px h-38px border-1px border-#fff ${className}`}>
<input
className="w-full h-full px-45px text-13px leading-38px text-#000/95 bg-transparent outline-none rounded-47px"
onInput={(e: any) => onInput(e.target.value)}
onFocus={onFocus}
style={{ boxShadow: '0px 6px 34px 0px rgba(0, 0, 0, 0.1)' }}
/>
<IconSearch className="absolute top-8px left-16px" />
</div>
);
}

@ -0,0 +1,78 @@
'use client';
import { useRef, useState, useEffect } from 'react';
import debounce from 'lodash/debounce';
import Dropdown from './Dropdown';
import Input from './Input';
import { apiSearchJournal, apiSearchSong } from '@/services';
interface IProps {
className?: string;
}
export default function Search({ className }: IProps) {
const [value, setValue] = useState<string>('');
const [journalData, setJournalData] = useState<JournalInfo[]>([]);
const [songData, setSongData] = useState<SongInfo[]>([]);
const [showDropDown, setShowDropDown] = useState<boolean>(false);
const searchRef = useRef<HTMLDivElement>(null);
const handleInput: (value: string) => void = (value) => {
setShowDropDown(!!value.trim());
if (value.trim()) {
setValue(value);
handleSearch(value);
}
};
const handleSearch = async (keyword: string) => {
const [journalRes, songRes] = await Promise.all([
apiSearchJournal({ keyword, pageNum: 1, pageSize: 20 }),
apiSearchSong({ keyword, pageNum: 1, pageSize: 20 }),
]);
if (journalRes.code === 200) {
setJournalData(journalRes.data.rows);
} else {
setJournalData([]);
}
if (songRes.code === 200) {
setSongData(songRes.data.rows);
} else {
setSongData([]);
}
};
// input 聚焦时,展示下拉框
const handleInputFocus = () => {
if (value.trim()) setShowDropDown(true);
};
const handleClickOutside = (e: Event) => {
if (searchRef.current && !searchRef.current.contains(e.target as Node)) {
setShowDropDown(false);
}
};
useEffect(() => {
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
});
return (
<div ref={searchRef} className={`w-208px h-38px ${className}`}>
<Input onInput={debounce((value) => handleInput(value), 200)} onFocus={handleInputFocus} />
{showDropDown && (
<Dropdown journalData={journalData} songData={songData} onClose={() => setShowDropDown(false)} />
)}
</div>
);
}

@ -0,0 +1,21 @@
.dropdownScrollbar::-webkit-scrollbar {
width: 4px; /* 滚动条宽度 */
}
/* 滚动条轨道透明度 */
.dropdownScrollbar::-webkit-scrollbar-track {
margin: 24px 0 40px 0;
opacity: 0;
}
/* 滚动条滑块颜色 */
.dropdownScrollbar::-webkit-scrollbar-thumb {
border-radius: 5px;
background: rgba(0, 0, 0, 0.2);
transition: all 0.3s;
}
/* 滚动条滑块 hover */
.dropdownScrollbar::-webkit-scrollbar-thumb:hover {
background: rgba(0, 0, 0, 0.3);
}

@ -0,0 +1,20 @@
export default function IconSearch({ className }: { className?: string }) {
return (
<svg
width="22"
height="22"
viewBox="0 0 22 22"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10.9999 4.1189C7.17804 4.1189 4.0791 7.21785 4.0791 11.0397C4.0791 14.8616 7.17804 17.9606 10.9999 17.9606C12.5637 17.9606 14.0064 17.4418 15.1654 16.5669L16.7472 18.1487C17.0157 18.4172 17.451 18.4172 17.7195 18.1487C17.988 17.8803 17.988 17.445 17.7195 17.1765L16.1763 15.6333C17.2616 14.4113 17.9207 12.8024 17.9207 11.0397C17.9207 7.21785 14.8218 4.1189 10.9999 4.1189ZM5.4541 11.0397C5.4541 7.97724 7.93744 5.4939 10.9999 5.4939C14.0624 5.4939 16.5457 7.97724 16.5457 11.0397C16.5457 14.1022 14.0624 16.5856 10.9999 16.5856C7.93744 16.5856 5.4541 14.1022 5.4541 11.0397Z"
fill="black"
fillOpacity="0.95"
/>
</svg>
);
}

@ -0,0 +1,16 @@
import { MiniJournalCardList } from '@/components';
interface IProps {
data: JournalInfo[];
className?: string;
onClose: () => void;
}
export default function Journal({ data, className, onClose }: IProps) {
return (
<div className={className}>
<p className="mt-30px"></p>
<MiniJournalCardList list={data} className="w-full mt-18px" onClick={onClose} />
</div>
);
}

@ -0,0 +1,11 @@
interface IProps {
className?: string;
}
export default function NoData({ className }: IProps) {
return (
<div className={`flex justify-center items-center ${className}`}>
<p className="text-#000/70 text-14px leading-21px"></p>
</div>
);
}

@ -0,0 +1,21 @@
import { SongCardList } from '@/components';
interface IProps {
data: SongInfo[];
className?: string;
}
export default function Single({ data, className }: IProps) {
return (
<div className={className}>
<p></p>
<SongCardList
className="w-full mt-6px ml-[-16px]"
clickType="playPush"
songList={data}
listInfo={{ type: 'collectSingle', id: null }}
collectType="playing"
/>
</div>
);
}

@ -9,41 +9,68 @@ import { SongCard } from '@/components';
import { useAudioStore } from '@/store'; import { useAudioStore } from '@/store';
interface Props { interface Props {
/** /**
* @description
* vol: * vol:
* collectSingle: * collectSingle:
* playerCard: * playerCard:
*/ */
listInfo: { type: 'vol' | 'collectSingle' | 'playerCard'; id: string | null }; listInfo: { type: 'vol' | 'collectSingle' | 'playerCard'; id: string | null };
/**
* @description
* playList:
* playPush: push
*/
clickType?: 'playList' | 'playPush';
songList: SongInfo[]; songList: SongInfo[];
/** 收藏按钮的显示逻辑 always: 总是显示 playing: 播放时显示 */ /** 收藏按钮的显示逻辑 always: 总是显示 playing: 播放时显示 */
collectType?: 'always' | 'playing'; collectType?: 'always' | 'playing';
className?: string; className?: string;
} }
export default function SongCardList({ listInfo, songList, className, collectType = 'always' }: Props) { export default function SongCardList({
const { playState, audioId, playListInfo, playQueue, setPlayState, setPlayListInfo, setPlayList, setAudioId } = listInfo,
useAudioStore( songList,
useShallow((state) => ({ className,
playState: state.playState, collectType = 'always',
audioId: state.audioId, clickType = 'playList',
playListInfo: state.playListInfo, }: Props) {
playQueue: state.playQueue, const {
setPlayState: state.setPlayState, playState,
setPlayListInfo: state.setPlayListInfo, audioId,
setPlayList: state.setPlayList, playListInfo,
setAudioId: state.setAudioId, playList,
})), playQueue,
); setPlayState,
setPlayListInfo,
setPlayQueue,
setPlayList,
setAudioId,
} = useAudioStore(
useShallow((state) => ({
playState: state.playState,
audioId: state.audioId,
playListInfo: state.playListInfo,
playList: state.playList,
playQueue: state.playQueue,
setPlayState: state.setPlayState,
setPlayListInfo: state.setPlayListInfo,
setPlayList: state.setPlayList,
setPlayQueue: state.setPlayQueue,
setAudioId: state.setAudioId,
})),
);
const handlePlayList = (id: string) => { const handlePlay = (id: string) => {
if (!id) return; if (!id) return;
// 如果是播放中的歌曲,改变播放状态 // 如果是播放中的歌曲,改变播放状态
if (id === audioId) { if (id === audioId) {
setPlayState(!playState); setPlayState(!playState);
return; return;
} }
// 如果选择的歌曲在队列中,直接切歌
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); // 所选的歌在队列中
@ -52,6 +79,15 @@ export default function SongCardList({ listInfo, songList, className, collectTyp
return; return;
} }
// 修改 playList、playQueue
if (clickType === 'playList') {
handlePlayList(id);
} else if (clickType === 'playPush') {
handlePlayPush(id);
}
};
const handlePlayList = (id: string) => {
// 重新设置播放列表 // 重新设置播放列表
const res = setPlayListInfo(listInfo as IPlayListInfo); const res = setPlayListInfo(listInfo as IPlayListInfo);
if (!res) return; if (!res) return;
@ -59,6 +95,15 @@ export default function SongCardList({ listInfo, songList, className, collectTyp
setAudioId(id); setAudioId(id);
}; };
const handlePlayPush = (id: string) => {
const newAudio: SongInfo | null = songList.find((item) => item.id === id) ?? null;
if (!newAudio) return;
if (playListInfo.type !== 'fm') setPlayList([...playList, newAudio]);
setPlayQueue([...playQueue, newAudio]);
setAudioId(id);
};
return ( return (
<div className={`${className}`}> <div className={`${className}`}>
{songList.map((item: SongInfo) => ( {songList.map((item: SongInfo) => (
@ -67,7 +112,7 @@ export default function SongCardList({ listInfo, songList, className, collectTyp
{...item} {...item}
playState={playState} playState={playState}
showEq={item.id === audioId} showEq={item.id === audioId}
onPlay={(audioId: string) => handlePlayList(audioId)} onPlay={(audioId: string) => handlePlay(audioId)}
showCollect={collectType === 'always' || (collectType === 'playing' && item.id === audioId)} showCollect={collectType === 'always' || (collectType === 'playing' && item.id === audioId)}
/> />
))} ))}

@ -63,3 +63,4 @@ export { default as QRCode } from './common/QRCode';
export { default as QRCodeDialog } from './common/QRCodeDialog'; export { default as QRCodeDialog } from './common/QRCodeDialog';
export { default as InfiniteScroller } from './common/InfiniteScroller'; export { default as InfiniteScroller } from './common/InfiniteScroller';
export { default as Pagination } from './Pagination/Pagination'; export { default as Pagination } from './Pagination/Pagination';
export { default as Search } from './Search/Search';

@ -71,3 +71,38 @@ export const apiGetSongWithCollectVol = async ({
); );
return result; return result;
}; };
/**
* @description
*/
export const apiSearchJournal = async ({
keyword,
pageNum,
pageSize,
}: {
keyword: string;
pageNum: number;
pageSize: number;
}) => {
const result: FetchResponse<Paging<JournalInfo>> = await clientHttp.get(
`/queyueapi/music/search/fuzzy/journal?keyword=${keyword}&pageNum=${pageNum}&pageSize=${pageSize}`,
);
return result;
};
/**
* @description
*/
export const apiSearchSong = async ({
keyword,
pageNum,
pageSize,
}: {
keyword: string;
pageNum: number;
pageSize: number;
}) => {
const result: FetchResponse<Paging<SongInfo>> = await clientHttp.get(
`/queyueapi/music/search/fuzzy/song?keyword=${keyword}&pageNum=${pageNum}&pageSize=${pageSize}`,
);
return result;
};

Loading…
Cancel
Save