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 Link from 'next/link';
import { HighlightText } from '@/components';
export default function MiniJournalCard({
image,
journalNo,
title,
keyword,
onClick,
}: {
image: string;
journalNo: string;
title: string;
keyword: string;
onClick?: () => void;
}) {
return (
@ -23,8 +27,16 @@ export default function MiniJournalCard({
className="w-162px h-100px rounded-3px group-hover:transform-scale-105 transition-transform-300"
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>
<div className="w-full mt-1px text-[0,0,0,0.95] text-14px leading-20px text-overflow">{title}</div>
<HighlightText
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>
</Link>
);

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

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

@ -14,18 +14,18 @@ interface IProps {
}
export default function Search({ className }: IProps) {
const [value, setValue] = useState<string>('');
const [keyword, setKeyword] = 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());
const handleInput: (keyword: string) => void = (keyword) => {
setShowDropDown(!!keyword.trim());
if (value.trim()) {
setValue(value);
handleSearch(value);
if (keyword.trim()) {
setKeyword(keyword);
handleSearch(keyword);
}
};
@ -50,7 +50,7 @@ export default function Search({ className }: IProps) {
// input 聚焦时,展示下拉框
const handleInputFocus = () => {
if (value.trim()) setShowDropDown(true);
if (keyword.trim()) setShowDropDown(true);
};
const handleClickOutside = (e: Event) => {
@ -68,10 +68,15 @@ export default function Search({ className }: IProps) {
return (
<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 && (
<Dropdown journalData={journalData} songData={songData} onClose={() => setShowDropDown(false)} />
<Dropdown
keyword={keyword}
journalData={journalData}
songData={songData}
onClose={() => setShowDropDown(false)}
/>
)}
</div>
);

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

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

@ -1,6 +1,6 @@
import Image from 'next/image';
import { CollectButton, IconEqualizer } from '@/components';
import { CollectButton, IconEqualizer, HighlightText } from '@/components';
interface Props extends SongInfo {
/** 播放状态 */
@ -9,36 +9,53 @@ interface Props extends SongInfo {
showCollect: boolean;
/** 显示均衡器效果 */
showEq: boolean;
keyword: string;
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 (
<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)}
>
{/* left */}
<div className="flex flex-row items-center">
<div className="flex flex-row items-center w-full">
{/* 专辑封面 */}
<Image
width={48}
height={48}
src={pic}
alt={title}
className="w-[48px] h-[48px] rounded-[3px] overflow-hidden"
className="flex-shrink-0 w-48px h-48px rounded-3px overflow-hidden"
unoptimized
/>
{/* 歌曲名称/歌手 */}
<div className={`flex flex-col justify-between ml-[15px]`}>
<div className={`text-[15px] leading-[21px] text-base group-hover:text-brand ${showEq && 'text-brand'}`}>
{title}
</div>
<div
className={`text-[13px] leading-[18.2px] text-[rgba(0,0,0,0.7)] group-hover:text-brand ${showEq && 'text-brand'}`}
>
{artist}
</div>
<div className={`flex flex-col justify-between ml-15px`}>
<HighlightText
text={title}
keyword={keyword}
className={`w-390px text-15px leading-21px text-base group-hover:text-brand flex-grow-1 text-overflow ${showEq && 'text-brand'}`}
/>
<HighlightText
text={`${artist}/${album}`}
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>
@ -48,17 +65,17 @@ export default function SongCard({ playState, id, title, pic, artist, haveCollec
{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
id={id}
active={haveCollect}
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>
);

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

@ -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 Pagination } from './Pagination/Pagination';
export { default as Search } from './Search/Search';
export { default as HighlightText } from './common/HighlightText';

Loading…
Cancel
Save