feat: 搜索页面和一堆更新

This commit is contained in:
qier222 2022-03-29 00:11:05 +08:00
parent 4d7bc14827
commit b4590c3c34
No known key found for this signature in database
GPG Key ID: 9C85007ED905F14D
17 changed files with 279 additions and 87 deletions

View File

@ -7,22 +7,22 @@ export enum SearchApiNames {
// 搜索 // 搜索
export enum SearchTypes { export enum SearchTypes {
SINGLE = 1, SINGLE = '1',
ALBUM = 10, ALBUM = '10',
ARTIST = 100, ARTIST = '100',
PLAYLIST = 1000, PLAYLIST = '1000',
USER = 1002, USER = '1002',
MV = 1004, MV = '1004',
LYRICS = 1006, LYRICS = '1006',
RADIO = 1009, RADIO = '1009',
VIDEO = 1014, VIDEO = '1014',
ALL = 1018, ALL = '1018',
} }
export interface SearchParams { export interface SearchParams {
keywords: string keywords: string
limit?: number // 返回数量 , 默认为 30 limit?: number // 返回数量 , 默认为 30
offset?: number // 偏移数量,用于分页 , 如 : 如 :( 页数 -1)*30, 其中 30 为 limit 的值 , 默认为 0 offset?: number // 偏移数量,用于分页 , 如 : 如 :( 页数 -1)*30, 其中 30 为 limit 的值 , 默认为 0
type?: SearchTypes // type: 搜索类型 type: keyof typeof SearchTypes // type: 搜索类型
} }
interface SearchResponse { interface SearchResponse {
code: number code: number
@ -71,7 +71,10 @@ export function search(params: SearchParams): Promise<SearchResponse> {
return request({ return request({
url: '/search', url: '/search',
method: 'get', method: 'get',
params: params, params: {
...params,
type: SearchTypes[params.type ?? SearchTypes.ALL],
},
}) })
} }

View File

@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="8" r="3" stroke="currentColor" stroke-width="2"/>
<path d="M6 19V19C6 16.7909 7.79086 15 10 15H14C16.2091 15 18 16.7909 18 19V19" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 341 B

View File

@ -1,29 +1,34 @@
import SvgIcon from '@/components/SvgIcon' import SvgIcon from '@/components/SvgIcon'
const Cover = ({ const Cover = ({
isRounded,
imageUrl, imageUrl,
onClick, onClick,
roundedClass = 'rounded-xl',
showPlayButton = false,
showHover = true,
}: { }: {
imageUrl: string imageUrl: string
isRounded?: boolean
onClick?: () => void onClick?: () => void
roundedClass?: string
showPlayButton?: boolean
showHover?: boolean
}) => { }) => {
const [isError, setIsError] = useState(false) const [isError, setIsError] = useState(false)
return ( return (
<div onClick={onClick} className='group relative z-0'> <div onClick={onClick} className='group relative z-0'>
{/* Neon shadow */} {/* Neon shadow */}
<div {showHover && (
className={classNames( <div
'absolute top-2 z-[-1] h-full w-full scale-x-[.92] scale-y-[.96] rounded-xl bg-cover opacity-0 blur-lg filter transition duration-300 group-hover:opacity-60', className={classNames(
isRounded && 'rounded-full', 'absolute top-2 z-[-1] h-full w-full scale-x-[.92] scale-y-[.96] rounded-xl bg-cover opacity-0 blur-lg filter transition duration-300 group-hover:opacity-60',
!isRounded && 'rounded-xl' roundedClass
)} )}
style={{ style={{
backgroundImage: `url("${imageUrl}")`, backgroundImage: `url("${imageUrl}")`,
}} }}
></div> ></div>
)}
{/* Cover */} {/* Cover */}
{isError ? ( {isError ? (
@ -34,20 +39,21 @@ const Cover = ({
<img <img
className={classNames( className={classNames(
'box-content aspect-square h-full w-full border border-black border-opacity-5 dark:border-white dark:border-opacity-[.03]', 'box-content aspect-square h-full w-full border border-black border-opacity-5 dark:border-white dark:border-opacity-[.03]',
isRounded && 'rounded-full', roundedClass
!isRounded && 'rounded-xl'
)} )}
src={imageUrl} src={imageUrl}
onError={() => setIsError(true)} onError={() => imageUrl && setIsError(true)}
/> />
)} )}
{/* Play button */} {/* Play button */}
<div className='absolute top-0 hidden h-full w-full place-content-center group-hover:grid'> {showPlayButton && (
<button className='btn-pressed-animation grid h-11 w-11 cursor-default place-content-center rounded-full border border-white border-opacity-[.08] bg-white bg-opacity-[.14] text-white backdrop-blur backdrop-filter transition-all hover:bg-opacity-[.44]'> <div className='absolute top-0 hidden h-full w-full place-content-center group-hover:grid'>
<SvgIcon className='ml-0.5 h-6 w-6' name='play-fill' /> <button className='btn-pressed-animation grid h-11 w-11 cursor-default place-content-center rounded-full border border-white border-opacity-[.08] bg-white bg-opacity-[.14] text-white backdrop-blur backdrop-filter transition-all hover:bg-opacity-[.44]'>
</button> <SvgIcon className='ml-0.5 h-6 w-6' name='play-fill' />
</div> </button>
</div>
)}
</div> </div>
) )
} }

View File

@ -145,6 +145,7 @@ const CoverRow = ({
<Cover <Cover
onClick={() => goTo(item.id)} onClick={() => goTo(item.id)}
imageUrl={getImageUrl(item)} imageUrl={getImageUrl(item)}
showPlayButton={true}
/> />
)} )}

View File

@ -1,4 +1,3 @@
import { Fragment } from 'react'
import ArtistInline from '@/components/ArtistsInline' import ArtistInline from '@/components/ArtistsInline'
import IconButton from '@/components/IconButton' import IconButton from '@/components/IconButton'
import Slider from '@/components/Slider' import Slider from '@/components/Slider'
@ -27,7 +26,7 @@ const PlayingTrack = () => {
} }
return ( return (
<Fragment> <>
{track && ( {track && (
<div className='flex items-center gap-3'> <div className='flex items-center gap-3'>
{track?.al?.picUrl && ( {track?.al?.picUrl && (
@ -67,7 +66,7 @@ const PlayingTrack = () => {
</div> </div>
)} )}
{!track && <div></div>} {!track && <div></div>}
</Fragment> </>
) )
} }

View File

@ -1,4 +1,3 @@
import { Fragment } from 'react'
import type { RouteObject } from 'react-router-dom' import type { RouteObject } from 'react-router-dom'
import { useRoutes } from 'react-router-dom' import { useRoutes } from 'react-router-dom'
import Album from '@/pages/Album' import Album from '@/pages/Album'
@ -6,6 +5,7 @@ import Home from '@/pages/Home'
import Login from '@/pages/Login' import Login from '@/pages/Login'
import Playlist from '@/pages/Playlist' import Playlist from '@/pages/Playlist'
import Artist from '@/pages/Artist' import Artist from '@/pages/Artist'
import Search from '@/pages/Search'
const routes: RouteObject[] = [ const routes: RouteObject[] = [
{ {
@ -16,6 +16,16 @@ const routes: RouteObject[] = [
path: '/login', path: '/login',
element: <Login />, element: <Login />,
}, },
{
path: '/search/:keywords',
element: <Search />,
children: [
{
path: ':type',
element: <Search />,
},
],
},
{ {
path: '/playlist/:id', path: '/playlist/:id',
element: <Playlist />, element: <Playlist />,
@ -32,7 +42,7 @@ const routes: RouteObject[] = [
const Router = () => { const Router = () => {
const element = useRoutes(routes) const element = useRoutes(routes)
return <Fragment>{element}</Fragment> return <>{element}</>
} }
export default Router export default Router

View File

@ -29,12 +29,13 @@ const NavigationButtons = () => {
} }
const SearchBox = () => { const SearchBox = () => {
const [keyword, setKeyword] = useState('') const { type } = useParams()
const [keywords, setKeywords] = useState('')
const navigate = useNavigate() const navigate = useNavigate()
const toSearch = (e: React.KeyboardEvent) => { const toSearch = (e: React.KeyboardEvent) => {
if (!keyword) return if (!keywords) return
if (e.key === 'Enter') { if (e.key === 'Enter') {
navigate(`/search/${keyword}`) navigate(`/search/${keywords}${type ? `/${type}` : ''}`)
} }
} }
@ -45,18 +46,18 @@ const SearchBox = () => {
name='search' name='search'
/> />
<input <input
value={keyword} value={keywords}
onChange={e => setKeyword(e.target.value)} onChange={e => setKeywords(e.target.value)}
onKeyDown={toSearch} onKeyDown={toSearch}
type='text' type='text'
className='flex-grow bg-transparent placeholder:text-gray-500 dark:text-white dark:placeholder:text-gray-400' className='flex-grow bg-transparent placeholder:text-gray-500 dark:text-white dark:placeholder:text-gray-400'
placeholder='搜索' placeholder='搜索'
/> />
<div <div
onClick={() => setKeyword('')} onClick={() => setKeywords('')}
className={classNames( className={classNames(
'cursor-default rounded-full p-1 transition after:bg-gray-300 hover:bg-white/20 dark:text-white/50', 'cursor-default rounded-full p-1 transition after:bg-gray-300 hover:bg-white/20 dark:text-white/50',
!keyword && 'hidden' !keywords && 'hidden'
)} )}
> >
<SvgIcon className='h-4 w-4' name='x' /> <SvgIcon className='h-4 w-4' name='x' />
@ -85,6 +86,12 @@ const Avatar = () => {
onClick={() => navigate('/login')} onClick={() => navigate('/login')}
className='app-region-no-drag h-9 w-9 rounded-full bg-gray-100 dark:bg-gray-700' className='app-region-no-drag h-9 w-9 rounded-full bg-gray-100 dark:bg-gray-700'
/> />
// <div onClick={() => navigate('/login')}>
// <SvgIcon
// name='user'
// className='h-9 w-9 rounded-full bg-gray-400/10 p-1 text-gray-400'
// />
// </div>
) )
} }

View File

@ -1,7 +1,6 @@
import ArtistInline from '@/components/ArtistsInline' import ArtistInline from '@/components/ArtistsInline'
import Skeleton from '@/components/Skeleton' import Skeleton from '@/components/Skeleton'
import { resizeImage } from '@/utils/common' import { resizeImage } from '@/utils/common'
import { Fragment } from 'react'
import SvgIcon from './SvgIcon' import SvgIcon from './SvgIcon'
const Track = ({ const Track = ({
@ -78,15 +77,27 @@ const TrackGrid = ({
tracks, tracks,
isSkeleton = false, isSkeleton = false,
onTrackDoubleClick, onTrackDoubleClick,
cols = 2,
}: { }: {
tracks: Track[] tracks: Track[]
isSkeleton?: boolean isSkeleton?: boolean
onTrackDoubleClick?: (trackID: number) => void onTrackDoubleClick?: (trackID: number) => void
cols?: number
}) => { }) => {
return ( return (
<div className='grid grid-cols-2 gap-x-2'> <div
className='grid gap-x-2'
style={{
gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))`,
}}
>
{tracks.map((track, index) => ( {tracks.map((track, index) => (
<Track key={track.id} track={track} isSkeleton={isSkeleton} /> <Track
key={track.id}
track={track}
isSkeleton={isSkeleton}
isHighlight={false}
/>
))} ))}
</div> </div>
) )

View File

@ -1,4 +1,4 @@
import { Fragment, memo } from 'react' import { memo } from 'react'
import { NavLink } from 'react-router-dom' import { NavLink } from 'react-router-dom'
import ArtistInline from '@/components/ArtistsInline' import ArtistInline from '@/components/ArtistsInline'
import Skeleton from '@/components/Skeleton' import Skeleton from '@/components/Skeleton'
@ -106,7 +106,7 @@ const Track = memo(
{isSkeleton ? ( {isSkeleton ? (
<Skeleton>PLACEHOLDER1234567890</Skeleton> <Skeleton>PLACEHOLDER1234567890</Skeleton>
) : ( ) : (
<Fragment> <>
<NavLink <NavLink
to={`/album/${track.al.id}`} to={`/album/${track.al.id}`}
className={classNames( className={classNames(
@ -117,7 +117,7 @@ const Track = memo(
{track.al.name} {track.al.name}
</NavLink> </NavLink>
<span className='flex-grow'></span> <span className='flex-grow'></span>
</Fragment> </>
)} )}
</div> </div>
@ -194,7 +194,7 @@ const TracksList = memo(
) )
return ( return (
<Fragment> <>
{/* Tracks table header */} {/* Tracks table header */}
<div className='ml-2 mr-4 mt-10 mb-2 grid grid-cols-12 border-b border-gray-100 py-2.5 text-sm text-gray-400 dark:border-gray-800 dark:text-gray-500'> <div className='ml-2 mr-4 mt-10 mb-2 grid grid-cols-12 border-b border-gray-100 py-2.5 text-sm text-gray-400 dark:border-gray-800 dark:text-gray-500'>
<div className='col-span-6 grid grid-cols-[4.2rem_auto]'> <div className='col-span-6 grid grid-cols-[4.2rem_auto]'>
@ -228,7 +228,7 @@ const TracksList = memo(
/> />
))} ))}
</div> </div>
</Fragment> </>
) )
} }
) )

View File

@ -137,6 +137,7 @@ declare interface Artist {
publishTime?: number publishTime?: number
picId_str?: string picId_str?: string
img1v1Id_str?: string img1v1Id_str?: string
occupation?: string
} }
declare interface Album { declare interface Album {
alias: unknown[] alias: unknown[]

View File

@ -1,5 +1,4 @@
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { Fragment } from 'react'
import { NavLink } from 'react-router-dom' import { NavLink } from 'react-router-dom'
import Button, { Color as ButtonColor } from '@/components/Button' import Button, { Color as ButtonColor } from '@/components/Button'
import CoverRow, { Subtitle } from '@/components/CoverRow' import CoverRow, { Subtitle } from '@/components/CoverRow'
@ -81,11 +80,11 @@ const Header = ({
const [isCoverError, setCoverError] = useState(false) const [isCoverError, setCoverError] = useState(false)
return ( return (
<Fragment> <>
{/* Header background */} {/* Header background */}
<div className='absolute top-0 left-0 z-0 h-[24rem] w-full overflow-hidden'> <div className='absolute top-0 left-0 z-0 h-[24rem] w-full overflow-hidden'>
{coverUrl && !isCoverError && ( {coverUrl && !isCoverError && (
<Fragment> <>
<img <img
src={coverUrl} src={coverUrl}
className='absolute -top-full w-full blur-[100px]' className='absolute -top-full w-full blur-[100px]'
@ -94,7 +93,7 @@ const Header = ({
src={coverUrl} src={coverUrl}
className='absolute -top-full w-full blur-[100px]' className='absolute -top-full w-full blur-[100px]'
/> />
</Fragment> </>
)} )}
<div className='absolute top-0 h-full w-full bg-gradient-to-b from-white/80 to-white dark:from-black/50 dark:to-[#1d1d1d]'></div> <div className='absolute top-0 h-full w-full bg-gradient-to-b from-white/80 to-white dark:from-black/50 dark:to-[#1d1d1d]'></div>
</div> </div>
@ -208,7 +207,7 @@ const Header = ({
</div> </div>
</div> </div>
</div> </div>
</Fragment> </>
) )
} }

View File

@ -8,26 +8,19 @@ import dayjs from 'dayjs'
import TracksGrid from '@/components/TracksGrid' import TracksGrid from '@/components/TracksGrid'
import CoverRow, { Subtitle } from '@/components/CoverRow' import CoverRow, { Subtitle } from '@/components/CoverRow'
import Skeleton from '@/components/Skeleton' import Skeleton from '@/components/Skeleton'
import { Fragment } from 'react'
import useTracks from '@/hooks/useTracks' import useTracks from '@/hooks/useTracks'
const Header = ({ artist }: { artist: Artist | undefined }) => { const Header = ({ artist }: { artist: Artist | undefined }) => {
const coverImage = resizeImage(artist?.img1v1Url || '', 'md') const coverImage = resizeImage(artist?.img1v1Url || '', 'md')
return ( return (
<Fragment> <>
<div className='absolute top-0 left-0 z-0 h-[24rem] w-full overflow-hidden'> <div className='absolute top-0 left-0 z-0 h-[24rem] w-full overflow-hidden'>
{coverImage && ( {coverImage && (
<Fragment> <>
<img <img src={coverImage} className='absolute w-full blur-[100px]' />
src={coverImage} <img src={coverImage} className='absolute w-full blur-[100px]' />
className='absolute -top-full w-full blur-[100px]' </>
/>
<img
src={coverImage}
className='absolute -top-full w-full blur-[100px]'
/>
</Fragment>
)} )}
<div className='absolute top-0 h-full w-full bg-gradient-to-b from-white/80 to-white dark:from-black/50 dark:to-[#1d1d1d]'></div> <div className='absolute top-0 h-full w-full bg-gradient-to-b from-white/80 to-white dark:from-black/50 dark:to-[#1d1d1d]'></div>
</div> </div>
@ -47,7 +40,7 @@ const Header = ({ artist }: { artist: Artist | undefined }) => {
<div className='text-7xl font-bold text-white'>{artist?.name}</div> <div className='text-7xl font-bold text-white'>{artist?.name}</div>
</div> </div>
</div> </div>
</Fragment> </>
) )
} }
@ -67,7 +60,7 @@ const LatestRelease = ({
{isLoading ? ( {isLoading ? (
<Skeleton className='aspect-square w-full rounded-xl'></Skeleton> <Skeleton className='aspect-square w-full rounded-xl'></Skeleton>
) : ( ) : (
<Cover imageUrl={album?.picUrl ?? ''} /> <Cover imageUrl={album?.picUrl ?? ''} showPlayButton={true} />
)} )}
<div className='line-clamp-2 line-clamp-1 mt-2 font-semibold leading-tight decoration-gray-600 decoration-2 hover:underline dark:text-white dark:decoration-gray-200'> <div className='line-clamp-2 line-clamp-1 mt-2 font-semibold leading-tight decoration-gray-600 decoration-2 hover:underline dark:text-white dark:decoration-gray-200'>
{album?.name} {album?.name}

View File

@ -1,6 +1,5 @@
import md5 from 'md5' import md5 from 'md5'
import QRCode from 'qrcode' import QRCode from 'qrcode'
import { Fragment } from 'react'
import { import {
checkLoginQrCodeStatus, checkLoginQrCodeStatus,
fetchLoginQrCodeKey, fetchLoginQrCodeKey,
@ -166,7 +165,7 @@ const OtherLoginMethods = ({
}, },
] ]
return ( return (
<Fragment> <>
<div className='mt-8 mb-4 flex w-full items-center'> <div className='mt-8 mb-4 flex w-full items-center'>
<span className='h-px flex-grow bg-gray-300 dark:bg-gray-700'></span> <span className='h-px flex-grow bg-gray-300 dark:bg-gray-700'></span>
<span className='mx-2 text-sm text-gray-400 '>or</span> <span className='mx-2 text-sm text-gray-400 '>or</span>
@ -187,7 +186,7 @@ const OtherLoginMethods = ({
) )
)} )}
</div> </div>
</Fragment> </>
) )
} }
@ -244,11 +243,11 @@ const LoginWithEmail = () => {
} }
return ( return (
<Fragment> <>
<EmailInput {...{ email, setEmail }} /> <EmailInput {...{ email, setEmail }} />
<PasswordInput {...{ password, setPassword }} /> <PasswordInput {...{ password, setPassword }} />
<LoginButton onClick={handleLogin} disabled={doLogin.isLoading} /> <LoginButton onClick={handleLogin} disabled={doLogin.isLoading} />
</Fragment> </>
) )
} }
@ -303,11 +302,11 @@ const LoginWithPhone = () => {
} }
return ( return (
<Fragment> <>
<PhoneInput {...{ countryCode, setCountryCode, phone, setPhone }} /> <PhoneInput {...{ countryCode, setCountryCode, phone, setPhone }} />
<PasswordInput {...{ password, setPassword }} /> <PasswordInput {...{ password, setPassword }} />
<LoginButton onClick={handleLogin} disabled={doLogin.isLoading} /> <LoginButton onClick={handleLogin} disabled={doLogin.isLoading} />
</Fragment> </>
) )
} }

View File

@ -1,4 +1,4 @@
import React, { Fragment, memo } from 'react' import { memo } from 'react'
import Button, { Color as ButtonColor } from '@/components/Button' import Button, { Color as ButtonColor } from '@/components/Button'
import Skeleton from '@/components/Skeleton' import Skeleton from '@/components/Skeleton'
import SvgIcon from '@/components/SvgIcon' import SvgIcon from '@/components/SvgIcon'
@ -25,7 +25,7 @@ const Header = memo(
const coverUrl = resizeImage(playlist?.coverImgUrl || '', 'lg') const coverUrl = resizeImage(playlist?.coverImgUrl || '', 'lg')
return ( return (
<Fragment> <>
{/* Header background */} {/* Header background */}
<div className='absolute top-0 left-0 z-0 h-[24rem] w-full overflow-hidden'> <div className='absolute top-0 left-0 z-0 h-[24rem] w-full overflow-hidden'>
<img src={coverUrl} className='absolute top-0 w-full blur-[100px]' /> <img src={coverUrl} className='absolute top-0 w-full blur-[100px]' />
@ -134,7 +134,7 @@ const Header = memo(
</div> </div>
</div> </div>
</div> </div>
</Fragment> </>
) )
} }
) )
@ -188,7 +188,7 @@ const Tracks = memo(
}, [tracksPages]) }, [tracksPages])
return ( return (
<Fragment> <>
{isLoadingPlaylist ? ( {isLoadingPlaylist ? (
<TracksList tracks={[]} isSkeleton={true} /> <TracksList tracks={[]} isSkeleton={true} />
) : isLoadingTracks ? ( ) : isLoadingTracks ? (
@ -199,7 +199,7 @@ const Tracks = memo(
) : ( ) : (
<TracksList tracks={tracks} onTrackDoubleClick={handlePlay} /> <TracksList tracks={tracks} onTrackDoubleClick={handlePlay} />
)} )}
</Fragment> </>
) )
} }
) )

View File

@ -1,5 +1,165 @@
import {
multiMatchSearch,
search,
SearchApiNames,
SearchTypes,
} from '@/api/search'
import Cover from '@/components/Cover'
import TrackGrid from '@/components/TracksGrid'
import { resizeImage } from '@/utils/common'
const Artists = ({ artists }: { artists: Artist[] }) => {
const navigate = useNavigate()
return (
<>
{artists.map(artist => (
<div
onClick={() => navigate(`/artist/${artist.id}`)}
key={artist.id}
className='btn-hover-animation flex items-center p-2.5 after:rounded-xl after:bg-gray-100 dark:after:bg-white/10'
>
<div className='mr-4 h-14 w-14'>
<Cover
imageUrl={resizeImage(artist.img1v1Url, 'xs')}
roundedClass='rounded-full'
showHover={false}
/>
</div>
<div>
<div className='text-lg font-semibold dark:text-white'>
{artist.name}
</div>
<div className='mt-0.5 text-sm font-medium text-gray-500 dark:text-gray-400'>
</div>
</div>
</div>
))}
</>
)
}
const Albums = ({ albums }: { albums: Album[] }) => {
const navigate = useNavigate()
return (
<>
{albums.map(album => (
<div
onClick={() => navigate(`/album/${album.id}`)}
key={album.id}
className='btn-hover-animation flex items-center p-2.5 after:rounded-xl after:bg-gray-100 dark:after:bg-white/10'
>
<div className='mr-4 h-14 w-14'>
<Cover
imageUrl={resizeImage(album.picUrl, 'xs')}
roundedClass='rounded-lg'
showHover={false}
/>
</div>
<div>
<div className='text-lg font-semibold dark:text-white'>
{album.name}
</div>
<div className='mt-0.5 text-sm font-medium text-gray-500 dark:text-gray-400'>
· {album?.artist.name} · 2020
</div>
</div>
</div>
))}
</>
)
}
const Search = () => { const Search = () => {
return <div></div> const { keywords = '', type = 'ALL' } = useParams()
const searchType: keyof typeof SearchTypes =
type.toUpperCase() in SearchTypes
? (type.toUpperCase() as keyof typeof SearchTypes)
: 'ALL'
const { data: bestMatchRaw, isLoading: isLoadingBestMatch } = useQuery(
[SearchApiNames.MULTI_MATCH_SEARCH, keywords],
() => multiMatchSearch({ keywords })
)
const bestMatch = useMemo(() => {
if (!bestMatchRaw?.result) return []
return bestMatchRaw.result.orders
.filter(order => ['album', 'artist'].includes(order)) // 暂时只支持专辑和艺人
.map(order => {
return bestMatchRaw.result[order][0]
})
.slice(0, 2)
}, [bestMatchRaw?.result])
const { data: searchResult, isLoading: isLoadingSearchResult } = useQuery(
[SearchApiNames.SEARCH, keywords, searchType],
() => search({ keywords, type: searchType })
)
return (
<div>
<div className='mt-6 mb-8 text-4xl font-semibold dark:text-white'>
<span className='text-gray-500'></span> &quot;{keywords}&quot;
</div>
{/* Best match */}
{bestMatch.length !== 0 && (
<div className='mb-6'>
<div className='mb-2 text-sm font-medium text-gray-400'></div>
<div className='grid grid-cols-2'>
{bestMatch.map(match => (
<div
key={`${match.id}${match.picUrl}`}
className='btn-hover-animation flex items-center p-3 after:rounded-xl after:bg-gray-100 dark:after:bg-white/10'
>
<div className='mr-6 h-24 w-24'>
<Cover
imageUrl={resizeImage(match.picUrl, 'xs')}
showHover={false}
roundedClass='rounded-full'
/>
</div>
<div>
<div className='text-xl font-semibold dark:text-white'>
{match.name}
</div>
<div className='mt-0.5 font-medium text-gray-500 dark:text-gray-400'>
{(match as Artist).occupation === '歌手' ? '艺人' : '专辑'}
</div>
</div>
</div>
))}
</div>
</div>
)}
{/* Search result */}
<div className='grid grid-cols-2 gap-6'>
{searchResult?.result.artist.artists && (
<div>
<div className='mb-2 text-sm font-medium text-gray-400'></div>
<Artists artists={searchResult.result.artist.artists} />
</div>
)}
{searchResult?.result.album.albums && (
<div>
<div className='mb-2 text-sm font-medium text-gray-400'></div>
<Albums albums={searchResult.result.album.albums} />
</div>
)}
{searchResult?.result.song.songs && (
<div className='col-span-2'>
<div className='mb-2 text-sm font-medium text-gray-400'></div>
<TrackGrid tracks={searchResult.result.song.songs} cols={3} />
</div>
)}
</div>
</div>
)
} }
export default Search export default Search

View File

@ -32,7 +32,7 @@
.btn-hover-animation { .btn-hover-animation {
@apply relative transform; @apply relative transform;
&::after { &::after {
@apply absolute top-0 left-0 z-[-1] h-full w-full scale-[0.92] rounded-lg opacity-0 transition-all duration-300; @apply absolute top-0 left-0 z-[-1] h-full w-full scale-[.92] rounded-lg opacity-0 transition-all duration-300;
content: ''; content: '';
} }

View File

@ -179,8 +179,7 @@ export class Player {
this.play() this.play()
this.state = State.PLAYING this.state = State.PLAYING
const id = this.trackID this._cacheAudio(this.trackID, audio)
_howler.once('load', () => this._cacheAudio(id, audio))
if (!this._progressInterval) { if (!this._progressInterval) {
this._setupProgressInterval() this._setupProgressInterval()