mirror of
https://github.com/qier222/YesPlayMusic.git
synced 2025-04-01 21:55:12 +08:00
feat: 搜索页面和一堆更新
This commit is contained in:
parent
4d7bc14827
commit
b4590c3c34
@ -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],
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
4
packages/renderer/src/assets/icons/user.svg
Normal file
4
packages/renderer/src/assets/icons/user.svg
Normal 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 |
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -145,6 +145,7 @@ const CoverRow = ({
|
|||||||
<Cover
|
<Cover
|
||||||
onClick={() => goTo(item.id)}
|
onClick={() => goTo(item.id)}
|
||||||
imageUrl={getImageUrl(item)}
|
imageUrl={getImageUrl(item)}
|
||||||
|
showPlayButton={true}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@ -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>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
|
@ -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>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
1
packages/renderer/src/interface.d.ts
vendored
1
packages/renderer/src/interface.d.ts
vendored
@ -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[]
|
||||||
|
@ -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>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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}
|
||||||
|
@ -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>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -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> "{keywords}"
|
||||||
|
</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
|
||||||
|
@ -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: '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user