160 ? 'bg-[rgba(0,0,0,.35)]' : 'bg-[rgba(0,0,0,.1)]'
189 | }
190 | absolute='~'
191 | rounded='md'
192 | h='full'
193 | w='full'
194 | />
195 | {!currentMusic.initFlag ? (
196 | content
197 | ) : (
198 |
206 | 什么都没有哦~
207 |
208 | 快点击卡片右上角的按钮~
209 |
210 | 去选择你喜欢的音乐吧~
211 |
212 | )}
213 | {/* 更多按钮 */}
214 |
changePageActive()}
221 | />
222 |
223 | >
224 | )
225 | }
226 | )
227 |
228 | export default Card
229 |
--------------------------------------------------------------------------------
/src/components/card/style.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 | export const PanWrapper = styled.div`
3 | z-index: var(--pan-index);
4 | transition: all 0.5s;
5 | position: fixed;
6 | left: 3px;
7 | bottom: 50px;
8 | cursor: pointer;
9 | transform: translateX(-50%);
10 | &.active {
11 | transform: translate(25px, -215px);
12 | }
13 | &:hover.deactive {
14 | transform: translateX(0);
15 | }
16 |
17 | @keyframes cycle {
18 | from {
19 | transform: rotate(0);
20 | }
21 | to {
22 | transform: rotate(360deg);
23 | }
24 | }
25 | .bg-pan {
26 | background-size: 100%;
27 | animation: cycle infinite 10s linear;
28 | &:hover {
29 | box-shadow: 0 0 15px white;
30 | }
31 | &.pause {
32 | /* 暂停动画 */
33 | animation-play-state: paused;
34 | }
35 | }
36 | `
37 |
38 | export const CardWrapper = styled.div`
39 | height: var(--card-height);
40 | width: var(--card-width);
41 | position: fixed;
42 | left: 20px;
43 | bottom: 20px;
44 | display: flex;
45 | flex-direction: column;
46 | align-items: center;
47 | border-radius: 0.375rem;
48 | z-index: var(--card-index);
49 | transform: scale(0) translate(-500px, 500px);
50 | opacity: 0;
51 | background-size: cover;
52 | background-repeat: no-repeat;
53 | &.active {
54 | transform: scale(1) translate(0, 0);
55 | opacity: 1;
56 | }
57 |
58 | .arrow {
59 | position: absolute;
60 | top: 50%;
61 | transform: translateY(-50%);
62 | cursor: pointer;
63 | font-size: 20px;
64 |
65 | &:hover {
66 | transform: translateY(-50%) scale(1.2);
67 | }
68 | }
69 | .blur-8px {
70 | filter: blur(8px);
71 | }
72 | .blur-5px {
73 | filter: blur(5px);
74 | }
75 | .blur-2px {
76 | filter: blur(2px);
77 | }
78 |
79 | .volume-slider-hover {
80 | &:hover + div {
81 | opacity: 1;
82 | }
83 | }
84 | `
85 |
--------------------------------------------------------------------------------
/src/components/page/child-components/Footer.tsx:
--------------------------------------------------------------------------------
1 | import { IAudio } from '@/hooks/useAudio'
2 | import React, { memo } from 'react'
3 | const Footer = memo(
4 | (props: {
5 | audioInfo: IAudio
6 | TimeSlider: () => JSX.Element
7 | VolumeSlider: () => JSX.Element
8 | }) => {
9 | // 获取音频相关信息
10 | const {
11 | switchMusic,
12 | switchMusicStaus,
13 | changeJingyin,
14 | switchOrder,
15 | isPlaying,
16 | volume,
17 | currentOrder,
18 | } = props.audioInfo
19 | return (
20 |
65 | )
66 | }
67 | )
68 |
69 | export default Footer
70 |
--------------------------------------------------------------------------------
/src/components/page/child-components/Header.tsx:
--------------------------------------------------------------------------------
1 | import { useAppDispatch, useAppSelector } from '@/store'
2 | import { getUserInfoAction } from '@/store/user'
3 | import React, { memo, useEffect } from 'react'
4 | import { alert, confirm } from '@/common/modal'
5 | import useStorage from '@/hooks/useStorage'
6 | import { changeUid } from '@/store/user'
7 | const Header = memo(() => {
8 | const dispatch = useAppDispatch()
9 | const { uid, userInfo } = useAppSelector(state => state.user)
10 | useEffect(() => {
11 | dispatch(getUserInfoAction(uid))
12 | }, [dispatch, uid])
13 | const storage = useStorage()
14 | // 退出登录
15 | const logout = () =>
16 | confirm({
17 | children: 您确定要退出嘛~
,
18 | title: '提示',
19 | }).then(() => {
20 | storage.removeItem('uid')
21 | dispatch(changeUid(0))
22 | })
23 |
24 | // 登录
25 | const login = () =>
26 | alert(['uid'], {
27 | title: '提示',
28 | }).then((res: { uid: number }) => {
29 | const uid = res.uid ?? 0
30 | dispatch(changeUid(uid))
31 | storage.setItem('uid', uid)
32 | })
33 | return (
34 |
40 |
44 | 勾勾的音乐组件
45 |
46 |
53 | {uid !== 0 ? (
54 |

62 | ) : (
63 |
64 | )}
65 |
66 |
67 | )
68 | })
69 |
70 | export default Header
71 |
--------------------------------------------------------------------------------
/src/components/page/child-components/Music.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react'
2 | import { PAGE_SINGER_NULL_TEXT, PAGE_SONG_NULL_TEXT } from '@/constant'
3 | import { ILyric } from '@/hooks/useLyric'
4 | import LyricBox from '@/common/lyricBox'
5 | import { IMusicInfo } from '@/hooks/useMusic'
6 | const Music = memo((props: { lyricInfo: ILyric; musicInfo: IMusicInfo }) => {
7 | // 获取音乐信息相关
8 | const { singers, name: songName } = props.musicInfo
9 | // 获取歌词相关信息
10 | const { currentLyricIndex, lyricList, lyricBoxRef } = props.lyricInfo
11 | return (
12 |
47 | )
48 | })
49 |
50 | export default Music
51 |
--------------------------------------------------------------------------------
/src/components/page/child-views/Mine/hooks/useChangeActiveItem.ts:
--------------------------------------------------------------------------------
1 | import { getAllMusic, getPlayListDetail } from '@/service/music'
2 | import { PlayingListItem } from '@/store/user/types'
3 | import { MusicListItem } from '@/store/music/types'
4 | import { useState, useEffect, useMemo } from 'react'
5 | import { useAppDispatch } from '@/store'
6 | type Detail = {
7 | tracks: MusicListItem[]
8 | trackCount: number
9 | trackIds?: string[]
10 | }
11 |
12 | export default function () {
13 | // 当前点击的歌单
14 | const [activeItem, setActiveItem] = useState(null)
15 | const [detail, setDetail] = useState()
16 | const [tracks, setTracks] = useState()
17 | // const dispatch = useAppDispatch()
18 |
19 | useEffect(() => {
20 | activeItem?.id &&
21 | getPlayListDetail(activeItem.id).then(res => {
22 | // 先请求到所有trackIds
23 | setDetail(res.playlist)
24 | return getAllMusic(res.playlist.trackIds).then(res => {
25 | // 再根据trackIds获取所有的songs
26 | setTracks(res.songs)
27 | })
28 | })
29 | }, [activeItem])
30 |
31 | const [showLength, setShowLength] = useState(10)
32 |
33 | // 滚动到底部时触发的回调函数
34 | const showMore = async () => {
35 | if (detail && detail.trackCount > showLength) {
36 | setShowLength(showLength + 10)
37 | }
38 | }
39 |
40 | // 截取tracks
41 | const computedTracks = useMemo(() => {
42 | return tracks?.slice(0, showLength) ?? []
43 | }, [showLength, tracks])
44 |
45 | return { activeItem, detail, setActiveItem, showMore, computedTracks }
46 | }
47 |
--------------------------------------------------------------------------------
/src/components/page/child-views/Mine/hooks/useGetPlayList.ts:
--------------------------------------------------------------------------------
1 | import { useAppSelector } from '@/store'
2 | import useSWR from 'swr'
3 | import { getPlayList } from '@/service/user'
4 | const fetchePlayList = uid => {
5 | return getPlayList(Number(uid)).then(
6 | res => new Promise(resolve => setTimeout(resolve, 500, res))
7 | )
8 | }
9 | export default function () {
10 | // 歌单数据
11 | const { uid } = useAppSelector(state => state.user)
12 | // Suspense优化???
13 | // 最终决定不放在store中了
14 | const res = useSWR(uid + '', fetchePlayList, { suspense: true })
15 | const playlist = res.data.playlist
16 | return playlist
17 | }
18 |
--------------------------------------------------------------------------------
/src/components/page/child-views/Mine/index.tsx:
--------------------------------------------------------------------------------
1 | import MusicList from '@/common/musicList'
2 | import {
3 | LIST_NULL_TEXT,
4 | PAGE_MINE_DESC_NULL_TEXT,
5 | PAGE_MINE_TAGS_NULL_TEXT,
6 | } from '@/constant'
7 | import { pushPlayingMusicList, switchCurrentMusic } from '@/store/music'
8 | import { useAppDispatch } from '@/store'
9 | import { MusicListItem } from '@/store/music/types'
10 | import { formatCount, parseTime } from '@/utils'
11 | import React, { memo } from 'react'
12 | import useGetPlayList from './hooks/useGetPlayList'
13 | import useChangeActiveItem from './hooks/useChangeActiveItem'
14 |
15 | const Mine = memo(() => {
16 | const dispatch = useAppDispatch()
17 | const playList = useGetPlayList()
18 | const { setActiveItem, activeItem, computedTracks, showMore } =
19 | useChangeActiveItem()
20 | // 点击歌单详情列表的歌曲添加到playing中
21 | const pushIntoPlayingMusicList = (item: MusicListItem) => {
22 | dispatch(switchCurrentMusic(item))
23 | dispatch(pushPlayingMusicList(item))
24 | //TODO:push成功的dialog
25 | }
26 |
27 | return !playList?.length ? (
28 |
29 | {LIST_NULL_TEXT}
30 |
31 | ) : !activeItem ? (
32 |
33 | {playList.map(item => (
34 |
setActiveItem(item)}
38 | p='10px'
39 | flex='~ col'
40 | justify='center'
41 | relative='~'
42 | >
43 |
44 |

53 |
54 |
64 | {formatCount(item.playCount ?? 0, 0)}
65 |
66 |
70 | {item.description ?? '咋那么懒,连个描述都没~'}
71 |
72 |
73 | ))}
74 |
75 | ) : (
76 |
77 |
78 |
79 |

86 |
87 |
88 |
89 |
{activeItem.name}
90 |
97 |
98 |
99 | 创建时间:
100 | {parseTime(activeItem.createTime ?? 0)}
101 |
102 |
歌单作者:{activeItem.creator?.nickname}
103 |
播放量:{formatCount(activeItem.playCount ?? 0, 2)}
104 |
105 | 标签:
106 | {activeItem?.tags?.length
107 | ? activeItem?.tags?.map((item, index) => (
108 |
109 | {item}
110 |
111 | ))
112 | : PAGE_MINE_TAGS_NULL_TEXT}
113 |
114 |
115 |
116 | 介绍:
117 |
118 |
119 | {activeItem.description
120 | ?.split('\n')
121 | .map((item, index) =>
{item}
) ??
122 | PAGE_MINE_DESC_NULL_TEXT}
123 |
124 |
125 |
126 |
127 |
128 | showMore()}
132 | />
133 |
134 |
135 | )
136 | })
137 |
138 | export default Mine
139 |
--------------------------------------------------------------------------------
/src/components/page/child-views/Playing.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react'
2 | import { useAppDispatch, useAppSelector } from '@/store'
3 | import {
4 | initialCurrentMusic,
5 | removeFromPlayingMusicList,
6 | switchCurrentMusic,
7 | } from '@/store/music'
8 | import MusicList from '@/common/musicList'
9 | import useAudio from '@/hooks/useAudio'
10 | import type { MusicListItem } from '@/store/music/types'
11 | const Playing = memo(() => {
12 | const { currentMusic, playingMusicList } = useAppSelector(
13 | state => state.music
14 | )
15 |
16 | const dispatch = useAppDispatch()
17 |
18 | const { switchMusic } = useAudio()
19 |
20 | // 点击列表音乐时,切换音乐
21 | const rowClick = (item: MusicListItem) => {
22 | dispatch(switchCurrentMusic(item))
23 | }
24 |
25 | const remove = (item: MusicListItem) => {
26 | // 如果是相同的,就先跳到下一首,再删除
27 | if (currentMusic === item) {
28 | //最后一个元素,重置currentMusic
29 | playingMusicList.length === 1
30 | ? dispatch(switchCurrentMusic(initialCurrentMusic))
31 | : switchMusic('next')
32 | }
33 | dispatch(removeFromPlayingMusicList(item))
34 | }
35 |
36 | return (
37 |
42 | )
43 | })
44 |
45 | export default Playing
46 |
--------------------------------------------------------------------------------
/src/components/page/child-views/Recommend.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo, useEffect } from 'react'
2 | import { useAppDispatch, useAppSelector } from '@/store'
3 | import {
4 | fetchHotRecommend,
5 | pushPlayingMusicList,
6 | switchCurrentMusic,
7 | } from '@/store/music'
8 | import MusicList from '@/common/musicList'
9 | import type { MusicListItem } from '@/store/music/types'
10 |
11 | const Recommend = memo(() => {
12 | // 修改音乐
13 | const dispatch = useAppDispatch()
14 | const { dailyMusicList } = useAppSelector(state => state.music)
15 | useEffect(() => {
16 | // 请求热榜推荐歌曲的数据
17 | if (!dailyMusicList.length) dispatch(fetchHotRecommend())
18 | }, [dispatch])
19 | const pushIntoPlayingMusicList = (item: MusicListItem) => {
20 | dispatch(switchCurrentMusic(item))
21 | dispatch(pushPlayingMusicList(item))
22 | //TODO:push成功的dialog
23 | // alert('push成功')
24 | }
25 | return (
26 |
27 | )
28 | })
29 |
30 | export default Recommend
31 |
--------------------------------------------------------------------------------
/src/components/page/child-views/Search.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo, useEffect, useState } from 'react'
2 | import { getMusic, search } from '@/service/music'
3 | import { formatTime } from '@/utils'
4 | import { pushPlayingMusicList } from '@/store/music'
5 | import { useAppDispatch } from '@/store'
6 | import { LIST_NULL_TEXT } from '@/constant'
7 | import useScrollToBottom from '@/hooks/useScrollToBottom'
8 |
9 | const LIMIT = 30
10 |
11 | const Search = memo(() => {
12 | const dispatch = useAppDispatch()
13 | const [isFocus, setIsFocus] = useState(false)
14 | // 保存表单数据
15 | const [value, setValue] = useState('')
16 | const [dataList, setDataList] = useState([] as any[])
17 | const [songCount, setSongCount] = useState(0)
18 |
19 | useEffect(() => {
20 | // 防抖,获取搜索结果
21 | const timer = setTimeout(() => {
22 | search(value).then(res => {
23 | setDataList(res?.result?.songs ?? ([] as any[]))
24 | setSongCount(res?.result?.songCount)
25 | })
26 | }, 300)
27 | return () => clearTimeout(timer)
28 | }, [value])
29 |
30 | // 点击歌曲,添加到正在播放列表中
31 | const add = async (item: any) => {
32 | const res = await getMusic(item.id)
33 | dispatch(pushPlayingMusicList(res.songs[0]))
34 | }
35 |
36 | const showMore = async e => {
37 | const { scrollTop, scrollHeight, clientHeight } = e.target
38 | if (scrollTop + clientHeight >= scrollHeight) {
39 | if (dataList.length < songCount) {
40 | let offset = ~~(dataList.length / LIMIT)
41 | let limit = LIMIT
42 | if ((offset + 1) * LIMIT > songCount) {
43 | limit = songCount - offset * LIMIT
44 | }
45 | const res = await search(value, offset, limit)
46 | setDataList([...dataList, ...(res?.result?.songs ?? [])])
47 | }
48 | } else {
49 | console.log('还没滚动到底部')
50 | }
51 | }
52 |
53 | return (
54 |
55 |
63 | setIsFocus(true)}
74 | onBlur={() => setIsFocus(false)}
75 | value={value}
76 | onChange={e => setValue(e.target.value)}
77 | />
78 |
79 | {dataList.length ? (
80 |
81 |
88 |
89 | 歌曲
90 | 歌手
91 | 时长
92 |
93 |
showMore(e)}>
94 | {dataList.map((item, index) => (
95 |
add(item)}
104 | >
105 | {/* 序号 */}
106 |
107 | {index + 1}
108 |
109 | {/* 歌名 */}
110 | {item.name}
111 | {/* 歌手 */}
112 |
113 | {item.artists && item.artists[0].name}
114 |
115 | {/* 时常 */}
116 | {formatTime(item.duration ?? 0)}
117 |
118 | ))}
119 |
120 |
121 | ) : (
122 |
123 | {LIST_NULL_TEXT}
124 |
125 | )}
126 |
127 | )
128 | })
129 |
130 | export default Search
131 |
--------------------------------------------------------------------------------
/src/components/page/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo, useState, Suspense } from 'react'
2 |
3 | import { imgUrl } from '@/utils'
4 | import Mine from './child-views/Mine'
5 | import Search from './child-views/Search'
6 | import Recommend from './child-views/Recommend'
7 | import Header from './child-components/Header'
8 | import DivWrapper, { NavButton } from './style'
9 | import Playing from './child-views/Playing'
10 | import Footer from './child-components/Footer'
11 | import Music from './child-components/Music'
12 |
13 | import type { IMusicInfo } from '@/hooks/useMusic'
14 | import type { ILyric } from '@/hooks/useLyric'
15 | import type { IAudio } from '@/hooks/useAudio'
16 | //TODO:手机端兼容
17 | //TODO:使背景切换更自然
18 | const navList = [
19 | { title: '正在播放', element: },
20 | { title: '每日推荐', element: },
21 | { title: '搜索', element: },
22 | { title: '我的歌单', element: },
23 | ]
24 | const Page = memo(
25 | (props: {
26 | TimeSlider: () => JSX.Element
27 | VolumeSlider: () => JSX.Element
28 | musicInfo: IMusicInfo
29 | lyricInfo: ILyric
30 | audioInfo: IAudio
31 | }) => {
32 | // 切换content
33 | const [currentIndex, setCurrentIndex] = useState(0)
34 | const { musicInfo, lyricInfo, audioInfo, TimeSlider, VolumeSlider } = props
35 | // 获取音乐信息相关
36 | const { al } = musicInfo
37 |
38 | return (
39 |
40 | {/* 两张背景蒙版 */}
41 |
49 |
55 |
56 | {/* 内容 */}
57 |
64 |
65 | {/* 导航栏 */}
66 |
77 |
78 |
87 | loading...
88 |
89 | }
90 | >
91 | {/* 达到类似路由的效果 */}
92 | {navList[currentIndex].element}
93 |
94 |
95 |
96 |
97 |
98 |