update(Search):

1. 搜索高亮
2. 文本溢出省略
3. styles
feature/artists
mackt 7 months ago
parent a520e09419
commit b0c3834d31

@ -1,15 +1,19 @@
import Image from 'next/image'; import Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
import { HighlightText } from '@/components';
export default function MiniJournalCard({ export default function MiniJournalCard({
image, image,
journalNo, journalNo,
title, title,
keyword,
onClick, onClick,
}: { }: {
image: string; image: string;
journalNo: string; journalNo: string;
title: string; title: string;
keyword: string;
onClick?: () => void; onClick?: () => void;
}) { }) {
return ( return (
@ -23,8 +27,16 @@ export default function MiniJournalCard({
className="w-162px h-100px rounded-3px group-hover:transform-scale-105 transition-transform-300" className="w-162px h-100px rounded-3px group-hover:transform-scale-105 transition-transform-300"
unoptimized unoptimized
/> />
<div className="w-full mt-9px text-[rgba(0,0,0,0.4)] text-12px leading-17px">{`vol${journalNo.toString().padStart(4, '0')}`}</div> <HighlightText
<div className="w-full mt-1px text-[0,0,0,0.95] text-14px leading-20px text-overflow">{title}</div> text={`VOL·${journalNo.toString().padStart(4, '0')}`}
keyword={keyword}
className="w-full mt-9px text-[rgba(0,0,0,0.4)] text-12px leading-17px text-overflow"
/>
<HighlightText
text={title}
keyword={keyword}
className="w-full mt-1px text-[0,0,0,0.95] text-14px leading-20px text-overflow text-overflow"
/>
</div> </div>
</Link> </Link>
); );

@ -2,11 +2,12 @@ import MiniJournalCard from './MiniJournalCard';
interface IProps { interface IProps {
list: JournalInfo[]; list: JournalInfo[];
keyword: string;
className?: string; className?: string;
onClick: () => void; onClick: () => void;
} }
export default function MiniJournalCardList({ list, className, onClick }: IProps) { export default function MiniJournalCardList({ list, keyword, 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) => (
@ -15,6 +16,7 @@ export default function MiniJournalCardList({ list, className, onClick }: IProps
image={item.image} image={item.image}
journalNo={item.journalNo} journalNo={item.journalNo}
title={item.title} title={item.title}
keyword={keyword}
onClick={onClick} onClick={onClick}
/> />
))} ))}

@ -5,12 +5,13 @@ import Single from './widget/Single';
interface IProps { interface IProps {
journalData: JournalInfo[]; journalData: JournalInfo[];
keyword: string;
songData: SongInfo[]; songData: SongInfo[];
className?: string; className?: string;
onClose: () => void; onClose: () => void;
} }
export default function SearchDropdown({ journalData, songData, className, onClose }: IProps) { export default function SearchDropdown({ journalData, keyword, songData, className, onClose }: IProps) {
const haveData = journalData.length || songData.length; const haveData = journalData.length || songData.length;
return ( return (
@ -20,8 +21,8 @@ export default function SearchDropdown({ journalData, songData, className, onClo
> >
{haveData ? ( {haveData ? (
<div className="h-fit"> <div className="h-fit">
{!!songData.length && <Single data={songData} />} {!!songData.length && <Single keyword={keyword} data={songData} />}
{!!journalData.length && <Journal data={journalData} onClose={onClose} />} {!!journalData.length && <Journal keyword={keyword} data={journalData} onClose={onClose} />}
</div> </div>
) : ( ) : (
<NoData className="w-full h-160px" /> <NoData className="w-full h-160px" />

@ -14,18 +14,18 @@ interface IProps {
} }
export default function Search({ className }: IProps) { export default function Search({ className }: IProps) {
const [value, setValue] = useState<string>(''); const [keyword, setKeyword] = useState<string>('');
const [journalData, setJournalData] = useState<JournalInfo[]>([]); const [journalData, setJournalData] = useState<JournalInfo[]>([]);
const [songData, setSongData] = useState<SongInfo[]>([]); const [songData, setSongData] = useState<SongInfo[]>([]);
const [showDropDown, setShowDropDown] = useState<boolean>(false); const [showDropDown, setShowDropDown] = useState<boolean>(false);
const searchRef = useRef<HTMLDivElement>(null); const searchRef = useRef<HTMLDivElement>(null);
const handleInput: (value: string) => void = (value) => { const handleInput: (keyword: string) => void = (keyword) => {
setShowDropDown(!!value.trim()); setShowDropDown(!!keyword.trim());
if (value.trim()) { if (keyword.trim()) {
setValue(value); setKeyword(keyword);
handleSearch(value); handleSearch(keyword);
} }
}; };
@ -50,7 +50,7 @@ export default function Search({ className }: IProps) {
// input 聚焦时,展示下拉框 // input 聚焦时,展示下拉框
const handleInputFocus = () => { const handleInputFocus = () => {
if (value.trim()) setShowDropDown(true); if (keyword.trim()) setShowDropDown(true);
}; };
const handleClickOutside = (e: Event) => { const handleClickOutside = (e: Event) => {
@ -68,10 +68,15 @@ export default function Search({ className }: IProps) {
return ( return (
<div ref={searchRef} className={`w-208px h-38px ${className}`}> <div ref={searchRef} className={`w-208px h-38px ${className}`}>
<Input onInput={debounce((value) => handleInput(value), 200)} onFocus={handleInputFocus} /> <Input onInput={debounce((keyword) => handleInput(keyword), 200)} onFocus={handleInputFocus} />
{showDropDown && ( {showDropDown && (
<Dropdown journalData={journalData} songData={songData} onClose={() => setShowDropDown(false)} /> <Dropdown
keyword={keyword}
journalData={journalData}
songData={songData}
onClose={() => setShowDropDown(false)}
/>
)} )}
</div> </div>
); );

@ -2,15 +2,16 @@ import { MiniJournalCardList } from '@/components';
interface IProps { interface IProps {
data: JournalInfo[]; data: JournalInfo[];
keyword: string;
className?: string; className?: string;
onClose: () => void; onClose: () => void;
} }
export default function Journal({ data, className, onClose }: IProps) { export default function Journal({ data, keyword, className, onClose }: IProps) {
return ( return (
<div className={className}> <div className={className}>
<p className="mt-30px"></p> <p className="mt-30px text-#000 text-14px leading-20px font-500"></p>
<MiniJournalCardList list={data} className="w-full mt-18px" onClick={onClose} /> <MiniJournalCardList list={data} keyword={keyword} className="w-full mt-18px" onClick={onClose} />
</div> </div>
); );
} }

@ -2,19 +2,21 @@ import { SongCardList } from '@/components';
interface IProps { interface IProps {
data: SongInfo[]; data: SongInfo[];
keyword: string;
className?: string; className?: string;
} }
export default function Single({ data, className }: IProps) { export default function Single({ data, keyword, className }: IProps) {
return ( return (
<div className={className}> <div className={className}>
<p></p> <p className="text-#000 text-14px leading-20px font-500"></p>
<SongCardList <SongCardList
className="w-full mt-6px ml-[-16px]" className="w-full mt-6px ml-[-16px]"
clickType="playPush" clickType="playPush"
songList={data} songList={data}
keyword={keyword}
listInfo={{ type: 'collectSingle', id: null }} listInfo={{ type: 'collectSingle', id: null }}
collectType="playing" collectType="none"
/> />
</div> </div>
); );

@ -1,6 +1,6 @@
import Image from 'next/image'; import Image from 'next/image';
import { CollectButton, IconEqualizer } from '@/components'; import { CollectButton, IconEqualizer, HighlightText } from '@/components';
interface Props extends SongInfo { interface Props extends SongInfo {
/** 播放状态 */ /** 播放状态 */
@ -9,36 +9,53 @@ interface Props extends SongInfo {
showCollect: boolean; showCollect: boolean;
/** 显示均衡器效果 */ /** 显示均衡器效果 */
showEq: boolean; showEq: boolean;
keyword: string;
onPlay: (id: string) => void; onPlay: (id: string) => void;
} }
export default function SongCard({ playState, id, title, pic, artist, haveCollect, duration, showEq, onPlay }: Props) { export default function SongCard({
playState,
id,
title,
pic,
artist,
album,
showCollect,
haveCollect,
duration,
showEq,
keyword,
onPlay,
}: Props) {
return ( return (
<div <div
className="flex flex-row items-center justify-between w-full h-[72px] my-[3px] py-[12px] px-[18px] rounded-[3px] hover:bg-[#f2f3f7] group cursor-pointer" className="flex flex-row items-center justify-between w-full h-72px my-3px py-12px px-18px rounded-3px hover:bg-#f2f3f7 group cursor-pointer"
onClick={() => onPlay(id)} onClick={() => onPlay(id)}
> >
{/* left */} {/* left */}
<div className="flex flex-row items-center"> <div className="flex flex-row items-center w-full">
{/* 专辑封面 */} {/* 专辑封面 */}
<Image <Image
width={48} width={48}
height={48} height={48}
src={pic} src={pic}
alt={title} alt={title}
className="w-[48px] h-[48px] rounded-[3px] overflow-hidden" className="flex-shrink-0 w-48px h-48px rounded-3px overflow-hidden"
unoptimized unoptimized
/> />
{/* 歌曲名称/歌手 */} {/* 歌曲名称/歌手 */}
<div className={`flex flex-col justify-between ml-[15px]`}> <div className={`flex flex-col justify-between ml-15px`}>
<div className={`text-[15px] leading-[21px] text-base group-hover:text-brand ${showEq && 'text-brand'}`}> <HighlightText
{title} text={title}
</div> keyword={keyword}
<div className={`w-390px text-15px leading-21px text-base group-hover:text-brand flex-grow-1 text-overflow ${showEq && 'text-brand'}`}
className={`text-[13px] leading-[18.2px] text-[rgba(0,0,0,0.7)] group-hover:text-brand ${showEq && 'text-brand'}`} />
> <HighlightText
{artist} text={`${artist}/${album}`}
</div> keyword={keyword}
className={`w-390px text-13px leading-18.2px text-base group-hover:text-brand flex-grow-1 text-overflow ${showEq && 'text-brand'}`}
/>
</div> </div>
</div> </div>
@ -48,17 +65,17 @@ export default function SongCard({ playState, id, title, pic, artist, haveCollec
{showEq && <IconEqualizer active={playState} />} {showEq && <IconEqualizer active={playState} />}
{/* 音频时长 */} {/* 音频时长 */}
<p className="ml-[30px] mr-[13px] text-[12px] leading-[16.8px] text-[rgba(0,0,0,0.4)]">{duration || '00:00'}</p> <p className="ml-30px text-12px leading-17px text-#000/40">{duration || '00:00'}</p>
{/* 收藏按钮单曲 */} {/* 收藏按钮单曲 */}
{ {showCollect && (
<CollectButton <CollectButton
id={id} id={id}
active={haveCollect} active={haveCollect}
collectType="0" collectType="0"
textClassName="w-[42px] ml-[6px] mr-[24px] text-[14px] leading-[16px]" textClassName="w-42px ml-6px ml-12px mr-24px text-14px leading-16px text-overflow"
/> />
} )}
</div> </div>
</div> </div>
); );

@ -16,6 +16,8 @@ interface Props {
* playerCard: * playerCard:
*/ */
listInfo: { type: 'vol' | 'collectSingle' | 'playerCard'; id: string | null }; listInfo: { type: 'vol' | 'collectSingle' | 'playerCard'; id: string | null };
// 高亮关键词
keyword: string;
/** /**
* @description * @description
* playList: * playList:
@ -23,8 +25,8 @@ interface Props {
*/ */
clickType?: 'playList' | 'playPush'; clickType?: 'playList' | 'playPush';
songList: SongInfo[]; songList: SongInfo[];
/** 收藏按钮的显示逻辑 always: 总是显示 playing: 播放时显示 */ /** 收藏按钮的展示逻辑 always: 总是展示 playing: 播放时展示; none: 不展示*/
collectType?: 'always' | 'playing'; collectType?: 'always' | 'playing' | 'none';
className?: string; className?: string;
} }
@ -32,6 +34,7 @@ export default function SongCardList({
listInfo, listInfo,
songList, songList,
className, className,
keyword,
collectType = 'always', collectType = 'always',
clickType = 'playList', clickType = 'playList',
}: Props) { }: Props) {
@ -113,7 +116,9 @@ export default function SongCardList({
playState={playState} playState={playState}
showEq={item.id === audioId} showEq={item.id === audioId}
onPlay={(audioId: string) => handlePlay(audioId)} onPlay={(audioId: string) => handlePlay(audioId)}
showCollect={collectType === 'always' || (collectType === 'playing' && item.id === audioId)} keyword={keyword}
// showCollect={collectType === 'always' || (collectType === 'playing' && item.id === audioId)}
showCollect={collectType === 'always' || collectType === 'playing'}
/> />
))} ))}
</div> </div>

@ -0,0 +1,29 @@
'use client';
import { useEffect, useState } from 'react';
interface Iprops {
text: string;
keyword: string;
className?: string;
}
const HighlightText = ({ text, keyword, className }: Iprops) => {
// 使用useState钩子来存储处理后的文本
const [highlightedText, setHighlightedText] = useState<string>(text);
useEffect(() => {
if (!keyword) return;
// 创建一个正则表达式,全局匹配并忽略大小写
const regex = new RegExp(keyword, 'gi');
// 使用replace方法替换所有匹配的关键字并用span标签包裹起来
const newText = text.replace(regex, `<span class="text-brand">$&</span>`);
// 更新状态以触发组件重新渲染
setHighlightedText(newText);
}, []); // 仅在text或keyword变化时重新执行effect
// 渲染带有高亮的文本
return <p className={className} dangerouslySetInnerHTML={{ __html: highlightedText }} />;
};
export default HighlightText;

@ -64,3 +64,4 @@ 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'; export { default as Search } from './Search/Search';
export { default as HighlightText } from './common/HighlightText';

Loading…
Cancel
Save