├── .env
├── src
├── react-app-env.d.ts
├── assets
│ └── images
│ │ ├── aa7.png
│ │ ├── oq.png
│ │ ├── logo.png
│ │ ├── menu
│ │ └── aip.png
│ │ ├── player
│ │ ├── adi.png
│ │ ├── disc.png
│ │ ├── list.png
│ │ ├── needle.png
│ │ ├── next.png
│ │ ├── pause.png
│ │ ├── play.png
│ │ ├── prev.png
│ │ ├── list-del.png
│ │ ├── min-list.png
│ │ ├── min-play.png
│ │ ├── min-pause.png
│ │ ├── mode-list.png
│ │ ├── mode-random.png
│ │ ├── mode-single.png
│ │ └── list-all-del.png
│ │ └── discover
│ │ ├── zf.png
│ │ ├── icn_fm.png
│ │ ├── icn_daily.png
│ │ ├── icn_rank.png
│ │ ├── icn_fm_prs.png
│ │ ├── icn_daily_prs.png
│ │ ├── icn_playlist.png
│ │ ├── icn_rank_prs.png
│ │ └── icn_playlist_prs.png
├── pages
│ ├── sheetlist
│ │ ├── sheetlist.scss
│ │ └── sheetlist.js
│ ├── skin
│ │ ├── skin.scss
│ │ └── skin.js
│ ├── playlist
│ │ ├── playlist.scss
│ │ └── playlist.js
│ ├── search
│ │ ├── search.scss
│ │ └── search.js
│ ├── toplist
│ │ ├── toplist.scss
│ │ └── toplist.js
│ ├── App.js
│ └── discover
│ │ ├── discover.js
│ │ └── discover.scss
├── base
│ ├── scroll
│ │ ├── scroll.css
│ │ └── scroll.js
│ ├── slide
│ │ ├── dot
│ │ │ ├── dot.scss
│ │ │ └── dot.js
│ │ ├── slide.scss
│ │ └── silde.js
│ ├── loading
│ │ ├── loading.js
│ │ └── loading.scss
│ ├── toast
│ │ ├── toast.scss
│ │ └── toast.js
│ ├── drawer
│ │ ├── drawer.scss
│ │ └── drawer.js
│ ├── columnList
│ │ ├── columnList.scss
│ │ └── columnList.js
│ ├── rowList
│ │ ├── rowList.scss
│ │ └── rowList.js
│ ├── songlist
│ │ ├── songlist.scss
│ │ └── songlist.js
│ ├── notification
│ │ ├── notice.js
│ │ └── notification.js
│ └── progress
│ │ ├── progress.scss
│ │ └── progress.js
├── components
│ ├── mm-header
│ │ ├── ov.png
│ │ ├── pf.png
│ │ ├── t_actionbar_friends_normal.png
│ │ ├── t_actionbar_music_normal.png
│ │ ├── t_actionbar_music_selected.png
│ │ ├── t_actionbar_video_normal.png
│ │ ├── t_actionbar_video_selected.png
│ │ ├── t_actionbar_discover_normal.png
│ │ ├── t_actionbar_friends_selected.png
│ │ ├── t_actionbar_discover_selected.png
│ │ ├── mm-header.js
│ │ └── mm-header.scss
│ ├── menu
│ │ ├── menu.js
│ │ ├── menu-item
│ │ │ └── menu-item.js
│ │ └── menu.scss
│ ├── mm-nav
│ │ ├── mm-nav.js
│ │ └── mm-nav.scss
│ ├── player
│ │ ├── cd
│ │ │ ├── cd.js
│ │ │ └── cd.scss
│ │ ├── music-list
│ │ │ ├── music-list.scss
│ │ │ └── music-list.js
│ │ ├── player.scss
│ │ └── player.js
│ └── search-list
│ │ ├── search-list.scss
│ │ └── search-list.js
├── config.js
├── store
│ ├── actionTypes.js
│ ├── index.js
│ ├── reducers.js
│ └── actions.js
├── utils
│ ├── axios.js
│ └── utils.js
├── styles
│ ├── playCount.scss
│ ├── mixin.scss
│ ├── var.scss
│ ├── reset.css
│ └── index.scss
├── index.js
├── model
│ ├── song.js
│ └── playlist.js
└── api
│ └── index.js
├── public
├── favicon.ico
├── manifest.json
└── index.html
├── .prettierrc
├── .editorconfig
├── .gitignore
├── .npmrc
├── .yarnrc
├── config-overrides.js
├── tsconfig.json
├── LICENSE
├── package.json
└── README.md
/.env:
--------------------------------------------------------------------------------
1 | REACT_APP_BASE_API_URL = http://localhost:3000
2 |
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maomao1996/react-music/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/src/assets/images/aa7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maomao1996/react-music/HEAD/src/assets/images/aa7.png
--------------------------------------------------------------------------------
/src/assets/images/oq.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maomao1996/react-music/HEAD/src/assets/images/oq.png
--------------------------------------------------------------------------------
/src/assets/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maomao1996/react-music/HEAD/src/assets/images/logo.png
--------------------------------------------------------------------------------
/src/pages/sheetlist/sheetlist.scss:
--------------------------------------------------------------------------------
1 | .sheetlist {
2 | .column-item {
3 | flex: 0 0 50%;
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/assets/images/menu/aip.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maomao1996/react-music/HEAD/src/assets/images/menu/aip.png
--------------------------------------------------------------------------------
/src/base/scroll/scroll.css:
--------------------------------------------------------------------------------
1 | .scroll-wrapper {
2 | width: 100%;
3 | height: 100%;
4 | overflow: hidden;
5 | }
6 |
--------------------------------------------------------------------------------
/src/assets/images/player/adi.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maomao1996/react-music/HEAD/src/assets/images/player/adi.png
--------------------------------------------------------------------------------
/src/components/mm-header/ov.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maomao1996/react-music/HEAD/src/components/mm-header/ov.png
--------------------------------------------------------------------------------
/src/components/mm-header/pf.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maomao1996/react-music/HEAD/src/components/mm-header/pf.png
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": false,
3 | "singleQuote": true,
4 | "trailingComma": "none",
5 | "printWidth": 80
6 | }
7 |
--------------------------------------------------------------------------------
/src/assets/images/discover/zf.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maomao1996/react-music/HEAD/src/assets/images/discover/zf.png
--------------------------------------------------------------------------------
/src/assets/images/player/disc.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maomao1996/react-music/HEAD/src/assets/images/player/disc.png
--------------------------------------------------------------------------------
/src/assets/images/player/list.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maomao1996/react-music/HEAD/src/assets/images/player/list.png
--------------------------------------------------------------------------------
/src/assets/images/player/needle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maomao1996/react-music/HEAD/src/assets/images/player/needle.png
--------------------------------------------------------------------------------
/src/assets/images/player/next.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maomao1996/react-music/HEAD/src/assets/images/player/next.png
--------------------------------------------------------------------------------
/src/assets/images/player/pause.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maomao1996/react-music/HEAD/src/assets/images/player/pause.png
--------------------------------------------------------------------------------
/src/assets/images/player/play.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maomao1996/react-music/HEAD/src/assets/images/player/play.png
--------------------------------------------------------------------------------
/src/assets/images/player/prev.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maomao1996/react-music/HEAD/src/assets/images/player/prev.png
--------------------------------------------------------------------------------
/src/assets/images/discover/icn_fm.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maomao1996/react-music/HEAD/src/assets/images/discover/icn_fm.png
--------------------------------------------------------------------------------
/src/assets/images/player/list-del.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maomao1996/react-music/HEAD/src/assets/images/player/list-del.png
--------------------------------------------------------------------------------
/src/assets/images/player/min-list.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maomao1996/react-music/HEAD/src/assets/images/player/min-list.png
--------------------------------------------------------------------------------
/src/assets/images/player/min-play.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maomao1996/react-music/HEAD/src/assets/images/player/min-play.png
--------------------------------------------------------------------------------
/src/assets/images/discover/icn_daily.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maomao1996/react-music/HEAD/src/assets/images/discover/icn_daily.png
--------------------------------------------------------------------------------
/src/assets/images/discover/icn_rank.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maomao1996/react-music/HEAD/src/assets/images/discover/icn_rank.png
--------------------------------------------------------------------------------
/src/assets/images/player/min-pause.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maomao1996/react-music/HEAD/src/assets/images/player/min-pause.png
--------------------------------------------------------------------------------
/src/assets/images/player/mode-list.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maomao1996/react-music/HEAD/src/assets/images/player/mode-list.png
--------------------------------------------------------------------------------
/src/assets/images/player/mode-random.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maomao1996/react-music/HEAD/src/assets/images/player/mode-random.png
--------------------------------------------------------------------------------
/src/assets/images/player/mode-single.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maomao1996/react-music/HEAD/src/assets/images/player/mode-single.png
--------------------------------------------------------------------------------
/src/assets/images/discover/icn_fm_prs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maomao1996/react-music/HEAD/src/assets/images/discover/icn_fm_prs.png
--------------------------------------------------------------------------------
/src/assets/images/player/list-all-del.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maomao1996/react-music/HEAD/src/assets/images/player/list-all-del.png
--------------------------------------------------------------------------------
/src/assets/images/discover/icn_daily_prs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maomao1996/react-music/HEAD/src/assets/images/discover/icn_daily_prs.png
--------------------------------------------------------------------------------
/src/assets/images/discover/icn_playlist.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maomao1996/react-music/HEAD/src/assets/images/discover/icn_playlist.png
--------------------------------------------------------------------------------
/src/assets/images/discover/icn_rank_prs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maomao1996/react-music/HEAD/src/assets/images/discover/icn_rank_prs.png
--------------------------------------------------------------------------------
/src/assets/images/discover/icn_playlist_prs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maomao1996/react-music/HEAD/src/assets/images/discover/icn_playlist_prs.png
--------------------------------------------------------------------------------
/src/components/mm-header/t_actionbar_friends_normal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maomao1996/react-music/HEAD/src/components/mm-header/t_actionbar_friends_normal.png
--------------------------------------------------------------------------------
/src/components/mm-header/t_actionbar_music_normal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maomao1996/react-music/HEAD/src/components/mm-header/t_actionbar_music_normal.png
--------------------------------------------------------------------------------
/src/components/mm-header/t_actionbar_music_selected.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maomao1996/react-music/HEAD/src/components/mm-header/t_actionbar_music_selected.png
--------------------------------------------------------------------------------
/src/components/mm-header/t_actionbar_video_normal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maomao1996/react-music/HEAD/src/components/mm-header/t_actionbar_video_normal.png
--------------------------------------------------------------------------------
/src/components/mm-header/t_actionbar_video_selected.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maomao1996/react-music/HEAD/src/components/mm-header/t_actionbar_video_selected.png
--------------------------------------------------------------------------------
/src/components/mm-header/t_actionbar_discover_normal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maomao1996/react-music/HEAD/src/components/mm-header/t_actionbar_discover_normal.png
--------------------------------------------------------------------------------
/src/components/mm-header/t_actionbar_friends_selected.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maomao1996/react-music/HEAD/src/components/mm-header/t_actionbar_friends_selected.png
--------------------------------------------------------------------------------
/src/components/mm-header/t_actionbar_discover_selected.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maomao1996/react-music/HEAD/src/components/mm-header/t_actionbar_discover_selected.png
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
--------------------------------------------------------------------------------
/src/config.js:
--------------------------------------------------------------------------------
1 | // 网络请求地址
2 | export const URL = 'http://localhost:3000'
3 |
4 | // 版本号
5 | export const VERSION = '1.1.1'
6 |
7 | // 默认分页数量
8 | export const defaultLimit = 20
9 |
10 | // 请求成功状态码
11 | export const HTTP_OK = 200
12 |
--------------------------------------------------------------------------------
/src/store/actionTypes.js:
--------------------------------------------------------------------------------
1 | export const SET_SHOW_PLAYER = 'SET_SHOW_PLAYER' // 是否显示Player组件
2 | export const SET_PLAYING = 'SET_PLAYING' // 是否播放
3 | export const SET_CURRENTMUSIC = 'SET_CURRENTMUSIC' // 设置当前音乐
4 | export const SET_CURRENTINDEX = 'SET_CURRENTINDEX' // 设置当前音乐索引
5 | export const SET_PLAYLIST = 'SET_PLAYLIST' // 设置当前播放列表
6 |
--------------------------------------------------------------------------------
/src/components/menu/menu.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import MenuItem from './menu-item/menu-item'
4 |
5 | import './menu.scss'
6 |
7 | // 抽屉菜单
8 |
9 | const Menu = (
10 |
14 | )
15 |
16 | export default Menu
17 |
--------------------------------------------------------------------------------
/src/utils/axios.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 |
3 | const request = axios.create({
4 | baseURL: process.env.REACT_APP_BASE_API_URL
5 | })
6 |
7 | request.interceptors.response.use(
8 | response => {
9 | if (response.status === 200) {
10 | return response
11 | }
12 | },
13 | error => Promise.reject(error)
14 | )
15 |
16 | export default request
17 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "茂茂听音乐",
3 | "name": "茂茂听音乐",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#31c27c",
14 | "background_color": "#f3f4f6"
15 | }
16 |
--------------------------------------------------------------------------------
/src/store/index.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware, compose } from 'redux'
2 | import thunk from 'redux-thunk'
3 | import reducer from './reducers'
4 |
5 | //创建store
6 | const store = createStore(
7 | reducer,
8 | compose(
9 | applyMiddleware(thunk),
10 | window.devToolsExtension ? window.devToolsExtension() : f => f //开启redux调试
11 | )
12 | )
13 |
14 | export default store
15 |
--------------------------------------------------------------------------------
/src/pages/skin/skin.scss:
--------------------------------------------------------------------------------
1 | @import '~@/styles/mixin.scss';
2 |
3 | .skin {
4 | .mm-content {
5 | display: flex;
6 | flex-flow: wrap;
7 | justify-content: space-between;
8 | @include scroll;
9 | padding-top: 4vw;
10 | }
11 | &-item {
12 | flex: 0 0 48vw;
13 | height: 48vw;
14 | margin-bottom: 4vw;
15 | text-align: center;
16 | line-height: 48vw;
17 | color: #fff;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.*.local
18 | .idea
19 | .vscode
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
25 | yarn.lock
26 | package-lock.json
27 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | registry=https://registry.npm.taobao.org
2 | sass_binary_site=https://npm.taobao.org/mirrors/node-sass/
3 | phantomjs_cdnurl=http://cnpmjs.org/downloads
4 | electron_mirror=https://npm.taobao.org/mirrors/electron/
5 | sqlite3_binary_host_mirror=https://foxgis.oss-cn-shanghai.aliyuncs.com/
6 | profiler_binary_host_mirror=https://npm.taobao.org/mirrors/node-inspector/
7 | chromedriver_cdnurl=https://cdn.npm.taobao.org/dist/chromedriver
8 |
--------------------------------------------------------------------------------
/.yarnrc:
--------------------------------------------------------------------------------
1 | registry "https://registry.npm.taobao.org"
2 | sass_binary_site "https://npm.taobao.org/mirrors/node-sass/"
3 | phantomjs_cdnurl "http://cnpmjs.org/downloads"
4 | electron_mirror "https://npm.taobao.org/mirrors/electron/"
5 | sqlite3_binary_host_mirror "https://foxgis.oss-cn-shanghai.aliyuncs.com/"
6 | profiler_binary_host_mirror "https://npm.taobao.org/mirrors/node-inspector/"
7 | chromedriver_cdnurl "https://cdn.npm.taobao.org/dist/chromedriver"
8 |
--------------------------------------------------------------------------------
/src/base/slide/dot/dot.scss:
--------------------------------------------------------------------------------
1 | @import '~@/styles/var.scss';
2 |
3 | .dots {
4 | position: absolute;
5 | right: 0;
6 | left: 0;
7 | bottom: 25px;
8 | transform: translateZ(1px);
9 | text-align: center;
10 | font-size: 0;
11 |
12 | .dot {
13 | display: inline-block;
14 | margin: 0 10px;
15 | width: 20px;
16 | height: 20px;
17 | border-radius: 50%;
18 | background-color: $slide-dot;
19 | &.on {
20 | background-color: $slide-dot-active;
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/base/slide/slide.scss:
--------------------------------------------------------------------------------
1 | .silde-wrapper {
2 | position: relative;
3 | width: 100%;
4 | height: 100%;
5 | overflow: hidden;
6 |
7 | .slide-group {
8 | position: relative;
9 | overflow: hidden;
10 | white-space: nowrap;
11 | height: 100%;
12 | }
13 |
14 | .slide-item {
15 | float: left;
16 | box-sizing: border-box;
17 | overflow: hidden;
18 | text-align: center;
19 | padding: 0 18px;
20 | height: 100%;
21 | }
22 |
23 | .slide-item img {
24 | width: 100%;
25 | height: 100%;
26 | object-fit: cover;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/config-overrides.js:
--------------------------------------------------------------------------------
1 | const {
2 | override,
3 | addPostcssPlugins,
4 | addWebpackAlias
5 | } = require('customize-cra')
6 | const path = require('path')
7 |
8 | module.exports = override(
9 | addWebpackAlias({
10 | '@': path.resolve(__dirname, 'src')
11 | }),
12 | addPostcssPlugins([
13 | require('postcss-px-to-viewport')({
14 | viewportWidth: 1080,
15 | unitPrecision: 5,
16 | viewportUnit: 'vw',
17 | selectorBlackList: [],
18 | minPixelValue: 1,
19 | mediaQuery: false,
20 | exclude: /node_modules/i
21 | })
22 | ])
23 | )
24 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "module": "esnext",
16 | "moduleResolution": "node",
17 | "resolveJsonModule": true,
18 | "isolatedModules": true,
19 | "noEmit": true,
20 | "jsx": "react"
21 | },
22 | "include": [
23 | "src"
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------
/src/base/loading/loading.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | import './loading.scss'
5 |
6 | // Loading组件
7 |
8 | const Loading = props => {
9 | const { show, text } = props
10 | return (
11 |
12 | {text}
13 |
14 | )
15 | }
16 |
17 | Loading.defaultProps = {
18 | text: '努力加载中...',
19 | show: true
20 | }
21 |
22 | Loading.propTypes = {
23 | show: PropTypes.bool,
24 | text: PropTypes.string
25 | }
26 |
27 | export default Loading
28 |
--------------------------------------------------------------------------------
/src/components/mm-nav/mm-nav.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { withRouter } from 'react-router-dom'
4 |
5 | import './mm-nav.scss'
6 |
7 | // 页面导航栏组件
8 |
9 | const MmNav = props => {
10 | const { title = '歌单', history } = props
11 | return (
12 |
17 | )
18 | }
19 |
20 | MmNav.propTypes = {
21 | title: PropTypes.string //标题
22 | }
23 |
24 | export default withRouter(MmNav)
25 |
--------------------------------------------------------------------------------
/src/components/menu/menu-item/menu-item.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { withRouter } from 'react-router-dom'
4 |
5 | // 抽屉菜单子组件
6 |
7 | const MenuItem = props => {
8 | const { title, path } = props
9 | const open = function openRoute() {
10 | path && props.history.push({ pathname: path })
11 | }
12 | return (
13 |
17 | )
18 | }
19 |
20 | MenuItem.propTypes = {
21 | title: PropTypes.string.isRequired,
22 | path: PropTypes.string
23 | }
24 |
25 | export default withRouter(MenuItem)
26 |
--------------------------------------------------------------------------------
/src/base/slide/dot/dot.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import classNames from 'classnames'
4 |
5 | import './dot.scss'
6 |
7 | // 分页器组件
8 |
9 | const Dot = ({ data, currentIndex }) => {
10 | return (
11 |
12 | {data.length > 0 &&
13 | data.map((item, index) => (
14 |
18 | ))}
19 |
20 | )
21 | }
22 |
23 | Dot.propTypes = {
24 | data: PropTypes.array.isRequired,
25 | currentIndex: PropTypes.number
26 | }
27 |
28 | export default Dot
29 |
--------------------------------------------------------------------------------
/src/components/player/cd/cd.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import classNames from 'classnames'
4 |
5 | import './cd.scss'
6 |
7 | // CD组件
8 |
9 | const Cd = props => {
10 | const { isPlay, image } = props
11 | return (
12 |
13 |
14 |
15 |
16 |

17 |
18 |
19 | )
20 | }
21 |
22 | Cd.propTypes = {
23 | isPlay: PropTypes.bool.isRequired, // 播放状态
24 | image: PropTypes.string.isRequired // 当前音乐图片
25 | }
26 |
27 | export default Cd
28 |
--------------------------------------------------------------------------------
/src/components/mm-nav/mm-nav.scss:
--------------------------------------------------------------------------------
1 | @import '~@/styles/var.scss';
2 | @import '~@/styles/mixin.scss';
3 |
4 | .mm-nav {
5 | position: relative;
6 | height: 120px;
7 | padding: 20px 170px;
8 | line-height: 120px;
9 | color: $nav-color;
10 | background-color: $nav-bg;
11 | overflow: hidden;
12 | &-left {
13 | position: absolute;
14 | top: 44px;
15 | left: 44px;
16 | z-index: 1;
17 | display: block;
18 | width: 72px;
19 | height: 72px;
20 | @include bg-url('~@/assets/images/oq.png');
21 | @include bg-full;
22 | }
23 | &-title {
24 | position: relative;
25 | z-index: 1;
26 | width: 100%;
27 | font-size: $font-size-medium-x;
28 | }
29 | &-right {
30 | position: absolute;
31 | right: 0;
32 | z-index: 1;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/styles/playCount.scss:
--------------------------------------------------------------------------------
1 | //播放数量
2 | @mixin playCount {
3 | position: relative;
4 | &:before {
5 | content: '';
6 | position: absolute;
7 | top: 0;
8 | left: 0;
9 | right: 0;
10 | z-index: 1;
11 | height: 80px;
12 | background: linear-gradient(
13 | to bottom,
14 | rgba(0, 0, 0, 0.35) 0,
15 | rgba(0, 0, 0, 0.2) 80%,
16 | rgba(0, 0, 0, 0) 100%
17 | );
18 | }
19 | &:after {
20 | content: attr(data-play);
21 | position: absolute;
22 | top: 15px;
23 | right: 20px;
24 | z-index: 1;
25 | display: block;
26 | height: 40px;
27 | line-height: 40px;
28 | padding-left: 40px;
29 | font-size: $font-size-small;
30 | color: #fff;
31 | @include bg-url('~@/assets/images/discover/zf.png');
32 | @include bg-full($p: left 0, $s: 30px);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/components/menu/menu.scss:
--------------------------------------------------------------------------------
1 | @import '~@/styles/var.scss';
2 | @import '~@/styles/mixin.scss';
3 |
4 | .drawer-sidebar .menu {
5 | height: 100%;
6 | @include scroll;
7 | &-title {
8 | height: 480px;
9 | text-align: center;
10 | line-height: 480px;
11 | background-color: $theme-bg;
12 | opacity: 0.8;
13 | @include bg-url('~@/assets/images/menu/aip.png');
14 | @include bg-full;
15 | }
16 |
17 | &-item {
18 | display: flex;
19 | height: 100px;
20 | padding: 30px;
21 | border-bottom: 1px solid #ededed;
22 | line-height: 100px;
23 | p {
24 | flex: 1;
25 | }
26 | span {
27 | display: block;
28 | width: 60px;
29 | height: 100px;
30 | @include bg-url('~@/assets/images/aa7.png');
31 | @include bg-full($s: 30px, $p: center right);
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/base/toast/toast.scss:
--------------------------------------------------------------------------------
1 | $toastPrefixCls: mm-toast;
2 |
3 | .#{$toastPrefixCls} {
4 | position: fixed;
5 | z-index: 1996;
6 | font-size: 42px;
7 | text-align: center;
8 | transform: translateZ(1px);
9 | &.#{$toastPrefixCls}-mask {
10 | display: flex;
11 | justify-content: center;
12 | align-items: center;
13 | left: 0;
14 | top: 0;
15 | width: 100%;
16 | height: 100vh;
17 | }
18 | &.#{$toastPrefixCls}-nomask {
19 | max-width: 50%;
20 | width: auto;
21 | left: 50%;
22 | top: 50%;
23 | .#{$toastPrefixCls}-notice {
24 | transform: translateX(-50%) translateY(-50%);
25 | }
26 | }
27 | &-info {
28 | min-width: 250px;
29 | border-radius: 10px;
30 | color: #fff;
31 | background-color: rgba(58, 58, 58, 0.9);
32 | line-height: 1.5;
33 | padding: 20px 50px;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import { Provider } from 'react-redux'
4 |
5 | import App from '@/pages/App'
6 | import store from './store'
7 | import { VERSION } from './config'
8 |
9 | import '@/styles/index.scss'
10 | // import registerServiceWorker from './registerServiceWorker';
11 |
12 | // if(document.querySelector("#appQd")){
13 | // setTimeout(()=>{
14 | // document.body.removeChild(document.querySelector("#appQd"))
15 | // },2000);
16 | // }
17 |
18 | // 版权信息
19 | window.mmPlayer = window.mmplayer = `欢迎使用 茂茂听音乐!
20 | 当前版本为:V${VERSION}
21 | 作者:茂茂
22 | Github:https://github.com/maomao1996/react-music
23 | 歌曲来源于网易云音乐 (http://music.163.com)`
24 | console.info(`%c${window.mmPlayer}`, `color:blue`)
25 |
26 | ReactDOM.render(
27 |
28 |
29 | ,
30 | document.getElementById('root')
31 | )
32 | // registerServiceWorker();
33 |
--------------------------------------------------------------------------------
/src/components/search-list/search-list.scss:
--------------------------------------------------------------------------------
1 | @import '~@/styles/mixin.scss';
2 | @import '~@/styles/var.scss';
3 |
4 | .search-list {
5 | position: relative;
6 | height: 100%;
7 | & &-tab {
8 | display: flex;
9 | padding-bottom: 20px;
10 | line-height: 100px;
11 | color: rgba(255, 255, 255, 0.5);
12 | background-color: $theme-bg;
13 | li {
14 | flex: 1;
15 | text-align: center;
16 | span {
17 | padding-bottom: 15px;
18 | }
19 | &.active span {
20 | font-weight: 700;
21 | color: #fff;
22 | border-bottom: 6px solid #fff;
23 | }
24 | }
25 | }
26 | & &-content {
27 | position: absolute;
28 | top: 120px;
29 | right: 0;
30 | bottom: 0;
31 | left: 0;
32 | @include scroll;
33 | .search-content-item {
34 | display: none;
35 | &.active {
36 | display: block;
37 | }
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/styles/mixin.scss:
--------------------------------------------------------------------------------
1 | // 省略号
2 | @mixin no-wrap($line: 1) {
3 | overflow: hidden;
4 | @if $line == 1 {
5 | text-overflow: ellipsis;
6 | white-space: nowrap;
7 | } @else {
8 | display: -webkit-box;
9 | -webkit-line-clamp: $line;
10 | text-overflow: ellipsis;
11 | -webkit-box-orient: vertical;
12 | }
13 | }
14 |
15 | // 图片
16 | @mixin img-full($attr: cover) {
17 | width: 100%;
18 | height: 100%;
19 | object-fit: $attr;
20 | }
21 |
22 | // 背景图
23 | @mixin bg-full($p: center, $s: contain, $r: no-repeat) {
24 | background-position: $p;
25 | background-size: $s;
26 | background-repeat: $r;
27 | }
28 |
29 | @mixin bg-url($url) {
30 | background-image: url($url);
31 | }
32 |
33 | // 滚动
34 | @mixin scroll($type: y) {
35 | @if $type == x {
36 | overflow-x: auto;
37 | overflow-y: hidden;
38 | } @else {
39 | overflow-x: hidden;
40 | overflow-y: auto;
41 | }
42 | -webkit-overflow-scrolling: touch;
43 | }
44 |
--------------------------------------------------------------------------------
/src/base/drawer/drawer.scss:
--------------------------------------------------------------------------------
1 | $classPrefix: drawer;
2 | .#{$classPrefix} {
3 | position: relative;
4 | width: 100%;
5 | height: 100%;
6 | overflow: hidden;
7 | &.#{$classPrefix}-open {
8 | .#{$classPrefix}-sidebar {
9 | transform: translate3d(0, 0, 0);
10 | }
11 | .#{$classPrefix}-overlay {
12 | opacity: 1;
13 | visibility: visible;
14 | }
15 | }
16 | &-sidebar {
17 | position: absolute;
18 | top: 0;
19 | bottom: 0;
20 | left: 0;
21 | z-index: 1996;
22 | width: 900px;
23 | background: #fff;
24 | transform: translate3d(-101%, 0, 0);
25 | will-change: transform;
26 | transition: all 0.15s ease-out;
27 | }
28 | &-overlay {
29 | position: absolute;
30 | top: 0;
31 | left: 0;
32 | right: 0;
33 | bottom: 0;
34 | z-index: 1;
35 | opacity: 0;
36 | visibility: hidden;
37 | transition: opacity 0.2s ease-out;
38 | background-color: rgba(0, 0, 0, 0.4);
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/base/drawer/drawer.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import classNames from 'classnames'
4 |
5 | import './drawer.scss'
6 |
7 | // 抽屉组件
8 |
9 | const Drawer = props => {
10 | const { className, isDrawer, sidebar, children } = props
11 | const onClose = function closeDrawer() {
12 | props.onOpen(false)
13 | }
14 |
15 | return (
16 |
17 |
{children}
18 |
19 | {sidebar}
20 |
21 |
22 |
23 | )
24 | }
25 |
26 | Drawer.propTypes = {
27 | children: PropTypes.node.isRequired, //主体内容
28 | isDrawer: PropTypes.bool.isRequired, //开启状态
29 | className: PropTypes.string, //主体内容class名
30 | sidebar: PropTypes.node.isRequired //抽屉里的内容
31 | }
32 |
33 | export default Drawer
34 |
--------------------------------------------------------------------------------
/src/base/columnList/columnList.scss:
--------------------------------------------------------------------------------
1 | @import '~@/styles/var.scss';
2 | @import '~@/styles/mixin.scss';
3 | @import '~@/styles/playCount.scss';
4 |
5 | $classPrefix: column;
6 | .#{$classPrefix}-wrapper {
7 | display: flex;
8 | flex-flow: wrap;
9 | padding: 0 10px;
10 | .#{$classPrefix}-item {
11 | min-width: 0;
12 | flex: 0 0 33.33333%;
13 | box-sizing: border-box;
14 | padding: 20px 5px 0;
15 | .#{$classPrefix}-img {
16 | width: 100%;
17 | height: 0;
18 | padding-top: 100%;
19 | border-radius: 10px;
20 | overflow: hidden;
21 | @include playCount;
22 | img {
23 | position: absolute;
24 | top: 0;
25 | left: 0;
26 | @include img-full;
27 | }
28 | }
29 | .#{$classPrefix}-title {
30 | height: 100px;
31 | margin: 20px 10px;
32 | line-height: 50px;
33 | color: $columnList-title-color;
34 | font-size: $font-size-small-x;
35 | word-wrap: break-word;
36 | word-break: break-all;
37 | @include no-wrap(2);
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/components/mm-header/mm-header.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { NavLink, withRouter } from 'react-router-dom'
3 |
4 | import './mm-header.scss'
5 |
6 | // header组件
7 |
8 | const MmHeader = props => {
9 | const showHeader = /music|discover|video/.test(props.location.pathname)
10 | const open = function mmHeaderOpenDrawer() {
11 | props.onOpen(true)
12 | }
13 | const openSearch = function mmHeaderOpenDrawer() {
14 | props.history.push('/search')
15 | }
16 | return (
17 | showHeader && (
18 |
27 | )
28 | )
29 | }
30 |
31 | export default withRouter(MmHeader)
32 |
--------------------------------------------------------------------------------
/src/base/rowList/rowList.scss:
--------------------------------------------------------------------------------
1 | @import '~@/styles/mixin.scss';
2 | @import '~@/styles/var.scss';
3 |
4 | $classPrefix: row;
5 | .#{$classPrefix}-wrapper {
6 | .#{$classPrefix}-item {
7 | display: flex;
8 | align-items: center;
9 | padding: 10px 0 10px 20px;
10 | &:not(:last-child) {
11 | border-bottom: 1px solid $rowList-line;
12 | }
13 | .#{$classPrefix}-hd {
14 | width: 160px;
15 | height: 160px;
16 | margin-right: 30px;
17 | border-radius: 10px;
18 | overflow: hidden;
19 | }
20 | .#{$classPrefix}-bd {
21 | flex: 1;
22 | min-width: 0;
23 | padding: 25px 15px 25px 0;
24 | h2 {
25 | height: 50px;
26 | margin-bottom: 20px;
27 | line-height: 50px;
28 | font-size: 46px;
29 | color: $rowList-title-color;
30 | @include no-wrap;
31 | }
32 | p {
33 | height: 50px;
34 | line-height: 50px;
35 | font-size: 34px;
36 | color: $rowList-desc-color;
37 | @include no-wrap;
38 | }
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 茂茂
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/base/songlist/songlist.scss:
--------------------------------------------------------------------------------
1 | @import '~@/styles/mixin.scss';
2 | @import '~@/styles/var.scss';
3 |
4 | .song-wrapper {
5 | .song-item {
6 | display: flex;
7 | padding-left: 20px;
8 | &:not(:last-child) .song-info {
9 | border-bottom: 1px solid $songList-line;
10 | }
11 | .song-num {
12 | width: 160px;
13 | height: 160px;
14 | margin-left: -20px;
15 | text-align: center;
16 | line-height: 160px;
17 | }
18 | .song-info {
19 | flex: 1;
20 | min-width: 0;
21 | padding: 25px 15px 25px 0;
22 | h2 {
23 | height: 50px;
24 | margin-bottom: 20px;
25 | line-height: 50px;
26 | font-size: 46px;
27 | color: $songist-title-color;
28 | @include no-wrap;
29 | }
30 | p {
31 | height: 50px;
32 | line-height: 50px;
33 | font-size: 34px;
34 | color: $songist-desc-color;
35 | @include no-wrap;
36 | }
37 | }
38 | .song-btn {
39 | }
40 | &.active {
41 | .song-num {
42 | color: $theme-color;
43 | }
44 | h2,
45 | p {
46 | color: $theme-color;
47 | }
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/base/rowList/rowList.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | import { formatPlayCount } from '@/utils/utils'
5 |
6 | import './rowList.scss'
7 |
8 | // 歌单基础列表组件——行
9 |
10 | const RowList = props => {
11 | const { list, onItemClick } = props
12 | return (
13 |
14 | {list.length > 0 &&
15 | list.map((item, index) => (
16 |
onItemClick(item.id, index)}
19 | key={item.id}
20 | >
21 |
22 |

23 |
24 |
25 |
{item.name}
26 |
27 | {item.trackCount}首 by {item.nickname}, 播放
28 | {formatPlayCount(item.playCount)}次
29 |
30 |
31 |
32 | ))}
33 |
34 | )
35 | }
36 |
37 | RowList.propTypes = {
38 | list: PropTypes.any.isRequired,
39 | onItemClick: PropTypes.func.isRequired
40 | }
41 |
42 | export default RowList
43 |
--------------------------------------------------------------------------------
/src/base/columnList/columnList.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | import { formatPlayCount } from '@/utils/utils'
5 |
6 | import './columnList.scss'
7 |
8 | // 歌曲基础列表组件——列
9 |
10 | const ColumnList = props => {
11 | const { list, onItemClick } = props
12 | return (
13 |
14 | {list.length > 0 &&
15 | list.map(item => {
16 | return (
17 |
onItemClick(item.id)}
20 | key={item.id}
21 | >
22 |
26 |

32 |
33 |
{item.name.replace(/\s/g, ' ')}
34 |
35 | )
36 | })}
37 |
38 | )
39 | }
40 |
41 | ColumnList.propTypes = {
42 | list: PropTypes.any.isRequired,
43 | onItemClick: PropTypes.func.isRequired
44 | }
45 |
46 | export default ColumnList
47 |
--------------------------------------------------------------------------------
/src/base/songlist/songlist.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import classNames from 'classnames'
4 |
5 | import './songlist.scss'
6 |
7 | // 歌曲基础列表组件
8 |
9 | const BaseSongList = props => {
10 | const { list, showRank, onItemClick, activeId } = props
11 | return (
12 |
13 | {list.length > 0 &&
14 | list.map((item, index) => (
15 |
onItemClick(item.id, index)}
20 | key={item.id}
21 | >
22 | {showRank &&
{index + 1}
}
23 |
24 |
{item.name}
25 |
26 | {item.singer} - {item.album}
27 |
28 |
29 |
30 |
31 | ))}
32 |
33 | )
34 | }
35 |
36 | BaseSongList.propTypes = {
37 | list: PropTypes.any.isRequired,
38 | showRank: PropTypes.bool,
39 | onItemClick: PropTypes.func.isRequired,
40 | activeId: PropTypes.number || PropTypes.string
41 | }
42 |
43 | export default BaseSongList
44 |
--------------------------------------------------------------------------------
/src/utils/utils.js:
--------------------------------------------------------------------------------
1 | // 播放数量
2 | export const formatPlayCount = item => {
3 | return item / 10000 > 9
4 | ? item / 10000 > 10000
5 | ? `${(item / 100000000).toFixed(1)}亿`
6 | : `${Math.ceil(item / 10000)}万`
7 | : Math.floor(item)
8 | }
9 |
10 | // 补0函数
11 | export const addZero = s => {
12 | return s < 10 ? '0' + s : s
13 | }
14 |
15 | // 播放时间
16 | export const formatTime = s => {
17 | let minute = Math.floor(s / 60)
18 | let second = Math.floor(s % 60)
19 | return `${addZero(minute)}:${addZero(second)}`
20 | }
21 |
22 | /**
23 | * 找到并返回应项的索引
24 | * @param list list
25 | * @param music 查找对象
26 | */
27 | export const findIndex = (list, music) => {
28 | return list.findIndex(item => {
29 | return item.id === music.id
30 | })
31 | }
32 |
33 | // 防抖函数
34 | export const debounce = function(func, delay) {
35 | let timer
36 | return function(...args) {
37 | if (timer) {
38 | clearTimeout(timer)
39 | }
40 | timer = setTimeout(() => {
41 | func.apply(this, args)
42 | }, delay)
43 | }
44 | }
45 |
46 | // 节流函数
47 | export const throttle = function(func, delay) {
48 | let now = Date.now()
49 | return function(...args) {
50 | const current = Date.now()
51 | if (current - now >= delay) {
52 | func.apply(this, args)
53 | now = current
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/base/notification/notice.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | class Notice extends Component {
5 | static propTypes = {
6 | duration: PropTypes.number, // 显示时间
7 | prefixCls: PropTypes.string, // 前缀class
8 | onClose: PropTypes.func // 回调函数
9 | }
10 |
11 | static defaultProps = {
12 | prefixCls: 'mm-toast',
13 | onClose() {},
14 | duration: 1500
15 | }
16 |
17 | componentDidMount() {
18 | this.startCloseTimer()
19 | }
20 |
21 | componentWillUnmount() {
22 | this.clearCloseTimer()
23 | }
24 |
25 | // 执行回调
26 | close = () => {
27 | this.clearCloseTimer()
28 | this.props.onClose()
29 | }
30 |
31 | // 设置定时器
32 | startCloseTimer = () => {
33 | if (this.props.duration) {
34 | this.closeTimer = setTimeout(() => {
35 | this.close()
36 | }, this.props.duration)
37 | }
38 | }
39 |
40 | // 清空定时器
41 | clearCloseTimer = () => {
42 | if (this.closeTimer) {
43 | clearTimeout(this.closeTimer)
44 | this.closeTimer = null
45 | }
46 | }
47 |
48 | render() {
49 | const props = this.props
50 | return (
51 |
52 |
53 | {props.children}
54 |
55 |
56 | )
57 | }
58 | }
59 |
60 | export default Notice
61 |
--------------------------------------------------------------------------------
/src/styles/var.scss:
--------------------------------------------------------------------------------
1 | //颜色定义规范
2 | // $theme: #e5473c; // 网易红
3 | //$theme: #31c27c; // 企鹅绿
4 | //$theme: #0c8ed9; // 酷狗蓝
5 | //$theme: #f60; // 虾米橙
6 |
7 | /**
8 | * 主题色
9 | * 后缀说明:
10 | * color为文字颜色
11 | * bg为背景颜色
12 | * 若不带color或者bg后缀均为背景颜色(特殊说明除外)
13 | */
14 | $theme-color: var(--THEME);
15 | $theme-bg: var(--THEME);
16 | $theme-main-color: #f3f4f6;
17 | $theme-sub-bg: #fff;
18 | $theme-sub-color: #fff;
19 |
20 | //header组件(一级导航栏)
21 | $header-bg: $theme-bg;
22 |
23 | //nav组件(二级导航栏)
24 | $nav-color: $theme-sub-color;
25 | $nav-bg: $theme-bg;
26 |
27 | //loading组件
28 | $loading-color: #666;
29 |
30 | //slide组件(轮播)
31 | $slide-dot: #fff;
32 | $slide-dot-active: $theme-bg;
33 |
34 | //进度条组件
35 | $mmProgress-bar: rgba(255, 255, 255, 0.15);
36 | $mmProgress-outer: rgba(255, 255, 255, 0.2);
37 | $mmProgress-dot-outer: $theme-sub-bg;
38 | $mmProgress-dot-inner: $theme-bg;
39 |
40 | //歌单列表组件——行
41 | $rowList-title-color: #333;
42 | $rowList-desc-color: #777;
43 | $rowList-line: #ddd;
44 |
45 | //歌单列表组件——列
46 | $columnList-title-color: #333;
47 |
48 | //歌单详情列表
49 | $songist-title-color: #333;
50 | $songist-desc-color: #777;
51 | $songList-line: #ddd;
52 |
53 | //字体大小定义规范
54 | $font-size-small-ss: 30px;
55 | $font-size-small-s: 32px;
56 | $font-size-small: 34px;
57 | $font-size-small-x: 36px;
58 | $font-size-medium: 38px;
59 | $font-size-medium-x: 40px;
60 | $font-size-large-s: 42px;
61 | $font-size-large: 48px;
62 | $font-size-large-x: 50px;
63 |
--------------------------------------------------------------------------------
/src/base/progress/progress.scss:
--------------------------------------------------------------------------------
1 | @import '~@/styles/var.scss';
2 |
3 | .mmProgress {
4 | position: relative;
5 | width: 100%;
6 | height: 2Px;
7 | padding: 9Px 0;
8 | margin: 0 10Px;
9 | user-select: none;
10 | & .mmProgress-bar {
11 | height: 2Px;
12 | width: 100%;
13 | background-color: $mmProgress-bar;
14 | }
15 | & .mmProgress-outer {
16 | position: absolute;
17 | top: 50%;
18 | left: 0;
19 | display: inline-block;
20 | width: 0;
21 | height: 2Px;
22 | margin-top: -1px;
23 | background-color: $mmProgress-outer;
24 | }
25 | & .mmProgress-inner {
26 | position: absolute;
27 | top: 50%;
28 | left: 0;
29 | display: inline-block;
30 | width: 0;
31 | height: 2Px;
32 | transform: translateY(-50%);
33 | background-color: $theme-bg;
34 | //小圆点
35 | .mmProgress-dot {
36 | position: absolute;
37 | top: 50%;
38 | right: -9Px;
39 | width: 18Px;
40 | height: 18Px;
41 | border-radius: 50%;
42 | transform: translate3d(0, -50%, 0);
43 | background-color: $mmProgress-dot-outer;
44 | &:after {
45 | content: '';
46 | position: absolute;
47 | top: 50%;
48 | left: 50%;
49 | width: 5Px;
50 | height: 5Px;
51 | border-radius: 50%;
52 | background-color: $mmProgress-dot-inner;
53 | transform: translate3d(-50%, -50%, 0);
54 | }
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/components/mm-header/mm-header.scss:
--------------------------------------------------------------------------------
1 | @import '~@/styles/mixin.scss';
2 | @import '~@/styles/var.scss';
3 |
4 | .mm-header {
5 | position: relative;
6 | background-color: $header-bg;
7 | .mm-header-left {
8 | position: absolute;
9 | top: 45px;
10 | left: 40px;
11 | width: 70px;
12 | height: 70px;
13 | background-image: url('ov.png');
14 | @include bg-full;
15 | }
16 | .mm-header-title {
17 | display: flex;
18 | justify-content: center;
19 | padding: 45px 290px;
20 | .mm-header-item {
21 | flex: 0 0 33.333333%;
22 | height: 70px;
23 | @include bg-full($s: cover);
24 | &.music {
25 | background-image: url('t_actionbar_music_normal.png');
26 | &.active {
27 | background-image: url('t_actionbar_music_selected.png');
28 | }
29 | }
30 | &.discover {
31 | background-image: url('t_actionbar_discover_normal.png');
32 | &.active {
33 | background-image: url('t_actionbar_discover_selected.png');
34 | }
35 | }
36 | &.video {
37 | background-image: url('t_actionbar_video_normal.png');
38 | &.active {
39 | background-image: url('t_actionbar_video_selected.png');
40 | }
41 | }
42 | }
43 | }
44 | .mm-header-right {
45 | position: absolute;
46 | top: 45px;
47 | right: 33px;
48 | width: 70px;
49 | height: 70px;
50 | background-image: url('pf.png');
51 | @include bg-full;
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/pages/playlist/playlist.scss:
--------------------------------------------------------------------------------
1 | @import '~@/styles/var.scss';
2 | @import '~@/styles/mixin.scss';
3 | @import '~@/styles/playCount.scss';
4 |
5 | .playlist {
6 | height: 100%;
7 | & .mm-blur-min {
8 | position: fixed;
9 | height: 160px;
10 | .mm-blur-bg {
11 | top: 160px;
12 | &:after {
13 | background-color: rgba(0, 0, 0, 0.2);
14 | }
15 | }
16 | }
17 | & .mm-content {
18 | position: relative;
19 | }
20 | & &-header {
21 | position: relative;
22 | padding: 45px;
23 | overflow: hidden;
24 | &-wrapper {
25 | position: relative;
26 | z-index: 2;
27 | display: flex;
28 | }
29 | .playlist-header-hd {
30 | width: 300px;
31 | height: 300px;
32 | @include playCount;
33 | img {
34 | @include img-full;
35 | }
36 | }
37 | .playlist-header-bd {
38 | min-width: 0;
39 | flex: 1;
40 | margin-left: 50px;
41 | h1 {
42 | height: 140px;
43 | margin-bottom: 30px;
44 | line-height: 70px;
45 | color: #fff;
46 | font-weight: 700;
47 | }
48 | .playlist-header-user {
49 | img {
50 | width: 60px;
51 | height: 60px;
52 | margin-right: 30px;
53 | border-radius: 50%;
54 | vertical-align: middle;
55 | }
56 | span {
57 | font-size: 32px;
58 | color: rgba(255, 255, 255, 0.5);
59 | }
60 | }
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/store/reducers.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux'
2 | import * as ActionTypes from './actionTypes'
3 |
4 | // 初始数据
5 | const initialState = {
6 | showPlayer: false, //Player显示状态
7 | playList: [], //播放列表
8 | currentIndex: -1, //当前音乐索引
9 | currentMusic: {} //当前音乐
10 | }
11 |
12 | // 是否显示Player组件
13 | function showPlayer(showPlayer = initialState.showPlayer, action) {
14 | switch (action.type) {
15 | case ActionTypes.SET_SHOW_PLAYER:
16 | return action.showPlayer
17 | default:
18 | return showPlayer
19 | }
20 | }
21 |
22 | // 设置当前音乐
23 | function currentMusic(currentMusic = initialState.currentMusic, action) {
24 | switch (action.type) {
25 | case ActionTypes.SET_CURRENTMUSIC:
26 | return action.currentMusic
27 | default:
28 | return currentMusic
29 | }
30 | }
31 |
32 | // 设置当前音乐索引
33 | function currentIndex(currentIndex = initialState.currentIndex, action) {
34 | switch (action.type) {
35 | case ActionTypes.SET_CURRENTINDEX:
36 | return action.currentIndex
37 | default:
38 | return currentIndex
39 | }
40 | }
41 |
42 | // 设置当前播放列表
43 | function playList(playList = initialState.playList, action) {
44 | switch (action.type) {
45 | case ActionTypes.SET_PLAYLIST:
46 | return action.playList
47 | default:
48 | return playList
49 | }
50 | }
51 |
52 | const reducer = combineReducers({
53 | showPlayer,
54 | currentMusic,
55 | currentIndex,
56 | playList
57 | })
58 |
59 | export default reducer
60 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-music",
3 | "version": "1.1.1",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^4.2.4",
7 | "@testing-library/react": "^9.3.2",
8 | "@testing-library/user-event": "^7.1.2",
9 | "@types/jest": "^24.0.0",
10 | "@types/node": "^12.0.0",
11 | "@types/react": "^16.9.0",
12 | "@types/react-dom": "^16.9.0",
13 | "axios": "^0.19.0",
14 | "better-scroll": "^1.15.2",
15 | "classnames": "^2.2.6",
16 | "cross-env": "^6.0.3",
17 | "customize-cra": "^0.9.1",
18 | "node-sass": "^4.13.0",
19 | "postcss-px-to-viewport": "^1.1.1",
20 | "react": "^16.12.0",
21 | "react-app-rewired": "^2.1.5",
22 | "react-dom": "^16.12.0",
23 | "react-redux": "^7.1.3",
24 | "react-router-dom": "^5.1.2",
25 | "react-scripts": "^3.4.1",
26 | "redux": "^4.0.4",
27 | "redux-thunk": "^2.3.0",
28 | "sass-loader": "^8.0.0",
29 | "typescript": "~3.7.2"
30 | },
31 | "scripts": {
32 | "start": "cross-env PORT=8163 react-app-rewired start",
33 | "build": "react-app-rewired build",
34 | "test": "react-app-rewired test",
35 | "eject": "react-scripts eject"
36 | },
37 | "eslintConfig": {
38 | "extends": "react-app"
39 | },
40 | "browserslist": {
41 | "production": [
42 | ">0.2%",
43 | "not dead",
44 | "not op_mini all"
45 | ],
46 | "development": [
47 | "last 1 chrome version",
48 | "last 1 firefox version",
49 | "last 1 safari version"
50 | ]
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/store/actions.js:
--------------------------------------------------------------------------------
1 | import * as ActionTypes from './actionTypes'
2 | import { findIndex } from '@/utils/utils'
3 |
4 | // 显示Player组件
5 | export function setShowPlayer(showPlayer) {
6 | return { type: ActionTypes.SET_SHOW_PLAYER, showPlayer }
7 | }
8 |
9 | // 设置当前音乐
10 | export function setCurrentMusic(currentMusic) {
11 | return { type: ActionTypes.SET_CURRENTMUSIC, currentMusic }
12 | }
13 |
14 | // 设置当前音乐索引
15 | export function setCurrentIndex(currentIndex) {
16 | return { type: ActionTypes.SET_CURRENTINDEX, currentIndex }
17 | }
18 |
19 | // 设置当前播放列表
20 | export function setPlayList(playList) {
21 | return { type: ActionTypes.SET_PLAYLIST, playList }
22 | }
23 |
24 | // 播放歌曲(替换歌单列表)
25 | export const setAllPlay = ({ playList, currentIndex }) => dispatch => {
26 | dispatch(setShowPlayer(true))
27 | dispatch(setPlayList(playList))
28 | dispatch(setCurrentIndex(currentIndex))
29 | dispatch(setCurrentMusic(playList[currentIndex]))
30 | }
31 |
32 | // 播放歌曲(插入一条到播放列表)
33 | export const addPlay = music => (dispatch, getState) => {
34 | let playList = [...getState().playList]
35 | //查询当前播放列表是否有待插入的音乐,并返回其索引
36 | let index = findIndex(playList, music)
37 | //当前播放列表有待插入的音乐时,直接改变当前播放音乐的索引
38 | if (index > -1) {
39 | dispatch(setCurrentIndex(index))
40 | dispatch(setCurrentMusic(playList[index]))
41 | } else {
42 | index = playList.push(music) - 1
43 | dispatch(setPlayList(playList))
44 | dispatch(setCurrentIndex(index))
45 | dispatch(setCurrentMusic(playList[index]))
46 | }
47 | dispatch(setShowPlayer(true))
48 | }
49 |
--------------------------------------------------------------------------------
/src/components/player/cd/cd.scss:
--------------------------------------------------------------------------------
1 | @import '~@/styles/mixin.scss';
2 |
3 | .player-cd {
4 | position: relative;
5 | width: 100%;
6 | height: 100%;
7 | overflow: hidden;
8 | &.pause {
9 | .needle {
10 | transform: translate3d(-35px, 0, 0) rotate(-30deg);
11 | }
12 | .disc-box {
13 | animation-play-state: paused;
14 | }
15 | }
16 | .needle {
17 | position: absolute;
18 | top: -50px;
19 | left: 50%;
20 | z-index: 9;
21 | width: 276px;
22 | height: 414px;
23 | @include bg-url('~@/assets/images/player/needle.png');
24 | @include bg-full;
25 | transform: translate3d(-35px, 0, 0);
26 | transform-origin: 48px 48px;
27 | transition: all 0.3s;
28 | }
29 | .disc-box {
30 | position: absolute;
31 | top: 190px;
32 | left: 50%;
33 | width: 804px;
34 | height: 804px;
35 | margin-left: -402px;
36 | animation: circle-rotate 12s linear infinite;
37 | .disc {
38 | position: relative;
39 | z-index: 1;
40 | width: 100%;
41 | height: 100%;
42 | border-radius: 50%;
43 | background: rgba(255, 255, 255, 0.1);
44 | @include bg-url('~@/assets/images/player/disc.png');
45 | @include bg-full;
46 | }
47 | img {
48 | position: absolute;
49 | top: 50%;
50 | left: 50%;
51 | width: 530px;
52 | height: 530px;
53 | transform: translate(-50%, -50%);
54 | }
55 | }
56 | }
57 |
58 | @keyframes circle-rotate {
59 | 0% {
60 | transform: rotate(0);
61 | }
62 | 100% {
63 | transform: rotate(360deg);
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/base/toast/toast.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Notification from '@/base/notification/notification'
3 | import classNames from 'classnames'
4 |
5 | import './toast.scss'
6 |
7 | const ToastPrefixCls = 'mm-toast'
8 | let newNotification
9 |
10 | // 获得一个Notification
11 | function getNewNotification(mask, callBack) {
12 | if (newNotification) {
13 | newNotification.destroy()
14 | newNotification = null
15 | }
16 | Notification.newInstance(
17 | {
18 | prefixCls: ToastPrefixCls,
19 | className: classNames({
20 | [`${ToastPrefixCls}-mask`]: mask,
21 | [`${ToastPrefixCls}-nomask`]: !mask
22 | })
23 | },
24 | notification => callBack && callBack(notification)
25 | )
26 | }
27 |
28 | // 配置Toast
29 | function notice(content, duration, mask, onClose) {
30 | getNewNotification(mask, notification => {
31 | newNotification = notification
32 | notification.notice({
33 | duration, // 显示时长
34 | mask, // 遮罩
35 | content: (
36 |
43 | ), // 内容
44 | onClose() {
45 | onClose && onClose() // 回调方法
46 | notification.destroy()
47 | notification = null
48 | newNotification = null
49 | } // 移除
50 | })
51 | })
52 | }
53 |
54 | export default {
55 | // 显示
56 | show(content, duration, mask, onClose) {
57 | return notice(content, duration, mask, onClose)
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/pages/search/search.scss:
--------------------------------------------------------------------------------
1 | @import '~@/styles/mixin.scss';
2 | @import '~@/styles/var.scss';
3 |
4 | .search {
5 | & &-head {
6 | position: relative;
7 | padding: 25px 50px 25px 170px;
8 | overflow: hidden;
9 | background-color: $theme-bg;
10 | &-left {
11 | position: absolute;
12 | top: 44px;
13 | left: 44px;
14 | display: block;
15 | width: 72px;
16 | height: 72px;
17 | @include bg-url('~@/assets/images/oq.png');
18 | @include bg-full;
19 | }
20 | .search-head-box {
21 | border-bottom: 1px solid rgba(255, 255, 255, 0.6);
22 | padding: 25px 0;
23 | }
24 | .search-head-input {
25 | width: 100%;
26 | height: 60px;
27 | border: 0;
28 | line-height: 60px;
29 | outline: 0;
30 | font-size: 50px;
31 | color: rgba(255, 255, 255, 0.9);
32 | background-color: transparent;
33 | &::-webkit-input-placeholder {
34 | color: rgba(255, 255, 255, 0.6);
35 | }
36 | }
37 | }
38 |
39 | //热门搜索
40 | & .search-hots {
41 | width: 100%;
42 | h1 {
43 | height: 100px;
44 | padding-top: 30px;
45 | padding-left: 30px;
46 | line-height: 100px;
47 | font-size: 36px;
48 | color: #888;
49 | }
50 | ul {
51 | font-size: 0;
52 | }
53 | li {
54 | display: inline-block;
55 | border: 1px solid #ccc;
56 | padding: 30px;
57 | margin-left: 30px;
58 | margin-bottom: 30px;
59 | border-radius: 100px;
60 | font-size: 42px;
61 | color: #000;
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/model/song.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 歌曲类模型
3 | */
4 |
5 | function filterSinger(singers) {
6 | if (!Array.isArray(singers) || !singers.length) {
7 | return ''
8 | }
9 | let arr = []
10 | singers.forEach(item => {
11 | arr.push(item.name)
12 | })
13 | return arr.join('/')
14 | }
15 |
16 | export class Song {
17 | constructor({ id, name, singer, album, image, duration }) {
18 | this.id = id //歌曲ID
19 | this.name = name //歌曲名称
20 | this.singer = singer //歌手
21 | this.album = album //专辑
22 | this.image = image //封面图
23 | this.duration = duration //时长
24 | // this.url = url //URL地址
25 | }
26 | }
27 |
28 | export function createSongs(music) {
29 | if (music.dt !== undefined) {
30 | return new Song({
31 | id: music.id,
32 | name: music.name,
33 | singer: filterSinger(music.ar),
34 | album: music.al.name,
35 | image: music.al.picUrl || null,
36 | duration: music.dt / 1000
37 | // url: `https://music.163.com/song/media/outer/url?id=${music.id}.mp3`
38 | })
39 | }
40 | return new Song({
41 | id: music.id,
42 | name: music.name,
43 | singer: filterSinger(music.artists),
44 | album: music.album.name,
45 | image: music.album.picUrl || null,
46 | duration: music.duration / 1000
47 | // url: `https://music.163.com/song/media/outer/url?id=${music.id}.mp3`
48 | })
49 | }
50 |
51 | // 歌曲数据格式化
52 | const formatSongs = function(list) {
53 | let Songs = []
54 | list.forEach(item => {
55 | if (item.id) {
56 | Songs.push(createSongs(item))
57 | }
58 | })
59 | return Songs
60 | }
61 |
62 | export default formatSongs
63 |
--------------------------------------------------------------------------------
/src/pages/skin/skin.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import MmNav from '@/components/mm-nav/mm-nav'
4 | import Toast from '@/base/toast/toast'
5 |
6 | import './skin.scss'
7 |
8 | // 皮肤切换
9 |
10 | export default function Skin() {
11 | const skinList = [
12 | {
13 | name: '网易红',
14 | key: 'neteaseSkin',
15 | value: '#e5473c'
16 | },
17 | {
18 | name: '企鹅绿',
19 | key: 'qqSkin',
20 | value: '#31c27c'
21 | },
22 | {
23 | name: '酷狗蓝',
24 | key: 'kuGouSkin',
25 | value: '#0c8ed9'
26 | },
27 | {
28 | name: '虾米橙',
29 | key: 'xiaMiSkin',
30 | value: '#f60'
31 | },
32 | {
33 | name: '炫酷黑',
34 | key: 'xuanKuSkin',
35 | value: '#222'
36 | },
37 | {
38 | name: '可爱粉',
39 | key: 'keAiSkin',
40 | value: '#ff87b4'
41 | },
42 | {
43 | name: '土豪金',
44 | key: 'tuHaoSkin',
45 | value: '#faac62'
46 | }
47 | ]
48 |
49 | const handleToggleSkin = value => {
50 | const elm = document.documentElement
51 | elm.style.setProperty('--USER-THEME-COLOR', value)
52 | Toast.show('皮肤修改成功', 1000, false)
53 | }
54 |
55 | return (
56 |
57 |
58 |
59 | {skinList.map(item => (
60 |
handleToggleSkin(item.value)}
65 | >
66 | {item.name}
67 |
68 | ))}
69 |
70 |
71 | )
72 | }
73 |
--------------------------------------------------------------------------------
/src/api/index.js:
--------------------------------------------------------------------------------
1 | import axios from '@/utils/axios'
2 | import { defaultLimit } from '@/config'
3 |
4 | axios.interceptors.response.use(response => {
5 | //欺骗自己的loading动画
6 | // return new Promise(resolve => {
7 | // setTimeout(() => {
8 | // resolve(response)
9 | // }, 300)
10 | // })
11 | return response
12 | })
13 |
14 | //获取轮播
15 | export function getBanner() {
16 | return axios.get('/banner')
17 | }
18 |
19 | //获取推荐歌单
20 | export function getPersonalized() {
21 | return axios.get('/personalized')
22 | }
23 |
24 | //获取用户歌单详情
25 | export function getUserPlaylist(uid) {
26 | return axios.get('/user/playlist', {
27 | params: {
28 | uid
29 | }
30 | })
31 | }
32 |
33 | //获取排行榜(完整版)
34 | export function getTopListDetail() {
35 | return axios.get('/toplist/detail')
36 | }
37 |
38 | //获取歌单 ( 网友精选碟 )
39 | export function getTopPlaylist(page = 0, limit = defaultLimit, order = 'hot') {
40 | return axios.get('/top/playlist', {
41 | params: {
42 | offset: page * limit,
43 | order,
44 | limit
45 | }
46 | })
47 | }
48 |
49 | //获取歌单详情
50 | export function getPlaylistDetail(id) {
51 | return axios.get('/playlist/detail', {
52 | params: {
53 | id
54 | }
55 | })
56 | }
57 |
58 | // 搜索
59 | export function search(keywords, type = 1, page = 0, limit = defaultLimit) {
60 | return axios.get('/search', {
61 | params: {
62 | offset: page * limit,
63 | type,
64 | limit,
65 | keywords
66 | }
67 | })
68 | }
69 |
70 | //热搜
71 | export function searchHot() {
72 | return axios.get('/search/hot')
73 | }
74 |
75 | //获取歌曲详情
76 | export function getMusicDetail(ids) {
77 | return axios.get('/song/detail', {
78 | params: {
79 | ids
80 | }
81 | })
82 | }
83 |
--------------------------------------------------------------------------------
/src/pages/toplist/toplist.scss:
--------------------------------------------------------------------------------
1 | @import '~@/styles/mixin.scss';
2 |
3 | .toplist {
4 | background-color: #f3f4f6;
5 | & &-title {
6 | position: relative;
7 | width: 100%;
8 | height: 152px;
9 | line-height: 152px;
10 | font-size: 46px;
11 | font-weight: bold;
12 | text-indent: 20px;
13 | }
14 | & .row-list {
15 | overflow: hidden;
16 | padding-left: 18px;
17 | margin-bottom: 30px;
18 | }
19 | & .row-list .row-item {
20 | display: flex;
21 | margin-bottom: 10px;
22 | width: 100%;
23 | .item-hd {
24 | width: 356px;
25 | height: 356px;
26 | }
27 | .row-item-bd {
28 | display: flex;
29 | flex-direction: column;
30 | justify-content: space-around;
31 | flex: 1;
32 | min-width: 0;
33 | box-sizing: border-box;
34 | padding: 0 32px;
35 | border-bottom: 1px solid rgba(0, 0, 0, 0.1);
36 | p {
37 | height: 40px;
38 | line-height: 40px;
39 | font-size: 38px;
40 | color: #666;
41 | word-break: keep-all;
42 | @include no-wrap;
43 | }
44 | }
45 | }
46 | & .item-hd {
47 | position: relative;
48 | img {
49 | border-radius: 5px;
50 | }
51 | p {
52 | position: absolute;
53 | left: 15px;
54 | bottom: 15px;
55 | font-size: 20px;
56 | color: #fff;
57 | }
58 | }
59 | & .column-list {
60 | padding: 12px;
61 | overflow: hidden;
62 | .column-item {
63 | float: left;
64 | width: 342px;
65 | margin: 0 5px 45px;
66 | .item-hd {
67 | width: 344px;
68 | height: 344px;
69 | }
70 | .column-bd {
71 | height: 120px;
72 | line-height: 60px;
73 | font-size: 38px;
74 | }
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/components/player/music-list/music-list.scss:
--------------------------------------------------------------------------------
1 | @import '~@/styles/var.scss';
2 | @import '~@/styles/mixin.scss';
3 |
4 | .musicList {
5 | position: absolute;
6 | top: 0;
7 | right: 0;
8 | bottom: 0;
9 | left: 0;
10 | z-index: 1990;
11 | transform: translate3d(0, 100%, 0);
12 | transition: all 0.2s ease-out;
13 | & .musicList-mask {
14 | position: absolute;
15 | top: 0;
16 | left: 0;
17 | width: 100vw;
18 | height: 100vh;
19 | background-color: rgba(0, 0, 0, 0.5);
20 | opacity: 0;
21 | transition: all 0.1s ease-out 0.1s;
22 | }
23 | &.active {
24 | transform: translate3d(0, 0, 0);
25 | .musicList-mask {
26 | opacity: 1;
27 | }
28 | }
29 | & &-wrapper {
30 | position: absolute;
31 | bottom: 0;
32 | left: 0;
33 | z-index: 1;
34 | width: 100vw;
35 | height: 60vh;
36 | border-radius: 10px 10px 0 0;
37 | overflow: hidden;
38 | background-color: $theme-main-color;
39 | }
40 | & &-header {
41 | display: flex;
42 | height: 150px;
43 | padding: 0 30px;
44 | border-bottom: 1px solid #eee;
45 | line-height: 150px;
46 | font-size: $font-size-large-s;
47 | }
48 | & &-item {
49 | display: flex;
50 | height: 140px;
51 | margin-left: 30px;
52 | line-height: 140px;
53 | font-size: $font-size-large-s;
54 | color: #333;
55 | &:not(:last-child) {
56 | border-bottom: 1px solid #eee;
57 | }
58 | &.active {
59 | color: $theme-color;
60 | small {
61 | color: inherit;
62 | }
63 | }
64 | h2 {
65 | min-width: 0;
66 | flex: 1;
67 | @include no-wrap;
68 | }
69 | small {
70 | font-size: $font-size-small;
71 | color: #888;
72 | }
73 | &-del {
74 | width: 110px;
75 | height: 100%;
76 | @include bg-full;
77 | @include bg-url('~@/assets/images/player/list-del.png');
78 | }
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/components/player/music-list/music-list.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import classNames from 'classnames'
4 |
5 | import Scroll from '@/base/scroll/scroll'
6 |
7 | import './music-list.scss'
8 |
9 | const MusicList = props => {
10 | const { show, onMaskClick, list, music, onItemClick, deleteClick } = props
11 | const maskClick = e => {
12 | onMaskClick(e, false)
13 | e.stopPropagation()
14 | }
15 | return (
16 |
17 |
18 |
19 |
20 | 当前歌曲数:{list.length}
21 |
22 |
23 |
24 | {list.length > 0 &&
25 | list.map((item, index) => (
26 |
32 |
onItemClick(item.id, index)}>
33 | {item.name}
34 | - {item.singer}
35 |
36 | deleteClick(item.id, index)}
39 | />
40 |
41 | ))}
42 |
43 |
44 |
45 | )
46 | }
47 |
48 | MusicList.propTypes = {
49 | show: PropTypes.bool.isRequired,
50 | onMaskClick: PropTypes.func.isRequired,
51 | list: PropTypes.any.isRequired,
52 | music: PropTypes.object.isRequired,
53 | onItemClick: PropTypes.func.isRequired,
54 | deleteClick: PropTypes.func.isRequired
55 | }
56 |
57 | export default MusicList
58 |
--------------------------------------------------------------------------------
/src/styles/reset.css:
--------------------------------------------------------------------------------
1 | html,
2 | body,
3 | div,
4 | span,
5 | applet,
6 | object,
7 | iframe,
8 | h1,
9 | h2,
10 | h3,
11 | h4,
12 | h5,
13 | h6,
14 | p,
15 | blockquote,
16 | pre,
17 | a,
18 | abbr,
19 | acronym,
20 | address,
21 | big,
22 | cite,
23 | code,
24 | del,
25 | dfn,
26 | em,
27 | img,
28 | ins,
29 | kbd,
30 | q,
31 | s,
32 | samp,
33 | small,
34 | strike,
35 | strong,
36 | sub,
37 | sup,
38 | tt,
39 | var,
40 | b,
41 | u,
42 | i,
43 | center,
44 | dl,
45 | dt,
46 | dd,
47 | ol,
48 | ul,
49 | li,
50 | fieldset,
51 | form,
52 | label,
53 | legend,
54 | table,
55 | caption,
56 | tbody,
57 | tfoot,
58 | thead,
59 | tr,
60 | th,
61 | td,
62 | article,
63 | aside,
64 | canvas,
65 | details,
66 | embed,
67 | figure,
68 | figcaption,
69 | footer,
70 | header,
71 | hgroup,
72 | menu,
73 | nav,
74 | output,
75 | ruby,
76 | section,
77 | summary,
78 | time,
79 | mark,
80 | audio,
81 | video {
82 | margin: 0;
83 | padding: 0;
84 | border: 0;
85 | font-size: 100%;
86 | font: inherit;
87 | vertical-align: baseline;
88 | -webkit-tap-highlight-color: transparent;
89 | }
90 |
91 | /* HTML5 display-role reset for older browsers */
92 | article,
93 | aside,
94 | details,
95 | figcaption,
96 | figure,
97 | footer,
98 | header,
99 | hgroup,
100 | menu,
101 | nav,
102 | section {
103 | display: block;
104 | }
105 |
106 | body {
107 | line-height: 1;
108 | }
109 |
110 | ol,
111 | ul {
112 | list-style: none;
113 | }
114 |
115 | blockquote,
116 | q {
117 | quotes: none;
118 | }
119 |
120 | blockquote:before,
121 | blockquote:after,
122 | q:before,
123 | q:after {
124 | content: '';
125 | content: none;
126 | }
127 |
128 | table {
129 | border-collapse: collapse;
130 | border-spacing: 0;
131 | }
132 |
133 | a {
134 | text-decoration: none;
135 | color: inherit;
136 | }
137 |
138 | input[type='number']::-webkit-inner-spin-button,
139 | input[type='number']::-webkit-outer-spin-button {
140 | -webkit-appearance: none;
141 | }
142 |
--------------------------------------------------------------------------------
/src/pages/sheetlist/sheetlist.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 |
3 | import Loading from '@/base/loading/loading'
4 | import MmNav from '@/components/mm-nav/mm-nav'
5 | import Scroll from '@/base/scroll/scroll'
6 | import ColumnList from '@/base/columnList/columnList'
7 |
8 | import { getTopPlaylist } from '@/api'
9 | import { HTTP_OK } from '@/config'
10 | import formatPlayList from '@/model/playlist'
11 |
12 | import './sheetlist.scss'
13 |
14 | class SheetList extends Component {
15 | constructor(props) {
16 | super(props)
17 | this.state = {
18 | loading: false,
19 | options: {
20 | pullUpLoad: true,
21 | probeType: 2
22 | },
23 | page: 0,
24 | data: []
25 | }
26 | }
27 |
28 | componentDidMount() {
29 | this.setState({
30 | loading: true
31 | })
32 | this._getTopPlaylist()
33 | }
34 |
35 | // 获取歌单
36 | _getTopPlaylist() {
37 | getTopPlaylist(this.state.page).then(res => {
38 | if (res.data.code === HTTP_OK) {
39 | // console.log(res.data);
40 | const data = this.state.data,
41 | page = this.state.page + 1
42 | this.setState({
43 | data: data.concat(formatPlayList(res.data.playlists)),
44 | loading: false,
45 | page
46 | })
47 | }
48 | })
49 | }
50 |
51 | // 上拉加载
52 | pullUpLoad = () => {
53 | // console.log('上拉');
54 | this.setState({
55 | loading: true
56 | })
57 | this._getTopPlaylist()
58 | }
59 |
60 | render() {
61 | const { loading, options, data } = this.state
62 | return (
63 |
64 |
65 |
70 | this.props.history.push(`/playlist/${id}`)}
73 | />
74 |
75 |
76 |
77 | )
78 | }
79 | }
80 |
81 | export default SheetList
82 |
--------------------------------------------------------------------------------
/src/styles/index.scss:
--------------------------------------------------------------------------------
1 | @import 'reset.css';
2 | @import 'var.scss';
3 | @import 'mixin.scss';
4 |
5 | // 间接设置全局动态属性可以防止它们在局部被覆
6 | :root {
7 | --THEME: var(--USER-THEME-COLOR, #e5473c);
8 | --THEME-COLOR: var(--USER-THEME-COLOR, #e5473c);
9 | }
10 |
11 | //#root, body, html {
12 | // width: 100%;
13 | // height: 100%;
14 | // overflow: hidden;
15 | //}
16 |
17 | #root {
18 | position: fixed;
19 | top: 0;
20 | left: 0;
21 | bottom: 0;
22 | right: 0;
23 | }
24 |
25 | .mm-block {
26 | display: block !important;
27 | }
28 |
29 | .mm-none {
30 | display: none !important;
31 | }
32 |
33 | //flex
34 | .mm-wrapper {
35 | display: flex;
36 | flex-direction: column;
37 | height: 100%;
38 | overflow: hidden;
39 | }
40 |
41 | .mm-content {
42 | min-height: 0;
43 | flex: 1;
44 | overflow: hidden;
45 | background-color: $theme-main-color;
46 | }
47 |
48 | //position
49 | //.mm-wrapper {
50 | // position: relative;
51 | // height: 100%;
52 | // overflow: hidden;
53 | //}
54 | //
55 | //.mm-content {
56 | // position: absolute;
57 | // top: 160px;
58 | // bottom: 0;
59 | // width: 100%;
60 | // overflow: hidden;
61 | //}
62 |
63 | .mm-blur {
64 | position: absolute;
65 | top: 0;
66 | bottom: 0;
67 | left: 0;
68 | width: 100vw;
69 | overflow: hidden;
70 | & .mm-blur-bg {
71 | position: absolute;
72 | top: 0;
73 | left: 0;
74 | width: 100vw;
75 | height: 100vw;
76 | filter: blur(30px);
77 | transform: scale(1.5);
78 | @include bg-full($p: 50%, $s: cover);
79 | &:after {
80 | content: ' ';
81 | position: absolute;
82 | left: 0;
83 | top: 0;
84 | right: 0;
85 | bottom: 0;
86 | z-index: 1;
87 | background-color: rgba(0, 0, 0, 0.25);
88 | }
89 | }
90 | }
91 |
92 | img {
93 | max-width: 100%;
94 | }
95 |
96 | .translate-enter {
97 | transform: translate3d(100%, 0, 0);
98 | &.translate-enter-active {
99 | transition: transform 0.3s;
100 | transform: translate3d(0, 0, 0);
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/src/pages/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component, lazy, Suspense } from 'react'
2 | import {
3 | BrowserRouter as Router,
4 | Route,
5 | Switch,
6 | Redirect
7 | } from 'react-router-dom'
8 | import { connect } from 'react-redux'
9 |
10 | import Drawer from '@/base/drawer/drawer'
11 | import Loading from '@/base/loading/loading'
12 | import MmHeader from '@/components/mm-header/mm-header'
13 | import Player from '@/components/player/player'
14 | import Menu from '@/components/menu/menu'
15 |
16 | const Discover = lazy(() => import('@/pages/discover/discover'))
17 | const Search = lazy(() => import('@/pages/search/search'))
18 | const TopList = lazy(() => import('@/pages/toplist/toplist'))
19 | const PlayList = lazy(() => import('@/pages/playlist/playlist'))
20 | const SheetList = lazy(() => import('@/pages/sheetlist/sheetlist'))
21 | const Skin = lazy(() => import('@/pages/skin/skin'))
22 |
23 | class App extends Component {
24 | constructor(props) {
25 | super(props)
26 | this.state = {
27 | isDrawer: false
28 | }
29 | }
30 |
31 | openDrawer = state => {
32 | this.setState({
33 | isDrawer: state
34 | })
35 | }
36 |
37 | render() {
38 | return (
39 |
40 |
46 |
47 |
48 | }>
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 | {this.props.showPlayer && }
61 |
62 |
63 | )
64 | }
65 | }
66 |
67 | //映射Redux全局的state到组件的props上
68 | const mapStateToProps = state => ({
69 | showPlayer: state.showPlayer
70 | })
71 |
72 | export default connect(mapStateToProps)(App)
73 |
--------------------------------------------------------------------------------
/src/pages/search/search.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { withRouter } from 'react-router-dom'
3 | import classNames from 'classnames'
4 |
5 | import SearchList from '@/components/search-list/search-list'
6 |
7 | import { searchHot } from '@/api'
8 | import { HTTP_OK } from '@/config'
9 |
10 | import './search.scss'
11 |
12 | // 搜索页面
13 |
14 | class Search extends Component {
15 | constructor(props) {
16 | super(props)
17 | this.state = {
18 | value: '',
19 | query: '', //搜索关键字
20 | type: 1, //搜索类型
21 | hots: [] //热搜
22 | }
23 | searchHot().then(res => {
24 | if (res.data.code === HTTP_OK) {
25 | this.setState({
26 | hots: res.data.result.hots
27 | })
28 | }
29 | })
30 | }
31 |
32 | searchChange = e => {
33 | this.setState({
34 | value: e.target.value
35 | })
36 | }
37 |
38 | onEnter = e => {
39 | if (e.keyCode === 13) {
40 | this.setState({
41 | query: e.target.value
42 | })
43 | }
44 | }
45 |
46 | render() {
47 | const { value, query, hots } = this.state
48 | const { history } = this.props
49 | return (
50 |
51 |
65 |
66 |
67 |
68 |
热门搜索
69 |
70 | {hots.length > 0 &&
71 | hots.map((itme, index) => (
72 | - {
75 | this.setState({ query: itme.first, value: itme.first })
76 | }}
77 | key={index}
78 | >
79 | {itme.first}
80 |
81 | ))}
82 |
83 |
84 |
85 |
86 | )
87 | }
88 | }
89 |
90 | export default withRouter(Search)
91 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 |
10 |
14 |
15 |
16 |
37 |
46 | 茂茂听音乐
47 | <% if ( process.env.NODE_ENV === 'production' ){ %>
48 |
57 | <% } %>
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
75 |
76 |
77 |
--------------------------------------------------------------------------------
/src/base/loading/loading.scss:
--------------------------------------------------------------------------------
1 | @import '~@/styles/var.scss';
2 |
3 | .loading-box {
4 | position: relative;
5 | width: 100%;
6 | padding: 80px 0;
7 | overflow: hidden;
8 | text-align: center;
9 | .loading {
10 | position: relative;
11 | display: inline-block;
12 | height: 50px;
13 | padding-left: 60px;
14 | line-height: 50px;
15 | font-size: 36px;
16 | color: $loading-color;
17 | &:before {
18 | content: '';
19 | position: absolute;
20 | top: 0;
21 | left: 0;
22 | width: 45px;
23 | height: 45px;
24 | background: url('data:image/gif;base64,R0lGODlhKAAoAIABANM6Mf///yH/C05FVFNDQVBFMi4wAwEAAAAh/wtYTVAgRGF0YVhNUDw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMDY3IDc5LjE1Nzc0NywgMjAxNS8wMy8zMC0yMzo0MDo0MiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKFdpbmRvd3MpIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOkI2QURFODRFRkZBRTExRTU4NTg0QTdFMUQ4QkI2MTI1IiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOkI2QURFODRGRkZBRTExRTU4NTg0QTdFMUQ4QkI2MTI1Ij4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6QjZBREU4NENGRkFFMTFFNTg1ODRBN0UxRDhCQjYxMjUiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6QjZBREU4NERGRkFFMTFFNTg1ODRBN0UxRDhCQjYxMjUiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz4B//79/Pv6+fj39vX08/Lx8O/u7ezr6uno5+bl5OPi4eDf3t3c29rZ2NfW1dTT0tHQz87NzMvKycjHxsXEw8LBwL++vby7urm4t7a1tLOysbCvrq2sq6qpqKempaSjoqGgn56dnJuamZiXlpWUk5KRkI+OjYyLiomIh4aFhIOCgYB/fn18e3p5eHd2dXRzcnFwb25tbGtqaWhnZmVkY2JhYF9eXVxbWllYV1ZVVFNSUVBPTk1MS0pJSEdGRURDQkFAPz49PDs6OTg3NjU0MzIxMC8uLSwrKikoJyYlJCMiISAfHh0cGxoZGBcWFRQTEhEQDw4NDAsKCQgHBgUEAwIBAAAh+QQJCgABACwAAAAAKAAoAAACeIyPqcvtD6OctNoD8rUZ7Np9VChKZAmdqKOuTOsqcIzMtGHfuaxxfbRrBGu/UfExXCRxxwRsGdg9m0IqpgmVYq1KbnTrMXmnYeAYzCtf1em2E11lf+VkFpxIP+f37td93dfF9zboVwhIaHfItJjoiBd4IzlJWalQAAAh+QQJCgABACwAAAAAKAAoAAACfIyPqcvtD6OctNqLs94WeB55AAiJ5GOeTaoubJu8nBzQGm0zuYF/0l7zIYAxocvIQzqAvVHROVRGoYemgqm0PpfZLjX53YqnV2nVWw5j1ejxkQ1Pc+Nu8FxeD4bJea2uzRf4hidotwJ4RvenmEg42IfoaFioB2N5iZmZUAAAIfkECQoAAQAsAAAAACgAKAAAAn+Mj6nL7Q+jnLRaCPK1GezafVTIaRIJmhE6qpg7sY98wg692g3+egnNm9mAup6C6EslD8hL8zcsGp4I6sI6jS6PWu42EAR3od8wVlyWor1s8m1chV/l2fQ3bm/j33m3n3F2tvfHREdYp1d4p5iIePgYqBbZB2goKIKZqbnJ+VAAACH5BAUKAAEALAAAAAAoACgAAAJ8jI+py+0PIwRUWkbB3Sjz731bKFpkGZ1mJaktm8KX29CT/Ng57ug9XwPeNC/iSLjwBY1DEFKhxDwTLl/0UH1eDVkmlNf9eqng8thqFquX606aTT6/pfJ6OznH5u/cfQBtF8cnSOgWSDcYBmeYqOWniFiod4hSaXmJmWlRAAA7')
25 | no-repeat center/45px;
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/base/notification/notification.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import ReactDOM from 'react-dom'
3 | import PropTypes from 'prop-types'
4 | import classNames from 'classnames'
5 |
6 | import Notice from './notice'
7 |
8 | let seed = 0
9 | const now = Date.now()
10 |
11 | function getUuid() {
12 | return `mNotification_${now}_${seed++}`
13 | }
14 |
15 | class Notification extends Component {
16 | static propTypes = {
17 | prefixCls: PropTypes.string,
18 | className: PropTypes.string
19 | }
20 |
21 | static defaultProps = {
22 | prefixCls: 'mm-notification'
23 | }
24 |
25 | constructor(props) {
26 | super(props)
27 | this.state = {
28 | notices: [], // 存储当前有的notices
29 | hasMask: true // 是否显示蒙版
30 | }
31 | }
32 |
33 | // 添加 notice
34 | add = notice => {
35 | const { notices } = this.state
36 | const key = (notice.key = notice.key || getUuid()) //生成唯一key
37 | const temp = notices.filter(item => item.key === key).length
38 | if (!temp) {
39 | this.setState(prevState => {
40 | return {
41 | notices: prevState.notices.concat(notice)
42 | }
43 | })
44 | }
45 | }
46 |
47 | // 根据 key 移除 notice
48 | remove = key => {
49 | this.setState(prevState => {
50 | return {
51 | notices: prevState.notices.filter(notice => notice.key !== key)
52 | }
53 | })
54 | }
55 |
56 | render() {
57 | const { notices } = this.state
58 | const props = this.props
59 | const noticeNodes = notices.map(notice => {
60 | const closeFun = () => {
61 | this.remove(notice.key)
62 | // 如果有用户传入的onClose 执行
63 | if (notice.onClose) notice.onClose()
64 | }
65 | // const onClose = createChainedFunction(this.remove.bind(this, notice.key), notice.onClose)
66 | return (
67 |
68 | {notice.content}
69 |
70 | )
71 | })
72 | return (
73 |
74 | {noticeNodes}
75 |
76 | )
77 | }
78 | }
79 |
80 | // 动态添加到页面中和重写
81 | Notification.newInstance = function newNotificationInstance(
82 | properties,
83 | callback
84 | ) {
85 | const { ...props } = properties || {}
86 | let div
87 | if (!div) {
88 | div = document.createElement('div')
89 | document.body.appendChild(div)
90 | }
91 | let called = false
92 | function ref(notification) {
93 | if (called) {
94 | return
95 | }
96 | called = true
97 | callback({
98 | notice(noticeProps) {
99 | notification.add(noticeProps)
100 | },
101 | removeNotice(key) {
102 | notification.remove(key)
103 | },
104 | component: notification,
105 | destroy() {
106 | ReactDOM.unmountComponentAtNode(div)
107 | document.body.removeChild(div)
108 | }
109 | })
110 | }
111 | ReactDOM.render(, div)
112 | }
113 |
114 | export default Notification
115 |
--------------------------------------------------------------------------------
/src/pages/discover/discover.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { Link } from 'react-router-dom'
3 |
4 | import Slide from '@/base/slide/silde'
5 | import Loading from '@/base/loading/loading'
6 | import Scroll from '@/base/scroll/scroll'
7 | import ColumnList from '@/base/columnList/columnList'
8 |
9 | import { getBanner, getPersonalized } from '@/api'
10 | import { HTTP_OK } from '@/config'
11 | import { formatPlayListMin } from '@/model/playlist'
12 |
13 | import './discover.scss'
14 |
15 | // 推荐页面
16 |
17 | class Discover extends Component {
18 | constructor(props) {
19 | super(props)
20 | this.state = {
21 | banners: [], // banner数组
22 | // getDate: new Date().getDate(),// 当前日期
23 | personalized: [] // 推荐歌单
24 | }
25 | }
26 |
27 | componentDidMount() {
28 | // alert(window.navigator.userAgent)
29 | getBanner().then(res => {
30 | if (res.data.code === HTTP_OK) {
31 | this.setState({
32 | banners: res.data.banners
33 | })
34 | }
35 | })
36 | getPersonalized().then(res => {
37 | if (res.data.code === HTTP_OK) {
38 | this.setState({
39 | personalized: formatPlayListMin(res.data.result)
40 | })
41 | }
42 | })
43 | }
44 |
45 | render() {
46 | const { banners, personalized } = this.state
47 | return (
48 |
49 | {personalized.length > 0 && banners.length > 0 ? (
50 |
51 | {this.state.banners && (
52 |
53 |
54 |
55 | )}
56 |
57 | {/*
*/}
58 | {/*
*/}
59 | {/*
私人FM
*/}
60 | {/*
*/}
61 | {/*
*/}
62 | {/*
*/}
63 | {/*
每日推荐
*/}
64 | {/*
*/}
65 |
66 |
67 |
歌单
68 |
69 |
70 |
71 |
排行榜
72 |
73 |
74 |
75 |
{
78 | this.props.history.push('/sheetlist')
79 | }}
80 | >
81 | 推荐歌单
82 |
83 | this.props.history.push(`/playlist/${id}`)}
86 | />
87 |
88 |
89 | ) : (
90 |
91 | )}
92 |
93 | )
94 | }
95 | }
96 |
97 | export default Discover
98 |
--------------------------------------------------------------------------------
/src/model/playlist.js:
--------------------------------------------------------------------------------
1 | import formatSongs from './song'
2 |
3 | /**
4 | * 歌单类模型
5 | */
6 | //歌单列表
7 | export class PlayList {
8 | constructor({
9 | id,
10 | name,
11 | coverImgUrl,
12 | userId,
13 | nickname,
14 | trackCount,
15 | playCount
16 | }) {
17 | this.id = id //歌单ID
18 | this.name = name //歌单名称
19 | this.coverImgUrl = coverImgUrl //歌单封面
20 | this.userId = userId //歌单创建者ID
21 | this.nickname = nickname //歌单创建者昵称
22 | this.trackCount = trackCount //歌曲数量
23 | this.playCount = playCount //播放数
24 | }
25 | }
26 |
27 | export const createPlayList = function(playlist) {
28 | return new PlayList({
29 | id: playlist.id,
30 | name: playlist.name,
31 | coverImgUrl: playlist.coverImgUrl,
32 | userId: playlist.creator.userId,
33 | nickname: playlist.creator.nickname,
34 | trackCount: playlist.trackCount,
35 | playCount: playlist.playCount
36 | })
37 | }
38 |
39 | // 歌单数据格式化
40 | const formatPlayList = function(list) {
41 | let PlayList = []
42 | list.forEach(item => {
43 | if (item.id) {
44 | PlayList.push(createPlayList(item))
45 | }
46 | })
47 | return PlayList
48 | }
49 |
50 | export default formatPlayList
51 |
52 | //歌单列表Min
53 | export class PlayListMin {
54 | constructor({ id, name, coverImgUrl, trackCount, playCount }) {
55 | this.id = id //歌单ID
56 | this.name = name //歌单名称
57 | this.coverImgUrl = coverImgUrl //歌单封面
58 | this.trackCount = trackCount //歌曲数量
59 | this.playCount = playCount //播放数
60 | }
61 | }
62 |
63 | export const createPlayListMin = function(playlist) {
64 | return new PlayListMin({
65 | id: playlist.id,
66 | name: playlist.name,
67 | coverImgUrl: playlist.picUrl,
68 | trackCount: playlist.trackCount,
69 | playCount: playlist.playCount
70 | })
71 | }
72 |
73 | export const formatPlayListMin = function(list) {
74 | let PlayListMin = []
75 | list.forEach(item => {
76 | if (item.id) {
77 | PlayListMin.push(createPlayListMin(item))
78 | }
79 | })
80 | return PlayListMin
81 | }
82 |
83 | //歌单详情
84 | export class PlayListDetail {
85 | constructor({
86 | id,
87 | name,
88 | coverImgUrl,
89 | userId,
90 | avatarUrl,
91 | nickname,
92 | createTime,
93 | trackCount,
94 | playCount,
95 | shareCount,
96 | commentCount,
97 | tracks
98 | }) {
99 | this.id = id //歌单ID
100 | this.name = name //歌单名称
101 | this.coverImgUrl = coverImgUrl //歌单封面
102 | this.userId = userId //歌单创建者ID
103 | this.avatarUrl = avatarUrl //歌单创建者头像
104 | this.nickname = nickname //歌单创建者昵称
105 | this.createTime = createTime //歌单创建时间
106 | this.trackCount = trackCount //歌曲数量
107 | this.playCount = playCount //播放数
108 | this.shareCount = shareCount //分享数
109 | this.commentCount = commentCount //评论数
110 | this.tracks = tracks //歌曲列表
111 | }
112 | }
113 |
114 | export const createPlayListDetail = function(playlist) {
115 | const tracks = formatSongs(playlist.tracks)
116 | return new PlayListDetail({
117 | id: playlist.id,
118 | name: playlist.name,
119 | coverImgUrl: playlist.coverImgUrl,
120 | userId: playlist.creator.userId,
121 | avatarUrl: playlist.creator.avatarUrl,
122 | nickname: playlist.creator.nickname,
123 | createTime: playlist.createTime,
124 | trackCount: playlist.trackCount,
125 | playCount: playlist.playCount,
126 | shareCount: playlist.shareCount,
127 | commentCount: playlist.commentCount,
128 | tracks
129 | })
130 | }
131 |
--------------------------------------------------------------------------------
/src/base/scroll/scroll.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import ReactDOM from 'react-dom'
3 | import PropTypes from 'prop-types'
4 | import BScroll from 'better-scroll'
5 |
6 | import './scroll.css'
7 |
8 | const DEFAULT_OPTIONS = {
9 | observeDOM: true,
10 | click: true,
11 | probeType: 1,
12 | scrollbar: false,
13 | pullDownRefresh: false,
14 | pullUpLoad: false
15 | }
16 |
17 | class Scroll extends Component {
18 | static propTypes = {
19 | className: PropTypes.string,
20 | options: PropTypes.object,
21 | refreshDelay: PropTypes.number,
22 | pullUpLoad: PropTypes.func
23 | }
24 |
25 | static defaultProps = {
26 | options: {},
27 | refreshDelay: 20
28 | }
29 |
30 | constructor(props) {
31 | super(props)
32 | this.state = {
33 | isPullingDown: false, // 是否锁定下拉事件
34 | isPullUpLoad: false // 是否锁定上拉事件
35 | }
36 | }
37 |
38 | componentDidMount() {
39 | this.initScroll()
40 | }
41 |
42 | shouldComponentUpdate(newProps, newState) {
43 | // console.log("newProps", newProps.children && newProps.children[0].props.list.length);
44 | // console.log("this", this.props.children && this.props.children[0].props.list.length);
45 | if (this.scroll.options.pullDownRefresh || this.scroll.options.pullUpLoad) {
46 | if (newProps.children[0].props.list.length > 0) {
47 | let newList = newProps.children[0].props.list,
48 | List = this.props.children[0].props.list
49 | if (newList.length !== List.length) {
50 | this.refresh()
51 | }
52 | }
53 | }
54 | return true
55 | }
56 |
57 | componentWillUnmount() {
58 | this.scroll.destroy()
59 | clearTimeout(this.refreshTimer)
60 | }
61 |
62 | // 初始化
63 | initScroll() {
64 | this.scrollWrapper = ReactDOM.findDOMNode(this.refs.scrollWrapper)
65 | if (!this.scroll) {
66 | let options = Object.assign({}, DEFAULT_OPTIONS, this.props.options)
67 | this.scroll = new BScroll(this.scrollWrapper, options)
68 | }
69 | if (this.props.options.pullDownRefresh) {
70 | this.scroll.on('pullingDown', this.onPullingDown)
71 | }
72 | if (this.props.options.pullUpLoad) {
73 | this.scroll.on('pullingUp', this.onPullingUp)
74 | }
75 | }
76 |
77 | // 重新计算
78 | refresh() {
79 | clearTimeout(this.refreshTimer)
80 | this.refreshTimer = setTimeout(() => {
81 | this.forceUpdate(true)
82 | }, this.props.refreshDelay)
83 | }
84 |
85 | // 下拉刷新
86 | onPullingDown = () => {
87 | // this.setState({
88 | // isPullingDown: true
89 | // });
90 | // this.props.pullDownRefresh()
91 | }
92 |
93 | // 上拉加载
94 | onPullingUp = () => {
95 | this.setState({
96 | isPullUpLoad: true
97 | })
98 | this.props.pullUpLoad()
99 | }
100 |
101 | // 数据更新
102 | forceUpdate(dirty = false) {
103 | if (this.props.options.pullDownRefresh && this.state.isPullingDown) {
104 | this.setState({
105 | isPullingDown: false
106 | })
107 | } else if (this.props.options.pullUpLoad && this.state.isPullUpLoad) {
108 | this.setState({
109 | isPullUpLoad: false
110 | })
111 | this.scroll.finishPullUp()
112 | dirty && this.scroll.refresh()
113 | } else {
114 | dirty && this.scroll.refresh()
115 | }
116 | }
117 |
118 | render() {
119 | const { className = '' } = this.props
120 | return (
121 |
122 | {/*获取子组件*/}
123 |
{this.props.children}
124 |
125 | )
126 | }
127 | }
128 |
129 | export default Scroll
130 |
--------------------------------------------------------------------------------
/src/pages/discover/discover.scss:
--------------------------------------------------------------------------------
1 | @import '~@/styles/var.scss';
2 | @import '~@/styles/mixin.scss';
3 | @import '~@/styles/playCount.scss';
4 |
5 | .discover {
6 | height: 100%;
7 | overflow: hidden;
8 | & .banner {
9 | position: relative;
10 | height: 390px;
11 | padding-top: 18px;
12 | &:before {
13 | content: '';
14 | position: absolute;
15 | top: 0;
16 | right: 0;
17 | bottom: 50px;
18 | left: 0;
19 | background-color: $theme-bg;
20 | }
21 | }
22 | & .menu {
23 | display: flex;
24 | justify-content: space-around;
25 | border-bottom: 1px solid #dbdcde;
26 | &-item {
27 | flex: 0 0 25%;
28 | padding: 45px 0;
29 | & p {
30 | text-align: center;
31 | font-size: $font-size-small-x;
32 | color: #2e302f;
33 | }
34 | }
35 | &-icon {
36 | position: relative;
37 | width: 50%;
38 | height: 0;
39 | margin: 0 25% 25px;
40 | padding-top: 50%;
41 | &:before {
42 | content: '';
43 | position: absolute;
44 | top: 0;
45 | left: 0;
46 | width: 100%;
47 | height: 100%;
48 | @include bg-full;
49 | }
50 | &.fm:before {
51 | @include bg-url('~@/assets/images/discover/icn_fm.png');
52 | }
53 |
54 | &.fm:active:before {
55 | @include bg-url('~@/assets/images/discover/icn_fm_prs.png');
56 | }
57 | &.daily {
58 | &:before {
59 | @include bg-url('~@/assets/images/discover/icn_daily.png');
60 | }
61 | &:after {
62 | content: attr(data-date);
63 | position: absolute;
64 | top: 60%;
65 | left: 50%;
66 | font-size: $font-size-small;
67 | color: $theme-color;
68 | transform: translate(-50%, -50%);
69 | }
70 | }
71 | &.daily:active {
72 | &:before {
73 | @include bg-url('~@/assets/images/discover/icn_daily_prs.png');
74 | }
75 | &:after {
76 | color: $theme-sub-color;
77 | }
78 | }
79 | &.playlist:before {
80 | @include bg-url('~@/assets/images/discover/icn_playlist.png');
81 | }
82 |
83 | &.playlist:active:before {
84 | @include bg-url('~@/assets/images/discover/icn_playlist_prs.png');
85 | }
86 | &.rank:before {
87 | @include bg-url('~@/assets/images/discover/icn_rank.png');
88 | }
89 |
90 | &.rank:active:before {
91 | @include bg-url('~@/assets/images/discover/icn_rank_prs.png');
92 | }
93 | }
94 | }
95 |
96 | & .lcrlist {
97 | width: 100%;
98 | &-hd {
99 | height: 160px;
100 | padding-left: 18px;
101 | line-height: 160px;
102 | font-size: $font-size-large;
103 | font-weight: 700;
104 | span {
105 | padding-right: 50px;
106 | @include bg-url('~@/assets/images/aa7.png');
107 | @include bg-full($s: 28px, $p: right center);
108 | }
109 | }
110 | &-bd {
111 | display: flex;
112 | flex-flow: wrap;
113 | padding: 0 13px;
114 | }
115 | &-item {
116 | position: relative;
117 | flex: 0 0 33.333333%;
118 | .item-img {
119 | width: calc(100% - 10px);
120 | height: 0;
121 | padding-top: calc(100% - 10px);
122 | margin: 0 5px;
123 | overflow: hidden;
124 | @include playCount;
125 | img {
126 | position: absolute;
127 | top: 0;
128 | left: 0;
129 | @include img-full;
130 | }
131 | }
132 | .item-title {
133 | height: 100px;
134 | margin: 20px 10px;
135 | line-height: 50px;
136 | color: #333;
137 | font-size: $font-size-small-x;
138 | word-wrap: break-word;
139 | word-break: break-all;
140 | @include no-wrap(2);
141 | }
142 | }
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/src/base/progress/progress.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import ReactDOM from 'react-dom'
3 | import PropTypes from 'prop-types'
4 |
5 | import './progress.scss'
6 |
7 | // 进度条拖动组件
8 |
9 | class Progress extends Component {
10 | static propTypes = {
11 | percent: PropTypes.number.isRequired,
12 | percentProgress: PropTypes.number,
13 | dragStart: PropTypes.func, // 拖拽开始事件
14 | dragMove: PropTypes.func, // 拖拽中事件
15 | dragEnd: PropTypes.func // 拖拽结束事件
16 | }
17 |
18 | constructor(props) {
19 | super(props)
20 | this.state = {
21 | offsetWidth: 0,
22 | status: false, // 是否可拖动
23 | startX: 0, // 记录最开始点击的X坐标
24 | left: 0 // 记录当前已经移动的距离
25 | }
26 | }
27 |
28 | componentDidMount() {
29 | this.mmProgress = ReactDOM.findDOMNode(this.refs.mmProgress)
30 | this.mmProgressInner = ReactDOM.findDOMNode(this.refs.mmProgressInner)
31 | this.bindEvents()
32 | }
33 |
34 | componentWillReceiveProps(nextProps) {
35 | if (!this.state.status && nextProps.percent !== this.props.percent) {
36 | this.setState({
37 | offsetWidth: this.mmProgress.clientWidth * nextProps.percent
38 | })
39 | }
40 | }
41 |
42 | componentWillUnmount() {
43 | this.unbindEvents()
44 | }
45 |
46 | //添加绑定事件
47 | bindEvents() {
48 | document.addEventListener('mousemove', this.barMove)
49 | document.addEventListener('mouseup', this.barUp)
50 |
51 | document.addEventListener('touchmove', this.barMove)
52 | document.addEventListener('touchend', this.barUp)
53 | }
54 |
55 | //移除绑定事件
56 | unbindEvents() {
57 | document.removeEventListener('mousemove', this.barMove)
58 | document.removeEventListener('mouseup', this.barUp)
59 |
60 | document.removeEventListener('touchmove', this.barMove)
61 | document.removeEventListener('touchend', this.barUp)
62 | }
63 |
64 | // 点击事件
65 | barClick = e => {
66 | let rect = this.mmProgress.getBoundingClientRect()
67 | let offsetWidth = Math.min(rect.width, Math.max(0, e.clientX - rect.left))
68 | this.setState({ offsetWidth })
69 | if (this.props.dragEnd) {
70 | this.props.dragEnd(offsetWidth / this.mmProgress.clientWidth)
71 | }
72 | }
73 |
74 | // 鼠标/触摸开始事件
75 | barDown = e => {
76 | this.setState({
77 | status: true,
78 | startX: e.clientX || e.touches[0].pageX,
79 | left: this.mmProgressInner.clientWidth
80 | })
81 | // console.log(e.nativeEvent)
82 | // console.log(this)
83 | }
84 | //鼠标/触摸移动事件
85 | barMove = e => {
86 | if (this.state.status) {
87 | let endX = e.clientX || e.touches[0].pageX,
88 | dist = endX - this.state.startX
89 | let offsetWidth = Math.min(
90 | this.mmProgress.clientWidth,
91 | Math.max(0, this.state.left + dist)
92 | )
93 | this.setState({ offsetWidth })
94 | // console.log(offsetWidth)
95 | }
96 | }
97 |
98 | //鼠标/触摸释放事件
99 | barUp = () => {
100 | // 避免打开Playing组件时触发
101 | if (this.state.status) {
102 | this.setState({
103 | status: false
104 | })
105 | if (this.props.dragEnd) {
106 | this.props.dragEnd(this.state.offsetWidth / this.mmProgress.clientWidth)
107 | }
108 | }
109 | }
110 |
111 | render() {
112 | const { offsetWidth } = this.state
113 | return (
114 |
129 | )
130 | }
131 | }
132 |
133 | export default Progress
134 |
--------------------------------------------------------------------------------
/src/base/slide/silde.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import ReactDOM from 'react-dom'
3 | import PropTypes from 'prop-types'
4 | import BScroll from 'better-scroll'
5 |
6 | import Dot from './dot/dot'
7 |
8 | import './slide.scss'
9 |
10 | // 轮播组件
11 |
12 | class Slide extends Component {
13 | static propTypes = {
14 | data: PropTypes.array.isRequired,
15 | interval: PropTypes.number,
16 | loop: PropTypes.bool,
17 | threshold: PropTypes.number,
18 | speed: PropTypes.number
19 | }
20 |
21 | static defaultProps = {
22 | interval: 4000, // 轮播间隔
23 | loop: true, // 是否循环
24 | autoPlay: true, // 是否自动切换
25 | threshold: 0.1, // 滚动到下一个的阈值
26 | speed: 400 // 动画速度
27 | }
28 |
29 | constructor(props) {
30 | super(props)
31 | this.state = {
32 | currentPageIndex: 0
33 | }
34 | }
35 |
36 | componentDidMount() {
37 | if (!this.slider) {
38 | this._initWdith()
39 | this._initSlide()
40 | if (this.props.autoPlay) {
41 | this._play()
42 | }
43 | }
44 | }
45 |
46 | componentWillUnmount() {
47 | this.slide && this.slide.destroy() //销毁 better-scroll
48 | }
49 |
50 | //重新计算 better-scroll
51 | refresh() {
52 | if (this.slide === null) {
53 | return false
54 | }
55 | this.slide && this.slide.refresh()
56 | }
57 |
58 | //初始化 better-scroll
59 | _initSlide() {
60 | const slideEle = ReactDOM.findDOMNode(this.refs.sildeWrapper)
61 | this.slide = new BScroll(slideEle, {
62 | scrollX: true,
63 | scrollY: false,
64 | momentum: false,
65 | snap: {
66 | loop: this.props.loop,
67 | threshold: this.props.threshold,
68 | speed: this.props.speed
69 | },
70 | bounce: !this.props.loop,
71 | stopPropagation: true
72 | })
73 |
74 | this.slide.goToPage(this.state.currentPageIndex, 0, 0)
75 |
76 | // 绑定滚动结束事件
77 | this.slide.on('scrollEnd', this._onScrollEnd)
78 |
79 | slideEle.removeEventListener('touchend', this._touchEndEvent, false)
80 | this._touchEndEvent = () => {
81 | if (this.props.autoPlay) {
82 | this._play()
83 | }
84 | }
85 | slideEle.addEventListener('touchend', this._touchEndEvent, false)
86 |
87 | // 绑定滚动开始前事件
88 | this.slide.on('beforeScrollStart', () => {
89 | if (this.props.autoPlay) {
90 | clearTimeout(this.timer)
91 | }
92 | })
93 | }
94 |
95 | //计算宽度
96 | _initWdith() {
97 | let slideWidth = ReactDOM.findDOMNode(this.refs.sildeWrapper).clientWidth
98 | let sildeList = ReactDOM.findDOMNode(this.refs.sildeList)
99 | let width = 0
100 | if (sildeList.children.length) {
101 | for (let i = 0; i < sildeList.children.length; i++) {
102 | let child = sildeList.children[i]
103 | child.style.width = `${slideWidth}px`
104 | width += slideWidth
105 | }
106 | }
107 | if (this.props.loop && sildeList.children.length > 1) {
108 | width += 2 * slideWidth
109 | }
110 | ReactDOM.findDOMNode(this.refs.sildeList).style.width = `${width}px`
111 | }
112 |
113 | // 自动切换
114 | _play = () => {
115 | clearTimeout(this.timer)
116 | this.timer = setTimeout(() => {
117 | this.slide && this.slide.next()
118 | }, this.props.interval)
119 | }
120 |
121 | //滚动结束事件
122 | _onScrollEnd = () => {
123 | let pageIndex = this.slide.getCurrentPage().pageX
124 | this.setState({
125 | currentPageIndex: pageIndex
126 | })
127 | if (this.props.autoPlay) {
128 | this._play()
129 | }
130 | }
131 |
132 | render() {
133 | const { data } = this.props
134 | return (
135 |
136 |
137 | {data &&
138 | data.length > 0 &&
139 | data.map((item, index) => (
140 |
141 |

142 |
143 | ))}
144 |
145 |
146 |
147 | )
148 | }
149 | }
150 |
151 | export default Slide
152 |
--------------------------------------------------------------------------------
/src/pages/playlist/playlist.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { withRouter } from 'react-router-dom'
3 | import { connect } from 'react-redux'
4 |
5 | import Loading from '@/base/loading/loading'
6 | import MmNav from '@/components/mm-nav/mm-nav'
7 | import BaseSongList from '@/base/songlist/songlist'
8 | import Scroll from '@/base/scroll/scroll'
9 |
10 | import { setAllPlay } from '@/store/actions'
11 | import { getPlaylistDetail, getMusicDetail } from '@/api'
12 | import { HTTP_OK } from '@/config'
13 | import { formatPlayCount } from '@/utils/utils'
14 | import { createPlayListDetail } from '@/model/playlist'
15 |
16 | import './playlist.scss'
17 |
18 | const defaultName = '歌单'
19 |
20 | class PlayList extends Component {
21 | constructor(props) {
22 | super(props)
23 | this.state = {
24 | data: {}, //歌单数据
25 | loading: true, //加载动画
26 | defaultName //默认歌单名称
27 | }
28 | }
29 |
30 | componentDidMount() {
31 | // 获取歌单详情
32 | // console.log(this.props)
33 | // console.log(this.props.location.query)
34 | getPlaylistDetail(this.props.match.params.id).then(res => {
35 | if (res.data.code === HTTP_OK) {
36 | const ids = res.data.playlist.trackIds.map((v) => v.id).toString()
37 | getMusicDetail(ids).then((result) => {
38 | res.data.playlist.tracks = result.data.songs
39 | this.setState({
40 | data: createPlayListDetail(res.data.playlist),
41 | loading: false,
42 | })
43 | })
44 | }
45 | })
46 | }
47 |
48 | onItemClick = (id, index) => {
49 | // console.log(id, index);
50 | // console.log(this.state.data.tracks[index]);
51 | this.props.setAllPlay({
52 | playList: this.state.data.tracks,
53 | currentIndex: index
54 | })
55 | // this.props.setShowPlayer(true);
56 | // this.props.setPlayList(this.state.data.tracks);
57 | // this.props.setCurrentIndex(index);
58 | // this.props.setCurrentMusic(this.state.data.tracks[index]);
59 | }
60 |
61 | render() {
62 | const { currentMusic } = this.props
63 | const { defaultName, loading } = this.state
64 | const {
65 | name,
66 | coverImgUrl,
67 | avatarUrl,
68 | nickname,
69 | playCount,
70 | tracks
71 | } = this.state.data
72 | return (
73 |
74 |
75 | {coverImgUrl && (
76 |
84 | )}
85 | {loading ? (
86 |
87 | ) : (
88 |
89 |
90 |
98 |
99 |
103 |

104 |
105 |
106 |
{name}
107 |
108 |

109 |
{nickname}
110 |
111 |
112 |
113 |
114 | {tracks && tracks.length > 0 && (
115 |
121 | )}
122 |
123 | )}
124 |
125 | )
126 | }
127 | }
128 |
129 | //映射Redux全局的state到组件的props上
130 | const mapStateToProps = state => ({
131 | showPlayer: state.showPlayer,
132 | currentMusic: state.currentMusic
133 | })
134 |
135 | //映射dispatch到props上
136 | const mapDispatchToProps = dispatch => ({
137 | setAllPlay: status => {
138 | dispatch(setAllPlay(status))
139 | }
140 | })
141 | export default connect(
142 | mapStateToProps,
143 | mapDispatchToProps
144 | )(withRouter(PlayList))
145 |
--------------------------------------------------------------------------------
/src/pages/toplist/toplist.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 |
3 | import Loading from '@/base/loading/loading'
4 | import MmNav from '@/components/mm-nav/mm-nav'
5 | import Scroll from '@/base/scroll/scroll'
6 |
7 | import { HTTP_OK } from '@/config'
8 | import { getTopListDetail } from '@/api'
9 |
10 | import './toplist.scss'
11 |
12 | // 排行榜页面
13 |
14 | class TopList extends Component {
15 | constructor(props) {
16 | super(props)
17 | this.state = {
18 | officialList: [],
19 | globalList: [],
20 | artistList: null
21 | }
22 | }
23 |
24 | componentDidMount() {
25 | getTopListDetail().then(res => {
26 | if (res.data.code === HTTP_OK) {
27 | // console.log(res.data.list)
28 | let officialList = [],
29 | globalList = [],
30 | artistList = res.data.artistToplist
31 | res.data.list.forEach(item => {
32 | if (item.ToplistType) {
33 | officialList.push({
34 | id: item.id,
35 | name: item.name,
36 | coverImgUrl: item.coverImgUrl,
37 | description: item.description,
38 | updateFrequency: item.updateFrequency,
39 | tracks: item.tracks,
40 | ToplistType: item.ToplistType
41 | })
42 | } else {
43 | globalList.push({
44 | id: item.id,
45 | name: item.name,
46 | coverImgUrl: item.coverImgUrl,
47 | description: item.description,
48 | updateFrequency: item.updateFrequency
49 | })
50 | }
51 | })
52 | this.setState({
53 | officialList,
54 | globalList,
55 | artistList
56 | })
57 | }
58 | })
59 | }
60 |
61 | render() {
62 | const { officialList, globalList, artistList } = this.state
63 | // console.log(artistList)
64 | return (
65 |
66 |
67 | {officialList.length > 0 ? (
68 |
69 | 官方榜单
70 |
71 | {officialList.map(item => (
72 |
{
75 | this.props.history.push({
76 | pathname: `/playlist/${item.id}`
77 | })
78 | }}
79 | key={item.id}
80 | >
81 |
82 |

83 |
{item.updateFrequency}
84 |
85 |
86 | {item.tracks.map((tracks, index) => (
87 |
{`${tracks.first}-${tracks.second}`}
90 | ))}
91 |
92 |
93 | ))}
94 | {artistList && artistList.name && (
95 |
96 |
97 |

98 |
{artistList.updateFrequency}
99 |
100 |
101 | {artistList.artists.map((item, index) => (
102 |
{`${item.first} ${item.third}`}
105 | ))}
106 |
107 |
108 | )}
109 |
110 | 全球榜
111 |
112 | {globalList.map(item => (
113 |
{
116 | this.props.history.push({
117 | pathname: `/playlist/${item.id}`
118 | })
119 | }}
120 | key={item.id}
121 | >
122 |
123 |

124 |
{item.updateFrequency}
125 |
126 |
{item.name}
127 |
128 | ))}
129 |
130 |
131 | ) : (
132 |
133 | )}
134 |
135 | )
136 | }
137 | }
138 |
139 | export default TopList
140 |
--------------------------------------------------------------------------------
/src/components/search-list/search-list.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { withRouter } from 'react-router-dom'
3 | import classNames from 'classnames'
4 | import { connect } from 'react-redux'
5 |
6 | import BaseSongList from '@/base/songlist/songlist'
7 | import RowList from '@/base/rowList/rowList'
8 | import Loading from '@/base/loading/loading'
9 |
10 | import { addPlay } from '@/store/actions'
11 | import { search, getMusicDetail } from '@/api'
12 | import { HTTP_OK } from '@/config'
13 | import formatSongs from '@/model/song'
14 | import formatPlayList from '@/model/playlist'
15 |
16 | import './search-list.scss'
17 |
18 | // 搜索列表组件
19 |
20 | class SearchList extends Component {
21 | constructor(props) {
22 | super(props)
23 | this.state = {
24 | tabData: [
25 | {
26 | title: '单曲',
27 | type: 1
28 | },
29 | {
30 | title: '歌单',
31 | type: 1000
32 | }
33 | ], //Tab数据
34 | type: 1, //选中的type
35 | songs: [], //搜索的歌曲
36 | playlists: [], //搜索的歌单
37 | loading: true
38 | }
39 | }
40 |
41 | componentWillReceiveProps(nextProps) {
42 | if (nextProps.query !== this.props.query) {
43 | this.setState({ songs: [], playlists: [], loading: true })
44 | this.search(nextProps.query, this.state.type)
45 | }
46 | }
47 |
48 | shouldComponentUpdate(newProps, newState) {
49 | if (newProps.query && newState.type !== this.state.type) {
50 | this.search(newProps.query, newState.type)
51 | }
52 | return true
53 | }
54 |
55 | // 播放单曲
56 | addPlay = (id, index) => {
57 | getMusicDetail(id).then(res => {
58 | if (res.data.code === HTTP_OK) {
59 | let music = this.state.songs[index]
60 | music.image = res.data.songs[0].al.picUrl
61 | this.props.addPlay(music)
62 | }
63 | })
64 | }
65 |
66 | // 跳转歌单
67 | openPlayList = id => {
68 | this.props.history.push({ pathname: `/playlist/${id}` })
69 | }
70 |
71 | //切换Tab
72 | toggleTab = type => {
73 | if (this.state.songs.length === 0 || this.state.playlists.length === 0) {
74 | this.setState({ loading: true, type })
75 | } else {
76 | this.setState({ type })
77 | }
78 | }
79 |
80 | // 搜索事件
81 | search = (query, type) => {
82 | search(query, type).then(res => {
83 | if (res.data.code === HTTP_OK) {
84 | // console.log(res.data.result);
85 | setTimeout(() => {
86 | switch (type) {
87 | case 1:
88 | this.setState({
89 | loading: false,
90 | songs: formatSongs(res.data.result.songs)
91 | })
92 | return
93 | case 1000:
94 | this.setState({
95 | loading: false,
96 | playlists: formatPlayList(res.data.result.playlists)
97 | })
98 | return
99 | default:
100 | }
101 | }, 300)
102 | }
103 | })
104 | }
105 |
106 | render() {
107 | const { currentMusic } = this.props
108 | const { tabData, type, songs, playlists, loading } = this.state
109 | const { query } = this.props
110 | return (
111 |
112 |
113 | {tabData.map(item => (
114 | - this.toggleTab(item.type)}
119 | key={item.type}
120 | >
121 | {item.title}
122 |
123 | ))}
124 |
125 |
126 |
131 | {loading ? (
132 |
133 | ) : (
134 | songs.length > 0 && (
135 |
140 | )
141 | )}
142 |
143 |
148 | {loading ? (
149 |
150 | ) : (
151 | playlists.length > 0 && (
152 |
153 | )
154 | )}
155 |
156 |
157 |
158 | )
159 | }
160 | }
161 |
162 | //映射Redux全局的state到组件的props上
163 | const mapStateToProps = state => ({
164 | currentMusic: state.currentMusic,
165 | playList: state.playList
166 | })
167 | //映射dispatch到props上
168 | const mapDispatchToProps = dispatch => ({
169 | addPlay: status => {
170 | dispatch(addPlay(status))
171 | }
172 | })
173 |
174 | export default connect(
175 | mapStateToProps,
176 | mapDispatchToProps
177 | )(withRouter(SearchList))
178 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React-Music(2018.12.27)
2 |
3 | 高仿网易云音乐安卓客户端
4 |
5 | > API:一个开源的[网易云音乐 NodeJS 版 API](https://binaryify.github.io/NeteaseCloudMusicApi)(有 api 才有动力写!!!)
6 |
7 | > [在线演示地址](https://reactmusic.fe-mm.com)
8 |
9 | > [Vue PC/移动端二合一版本](https://github.com/maomao1996/Vue-mmPlayer)
10 |
11 | > [交流 QQ 群:529940193](http://shang.qq.com/wpa/qunwpa?idkey=f8be1b627a89108ccfda9308720d2a4d0eb3306f253c5d3e8d58452e20b91129)
12 |
13 | ## 如何安装与使用
14 |
15 | > react-music
16 |
17 | ```sh
18 | # 下载 react-music
19 | git clone https://github.com/maomao1996/react-music.git
20 |
21 | # 进入 react-music 项目目录
22 | cd react-music
23 |
24 | # 安装依赖
25 | npm install
26 |
27 | # // 运行 react-music 访问 http://localhost:8163
28 | npm run start
29 |
30 | # // 项目编译打包
31 | npm run build
32 | ```
33 |
34 | > 后台服务器
35 |
36 | [网易云音乐 NodeJS 版 API](https://binaryify.github.io/NeteaseCloudMusicApi)
37 |
38 | ```
39 | # 下载 NeteaseCloudMusicApi
40 | git clone https://github.com/Binaryify/NeteaseCloudMusicApi.git
41 |
42 | # 安装依赖
43 | npm install
44 |
45 | # 服务端运行 访问 http://localhost:3000
46 | node app.js
47 | ```
48 |
49 | #### 运行 react-music 后无法获取音乐请检查后台服务器是否启动
50 |
51 | #### .env 的 REACT_APP_BASE_API_URL 地址要和后台服务器地址一致
52 |
53 | ## 技术栈
54 |
55 | - React(核心框架)
56 | - React-Router(页面路由)
57 | - Redux(状态管理)
58 | - React-Redux
59 | - Redux-Thunk
60 | - ES 6 / 7(JavaScript 语言的下一代标准)
61 | - Sass(CSS 预处理器)
62 | - Axios(网络请求)
63 | - ClassNames(处理动态 class )
64 | - [Better-Scroll](https://ustbhuangyi.github.io/better-scroll/#/zh)(一款重点解决移动端各种滚动场景需求的插件)
65 | - FastClick(解决移动端 300ms 点击延迟)
66 |
67 | ## 项目布局
68 |
69 |
70 | 展开查看
71 | .
72 | ├── config // webpack 配置文件
73 | ├── public // 项目启动页面
74 | ├── scripts // 脚本工具
75 | ├── screenshots // 项目截图
76 | ├── src // 项目源码目录
77 | │ ├── api // 数据交互
78 | │ │ └── index.js
79 | │ ├── assets // 静态资源目录
80 | │ │ └── images // 图片目录
81 | │ ├── base // 公共基础组件目录
82 | │ │ ├── columnList // 歌单基础列表组件 —— 列
83 | │ │ ├── drawer // 抽屉组件
84 | │ │ ├── loading // loading 组件
85 | │ │ ├── notification // 通知组件(Toast)
86 | │ │ ├── progress // 进度条拖动组件
87 | │ │ ├── rowList // 歌单列表基础组件 —— 行
88 | │ │ ├── scroll // 滚动组件
89 | │ │ ├── slide // slide 组件
90 | │ │ ├── songlist // 歌曲列表基础组件
91 | │ │ └── toast // Toast 组件
92 | │ ├── components // 公共项目组件目录
93 | │ │ ├── menu // 菜单组件
94 | │ │ ├── mm-header // 一级导航组件
95 | │ │ ├── mm-nav // 二级导航组件
96 | │ │ ├── player // 播放组件
97 | │ │ └── search-list // 搜索列表详情组件
98 | │ ├── model // 数据模型目录
99 | │ ├── pages // 项目主页面目录
100 | │ │ ├── discover // 发现页面
101 | │ │ ├── playlist // 歌单详情页面
102 | │ │ ├── search // 搜索
103 | │ │ ├── sheetlist // 歌单页面
104 | │ │ ├── skin // 皮肤切换页面
105 | │ │ ├── toplist // 排行榜页面
106 | │ │ └── App.js // 根组件
107 | │ ├── store // redux 目录
108 | │ │ ├── actions.js // 配置 actions 方法
109 | │ │ ├── actionTypes.js // 配置 actions 常量
110 | │ │ ├── index.js // 引用 redux
111 | │ │ └── reducers.js // 处理数据
112 | │ ├── styles // 样式表目录
113 | │ │ ├── index.scss // 基础样式
114 | │ │ ├── mixin.scss // 基础样式宏
115 | │ │ ├── playCount.scss // 播放数量样式宏
116 | │ │ ├── reset.css // 基础重置
117 | │ │ └── var.scss // 基本变量
118 | │ ├── utils // 公共 Js 目录
119 | │ │ └── utils.js // 公用 Js 方法
120 | │ ├── config.js // 基础配置
121 | │ └── index.js // 入口主文件
122 |
123 |
124 |
125 | ## 功能
126 |
127 | - 播放器
128 | - 正在播放
129 | - 排行榜
130 | - 歌单列表
131 | - 歌单详情
132 | - 搜索(歌曲、歌单)
133 | - 皮肤切换
134 |
135 | ## 更新说明
136 |
137 | ### V1.1.1(2018.12.27)
138 |
139 | - 修复 Banner 图片不显示问题
140 | - 修复歌单详情打开失败问题
141 |
142 | ### V1.1.0(2018.07.24)
143 |
144 | - 制作皮肤切换功能
145 | - 增加 Toast 弹出层
146 | - 优化 Scroll 组件逻辑
147 | - 优化抽屉组件样式
148 |
149 | ### V1.0.0(2018.06.12)发布正式版
150 |
151 | - 制作播放器功能
152 | - 制作正在播放列表功能
153 | - 制作排行榜功能
154 | - 制作歌单列表功能
155 | - 制作歌单详情功能
156 | - 制作搜索功能(歌曲、歌单)
157 |
158 | ## License
159 |
160 | [MIT](https://github.com/maomao1996/react-music/blob/master/LICENSE)
161 |
--------------------------------------------------------------------------------
/src/components/player/player.scss:
--------------------------------------------------------------------------------
1 | @import '~@/styles/var.scss';
2 | @import '~@/styles/mixin.scss';
3 |
4 | .player {
5 | & .player-full {
6 | position: fixed;
7 | top: 0;
8 | right: 0;
9 | bottom: 0;
10 | left: 0;
11 | z-index: 1966;
12 | display: none;
13 | background: #000;
14 | &.player-full-enter {
15 | transform: translate3d(0, 50%, 0);
16 | opacity: 0;
17 | }
18 | &.player-full-enter-active {
19 | opacity: 1;
20 | transition: all 0.15s ease-out;
21 | transform: translate3d(0, 0, 0);
22 | }
23 | &.player-full-exit {
24 | transform: translate3d(0, 0, 0);
25 | opacity: 1;
26 | }
27 | &.player-full-exit-active {
28 | transition: all 0.15s ease-out;
29 | transform: translate3d(0, 50%, 0);
30 | opacity: 0;
31 | }
32 | .player-bg {
33 | width: 100%;
34 | height: 100%;
35 | filter: blur(30px);
36 | transform: scale(1.5);
37 | @include bg-full;
38 | &:after {
39 | content: ' ';
40 | position: absolute;
41 | left: 0;
42 | top: 0;
43 | right: 0;
44 | bottom: 0;
45 | z-index: 1;
46 | background-color: rgba(0, 0, 0, 0.25);
47 | }
48 | }
49 | .header {
50 | position: absolute;
51 | top: 0;
52 | right: 0;
53 | left: 0;
54 | z-index: 1;
55 | height: 120px;
56 | padding: 20px 170px;
57 | border-bottom: 1px solid rgba(255, 255, 255, 0.15);
58 | .header-back {
59 | position: absolute;
60 | top: 44px;
61 | left: 44px;
62 | display: block;
63 | width: 72px;
64 | height: 72px;
65 | @include bg-url('~@/assets/images/oq.png');
66 | @include bg-full;
67 | }
68 | h1 {
69 | height: 60px;
70 | line-height: 70px;
71 | font-size: 46px;
72 | color: #fff;
73 | @include no-wrap;
74 | }
75 | h2 {
76 | height: 60px;
77 | line-height: 60px;
78 | font-size: 36px;
79 | color: rgba(255, 255, 255, 0.4);
80 | @include no-wrap;
81 | }
82 | }
83 | .middle {
84 | position: absolute;
85 | top: 160px;
86 | right: 0;
87 | left: 0;
88 | bottom: 360px;
89 | }
90 | .footer {
91 | position: absolute;
92 | right: 0;
93 | left: 0;
94 | bottom: 20px;
95 | height: 320px;
96 | .progress-wrapper {
97 | display: flex;
98 | height: 20px;
99 | padding: 8px 50px 0;
100 | .progress-time {
101 | width: 120px;
102 | line-height: 20px;
103 | font-size: $font-size-small-ss;
104 | color: #fff;
105 | &.progress-time-l {
106 | text-align: left;
107 | }
108 | &.progress-time-r {
109 | text-align: right;
110 | }
111 | }
112 | }
113 | .btn-wrapper {
114 | display: flex;
115 | justify-content: space-around;
116 | align-items: center;
117 | height: 252px;
118 | .btn {
119 | @include bg-full;
120 | &.btn-mode {
121 | width: 162px;
122 | height: 162px;
123 | margin-right: 30px;
124 | &.mode-list {
125 | @include bg-url('~@/assets/images/player/mode-list.png');
126 | }
127 | &.mode-random {
128 | @include bg-url('~@/assets/images/player/mode-random.png');
129 | }
130 | &.mode-single {
131 | @include bg-url('~@/assets/images/player/mode-single.png');
132 | }
133 | }
134 | &.btn-prev {
135 | width: 214px;
136 | height: 214px;
137 | @include bg-url('~@/assets/images/player/prev.png');
138 | }
139 | &.btn-play {
140 | width: 252px;
141 | height: 252px;
142 | @include bg-url('~@/assets/images/player/play.png');
143 | &.btn-pause {
144 | @include bg-url('~@/assets/images/player/pause.png');
145 | }
146 | }
147 | &.btn-next {
148 | width: 214px;
149 | height: 214px;
150 | @include bg-url('~@/assets/images/player/next.png');
151 | }
152 | &.btn-list {
153 | width: 162px;
154 | height: 162px;
155 | margin-left: 30px;
156 | @include bg-url('~@/assets/images/player/list.png');
157 | }
158 | }
159 | }
160 | }
161 | }
162 | & .player-min {
163 | display: flex;
164 | align-items: center;
165 | padding: 20px;
166 | background: #fff;
167 | &-img {
168 | width: 110px;
169 | height: 110px;
170 | margin-right: 20px;
171 | border-radius: 10px;
172 | overflow: hidden;
173 | }
174 | &-info {
175 | min-width: 0;
176 | flex: 1;
177 | font-size: $font-size-small;
178 | color: #888;
179 | h2,
180 | p {
181 | @include no-wrap;
182 | }
183 | h2 {
184 | height: 60px;
185 | line-height: 60px;
186 | font-size: $font-size-medium-x;
187 | color: #333;
188 | }
189 | p {
190 | height: 50px;
191 | line-height: 50px;
192 | }
193 | }
194 | &-play {
195 | width: 90px;
196 | height: 90px;
197 | margin-right: 50px;
198 | @include bg-full;
199 | @include bg-url('~@/assets/images/player/min-play.png');
200 | &.pause {
201 | @include bg-url('~@/assets/images/player/min-pause.png');
202 | }
203 | }
204 | &-list {
205 | width: 90px;
206 | height: 90px;
207 | @include bg-full;
208 | @include bg-url('~@/assets/images/player/min-list.png');
209 | }
210 | }
211 | }
212 |
--------------------------------------------------------------------------------
/src/components/player/player.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import ReactDOM from 'react-dom'
3 | // import {CSSTransition} from 'react-transition-group'
4 | import classNames from 'classnames'
5 | import { connect } from 'react-redux'
6 |
7 | import Cd from './cd/cd'
8 | import MusicList from './music-list/music-list'
9 | import Progress from '@/base/progress/progress'
10 |
11 | import {
12 | setShowPlayer,
13 | setCurrentMusic,
14 | setCurrentIndex,
15 | setPlayList
16 | } from '@/store/actions'
17 | import { formatTime } from '@/utils/utils'
18 |
19 | import './player.scss'
20 |
21 | // Play组件
22 |
23 | class Player extends Component {
24 | constructor(props) {
25 | super(props)
26 | this.state = {
27 | isFull: false, // 是否全屏显示Player
28 | isPlay: false, // 是否播放
29 | showMusicList: false, // 是否显示播放列表
30 | currentTime: 0, // 当前播放时间
31 | currentMusic: {
32 | id: 368727,
33 | name: '明天,你好',
34 | singer: '牛奶咖啡',
35 | album: 'Lost & Found 去寻找',
36 | image:
37 | 'http://p1.music.126.net/LQ2iUKlZwqGMysGkeCR4ww==/27487790697969.jpg',
38 | duration: 271,
39 | url: 'https://music.163.com/song/media/outer/url?id=368727.mp3'
40 | }
41 | }
42 | }
43 |
44 | componentDidMount() {
45 | this.mmPlayer = ReactDOM.findDOMNode(this.refs.mmPlayer)
46 | this.audioEle = ReactDOM.findDOMNode(this.refs.audioEle)
47 | this.audioEle.load()
48 | this.bindEvents()
49 | }
50 |
51 | componentWillUnmount() {
52 | this.unbindEvents()
53 | }
54 |
55 | //添加绑定事件
56 | bindEvents() {
57 | this.audioEle.addEventListener('canplay', this.readyPlay)
58 | this.audioEle.addEventListener('ended', this.next)
59 | this.audioEle.addEventListener('timeupdate', this.timeUpdate)
60 | }
61 |
62 | //移除绑定事件
63 | unbindEvents() {
64 | this.audioEle.removeEventListener('canplay', this.readyPlay)
65 | this.audioEle.removeEventListener('ended', this.next)
66 | this.audioEle.removeEventListener('timeupdate', this.timeUpdate)
67 | }
68 |
69 | // 开始播放事件
70 | readyPlay = () => {
71 | clearTimeout(this.timer)
72 | this.timer = setTimeout(() => {
73 | this.audioEle.play()
74 | this.setState({
75 | isPlay: true
76 | })
77 | }, 0)
78 | }
79 |
80 | // 播放时间改变
81 | timeUpdate = () => {
82 | this.setState({
83 | currentTime: this.audioEle.currentTime
84 | })
85 | }
86 |
87 | // 上一曲
88 | prev = () => {
89 | let index = this.props.currentIndex - 1
90 | if (index < 0) {
91 | index = this.props.playList.length - 1
92 | }
93 | this.props.setCurrentMusic(this.props.playList[index])
94 | this.props.setCurrentIndex(index)
95 | }
96 |
97 | // 播放暂停
98 | play = e => {
99 | if (this.state.isPlay) {
100 | // 暂停
101 | this.audioEle.pause()
102 | this.setState({
103 | isPlay: false
104 | })
105 | } else {
106 | // 播放
107 | this.audioEle.play()
108 | this.setState({
109 | isPlay: true
110 | })
111 | }
112 | e.stopPropagation()
113 | }
114 |
115 | // 下一曲
116 | next = () => {
117 | let index = this.props.currentIndex + 1
118 | if (index === this.props.playList.length) {
119 | index = 0
120 | }
121 | this.props.setCurrentMusic(this.props.playList[index])
122 | this.props.setCurrentIndex(index)
123 | }
124 |
125 | // 进度条改变
126 | progressEnd = value => {
127 | this.audioEle.currentTime = value * this.props.currentMusic.duration
128 | }
129 |
130 | // 切换播放列表显示
131 | toggleShow = (e, showMusicList = true) => {
132 | this.setState({ showMusicList })
133 | e.stopPropagation()
134 | }
135 |
136 | // 选中播放事件
137 | selectPlay = (id, index) => {
138 | if (id !== this.props.currentMusic.id) {
139 | this.props.setCurrentMusic(this.props.playList[index])
140 | this.props.setCurrentIndex(index)
141 | }
142 | }
143 |
144 | // 删除事件
145 | deleteClick = (id, index) => {
146 | let list = [...this.props.playList],
147 | currentIndex = this.props.currentIndex
148 | list.splice(index, 1)
149 | // 当播放列表没有歌曲时
150 | if (list.length === 0) {
151 | this.props.setShowPlayer(false)
152 | }
153 | // 当删除索引小于播放索引时
154 | if (
155 | index < this.props.currentIndex ||
156 | list.length === this.props.currentIndex
157 | ) {
158 | currentIndex--
159 | this.props.setCurrentIndex(currentIndex)
160 | }
161 | this.props.setCurrentMusic(list[currentIndex] || {})
162 | this.props.setPlayList(list)
163 | }
164 |
165 | render() {
166 | const { isFull, isPlay, showMusicList, currentTime } = this.state
167 | const { currentMusic, playList } = this.props
168 | return (
169 |
170 | {/*
{*/}
172 | {/*this.mmPlayer.style.display = 'block';*/}
173 | {/*}}*/}
174 | {/*onExited={() => {*/}
175 | {/*this.mmPlayer.style.display = 'none';*/}
176 | {/*}}>*/}
177 |
182 |
188 |
189 | {
192 | this.setState({ isFull: false })
193 | }}
194 | />
195 | {currentMusic.name}
196 | {currentMusic.singer}
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 | {formatTime(currentTime)}
205 |
206 |
210 |
211 | {formatTime(currentMusic.duration)}
212 |
213 |
214 |
215 | {/*
*/}
216 |
217 |
221 |
222 |
223 |
224 |
225 |
226 | {/**/}
227 |
this.setState({ isFull: true })}
230 | >
231 |
232 |

238 |
239 |
240 |
{currentMusic.name}
241 |
{currentMusic.singer}
242 |
243 |
247 |
248 |
249 |
253 |
261 |
262 | )
263 | }
264 | }
265 |
266 | //映射Redux全局的state到组件的props上
267 | const mapStateToProps = state => ({
268 | currentMusic: state.currentMusic,
269 | currentIndex: state.currentIndex,
270 | playList: state.playList
271 | })
272 | //映射dispatch到props上
273 | const mapDispatchToProps = dispatch => ({
274 | setShowPlayer: status => {
275 | dispatch(setShowPlayer(status))
276 | },
277 | setCurrentMusic: status => {
278 | dispatch(setCurrentMusic(status))
279 | },
280 | setCurrentIndex: status => {
281 | dispatch(setCurrentIndex(status))
282 | },
283 | setPlayList: status => {
284 | dispatch(setPlayList(status))
285 | }
286 | })
287 |
288 | export default connect(mapStateToProps, mapDispatchToProps)(Player)
289 |
--------------------------------------------------------------------------------