├── .gitignore ├── README.md ├── craco.config.js ├── package.json ├── public ├── favicon.ico └── index.html ├── src ├── App.js ├── assets │ ├── css │ │ └── base.css │ └── img │ │ ├── banner-control-left.png │ │ ├── banner-control-right.png │ │ ├── banner_sprite.png │ │ ├── download.png │ │ ├── friend_sprite.jpg │ │ ├── mine_sprite.png │ │ ├── playbar_sprite.png │ │ ├── playlist_sprite.png │ │ ├── playpanel_bg.png │ │ ├── progress_bar.png │ │ ├── radio_slide.png │ │ ├── recommend-top-bg.png │ │ ├── singer_sprite.png │ │ ├── sprite_01.png │ │ ├── sprite_02.png │ │ ├── sprite_button.png │ │ ├── sprite_button2.png │ │ ├── sprite_cover.png │ │ ├── sprite_footer_01.png │ │ ├── sprite_footer_02.png │ │ ├── sprite_icon.png │ │ ├── sprite_icon2.png │ │ ├── sprite_icon3.png │ │ ├── sprite_table.png │ │ └── wrap-bg.png ├── common │ └── global-style.js ├── components │ ├── album-cover │ │ ├── index.js │ │ └── style.js │ ├── app-footer │ │ ├── index.js │ │ └── style.js │ ├── app-header │ │ ├── index.js │ │ └── style.js │ ├── pagination │ │ ├── index.js │ │ └── style.js │ ├── radio-ranking-cover │ │ ├── index.js │ │ └── style.js │ ├── radio-recommend-cover │ │ ├── index.js │ │ └── style.js │ ├── song-operation-bar │ │ ├── index.js │ │ └── style.js │ ├── theme-cover │ │ ├── index.js │ │ └── style.js │ ├── theme-header-normal │ │ ├── index.js │ │ └── style.js │ ├── theme-header-player │ │ ├── index.js │ │ └── style.js │ ├── theme-header-rcm │ │ ├── index.js │ │ └── style.js │ ├── theme-header-small │ │ ├── index.js │ │ └── style.js │ ├── theme-header-song │ │ ├── index.js │ │ └── style.js │ └── top-ranking │ │ ├── index.js │ │ └── style.js ├── index.js ├── pages │ ├── discover │ │ ├── c-pages │ │ │ ├── album │ │ │ │ ├── c-cpns │ │ │ │ │ ├── hot-album │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ └── style.js │ │ │ │ │ └── top-album │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ └── style.js │ │ │ │ ├── index.js │ │ │ │ ├── store │ │ │ │ │ ├── actionCreators.js │ │ │ │ │ ├── constants.js │ │ │ │ │ ├── index.js │ │ │ │ │ └── reducer.js │ │ │ │ └── style.js │ │ │ ├── artist │ │ │ │ ├── c-cpns │ │ │ │ │ ├── artist-category │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ └── style.js │ │ │ │ │ └── artist-list │ │ │ │ │ │ ├── c-cpns │ │ │ │ │ │ ├── alpha-list │ │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ │ └── style.js │ │ │ │ │ │ └── artist-item │ │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ │ └── style.js │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ └── style.js │ │ │ │ ├── index.js │ │ │ │ ├── store │ │ │ │ │ ├── actionCreators.js │ │ │ │ │ ├── constants.js │ │ │ │ │ ├── index.js │ │ │ │ │ └── reducer.js │ │ │ │ └── style.js │ │ │ ├── djradio │ │ │ │ ├── c-cpns │ │ │ │ │ ├── radio-category │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ └── style.js │ │ │ │ │ ├── radio-ranking │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ └── style.js │ │ │ │ │ └── radio-recommend │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ └── style.js │ │ │ │ ├── index.js │ │ │ │ ├── store │ │ │ │ │ ├── actionCreators.js │ │ │ │ │ ├── constants.js │ │ │ │ │ ├── index.js │ │ │ │ │ └── reducer.js │ │ │ │ └── style.js │ │ │ ├── ranking │ │ │ │ ├── c-cpns │ │ │ │ │ ├── ranking-header │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ └── style.js │ │ │ │ │ ├── ranking-list │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ └── style.js │ │ │ │ │ └── top-ranking │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ └── style.js │ │ │ │ ├── index.js │ │ │ │ ├── store │ │ │ │ │ ├── actionCreators.js │ │ │ │ │ ├── constants.js │ │ │ │ │ ├── index.js │ │ │ │ │ └── reducer.js │ │ │ │ └── style.js │ │ │ ├── recommend │ │ │ │ ├── c-cpns │ │ │ │ │ ├── hot-radio │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ └── style.js │ │ │ │ │ ├── hot-recommend │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ └── style.js │ │ │ │ │ ├── new-album │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ └── style.js │ │ │ │ │ ├── ranking-list │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ └── style.js │ │ │ │ │ ├── settle-singer │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ └── style.js │ │ │ │ │ ├── top-banner │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ └── style.js │ │ │ │ │ └── user-login │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ └── style.js │ │ │ │ ├── index.js │ │ │ │ ├── store │ │ │ │ │ ├── actionCreators.js │ │ │ │ │ ├── constants.js │ │ │ │ │ ├── index.js │ │ │ │ │ └── reducer.js │ │ │ │ └── style.js │ │ │ └── songs │ │ │ │ ├── c-cpns │ │ │ │ ├── songs-category │ │ │ │ │ ├── index.js │ │ │ │ │ └── style.js │ │ │ │ ├── songs-header │ │ │ │ │ ├── index.js │ │ │ │ │ └── style.js │ │ │ │ └── songs-list │ │ │ │ │ ├── index.js │ │ │ │ │ └── style.js │ │ │ │ ├── index.js │ │ │ │ ├── store │ │ │ │ ├── actionCreators.js │ │ │ │ ├── constants.js │ │ │ │ ├── index.js │ │ │ │ └── reducer.js │ │ │ │ └── style.js │ │ ├── index.js │ │ └── style.js │ ├── friend │ │ ├── index.js │ │ └── style.js │ ├── main │ │ └── index.js │ ├── mine │ │ ├── index.js │ │ └── style.js │ └── player │ │ ├── app-play-bar │ │ ├── index.js │ │ └── style.js │ │ ├── app-play-panel │ │ ├── c-cpns │ │ │ ├── lyric-panel │ │ │ │ ├── index.js │ │ │ │ └── style.js │ │ │ ├── play-header │ │ │ │ ├── index.js │ │ │ │ └── style.js │ │ │ └── play-list │ │ │ │ ├── index.js │ │ │ │ └── style.js │ │ ├── index.js │ │ └── style.js │ │ ├── c-cpns │ │ ├── player-comment │ │ │ └── index.js │ │ ├── player-info │ │ │ ├── index.js │ │ │ └── style.js │ │ ├── player-relevant │ │ │ ├── index.js │ │ │ └── style.js │ │ └── player-songs │ │ │ ├── index.js │ │ │ └── style.js │ │ ├── index.js │ │ ├── store │ │ ├── actionCreators.js │ │ ├── constants.js │ │ ├── index.js │ │ └── reducer.js │ │ └── style.js ├── router │ └── index.js ├── services │ ├── album.js │ ├── artist.js │ ├── axios.js │ ├── djradio.js │ ├── local-data.js │ ├── player.js │ ├── ranking.js │ ├── recommend.js │ └── songs.js ├── store │ ├── index.js │ └── reducer.js └── utils │ ├── format-utils.js │ ├── handle-data.js │ ├── lrc-parse.js │ └── ui-helper.js └── yarn.lock /.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.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React实战之云音乐项目 2 | 3 | > 如果觉得不错,或者对你有帮助,点一个star~ coderwhy 4 | 5 | ### 1.1. 项目简介 6 | 7 | 使用React编写的云音乐PC Web项目,接口来源于开源的接口,自己已经做了部署。 8 | 9 | 项目已经完成功能如下:(你可以下载下来自己体验一下) 10 | 11 | 推荐页面: 12 | 13 | * 轮播图 14 | * 热门推荐 15 | * 新碟上架 16 | * 榜单 17 | * 等等 18 | 19 | ![推荐页面](https://tva1.sinaimg.cn/large/007S8ZIlgy1ghv9kwh3cqj31ck0p37uj.jpg) 20 | 21 | ![推荐页面](https://tva1.sinaimg.cn/large/007S8ZIlgy1ghva5bx7bvj31d10p6thq.jpg) 22 | 23 | 歌曲播放: 24 | 25 | * 目前做了榜单中歌曲的点击播放; 26 | * 事实上其他页面只要将歌曲的id传入到redux中就可以,整个逻辑已经打通; 27 | * 做了歌曲的各种控制(暂停、播放、上一首、下一首、进度改变); 28 | * 做了播放循序切换:顺序播放、随机播放、单曲循环; 29 | * 做了歌词的解析、展示、滚动; 30 | 31 | ![歌曲播放](https://tva1.sinaimg.cn/large/007S8ZIlgy1ghv9pc3ki6j30ws0ahti8.jpg) 32 | 33 | 排行榜页面: 34 | 35 | * 各种榜单的切换; 36 | 37 | ![排行榜页面](https://tva1.sinaimg.cn/large/007S8ZIlgy1ghv9qjg0m4j31d10p4ai3.jpg) 38 | 39 | 歌单页面: 40 | 41 | * 选择分类、选择分类后根据分类切换歌单; 42 | * 根据分类,歌单列表的展示; 43 | * 分页功能; 44 | 45 | ![歌单页面](https://tva1.sinaimg.cn/large/007S8ZIlgy1ghv9rsp1stj31d10p44j2.jpg) 46 | 47 | 主播电台: 48 | 49 | * 电台分类的展示、滚动; 50 | * 不同分类展示不同的数据; 51 | * 电台排行榜展示、分页; 52 | 53 | ![主播电台](https://tva1.sinaimg.cn/large/007S8ZIlgy1ghv9sxt3nqj31d10p4k4s.jpg) 54 | 55 | 歌手页面: 56 | 57 | * 各种歌手分类(没找到接口,还自定义了一些数据) 58 | * 歌手字母分类、对应歌手展示; 59 | 60 | ![歌手页面](https://tva1.sinaimg.cn/large/007S8ZIlgy1ghv9ts3h5dj31d10p4qfo.jpg) 61 | 62 | 新碟上架页面: 63 | 64 | * 热门新碟; 65 | * 全部新碟、分页展示; 66 | 67 | ![新碟上架页面](https://tva1.sinaimg.cn/large/007S8ZIlgy1ghv9uryexgj31d10p4aue.jpg) 68 | 69 | 70 | 71 | ### 1.2. 项目规范 72 | 73 | **项目规范:项目中有一些开发规范和代码风格** 74 | 75 | * 1.文件夹、文件名称统一小写、多个单词以连接符(-)连接; 76 | * 2.JavaScript变量名称采用小驼峰标识,常量全部使用大写字母,组件采用大驼峰; 77 | * 3.CSS采用普通CSS和styled-component结合来编写(全局采用普通CSS、局部采用styled-component); 78 | * 4.整个项目不再使用class组件,统一使用函数式组件,并且全面使用Hooks; 79 | * 5.所有的函数式组件,为了避免不必要的渲染,全部使用memo进行包裹; 80 | * 6.组件内部的状态,使用useState、useReducer;业务数据全部放在redux中管理; 81 | * 7.函数组件内部基本按照如下顺序编写代码: 82 | * 组件内部state管理; 83 | * redux的hooks代码; 84 | * 其他组件hooks代码; 85 | * 其他逻辑代码; 86 | * 返回JSX代码; 87 | * 8.redux代码规范如下: 88 | * redux结合ImmutableJS 89 | * 每个模块有自己独立的reducer,通过combineReducer进行合并; 90 | * 异步请求代码使用redux-thunk,并且写在actionCreators中; 91 | * redux直接采用redux hooks方式编写,不再使用connect; 92 | * 9.网络请求采用axios 93 | * 对axios进行二次封装; 94 | * 所有的模块请求会放到一个请求文件中单独管理; 95 | * 10.项目使用AntDesign 96 | * 项目中某些AntDesign中的组件会被拿过来使用; 97 | * 但是多部分组件还是自己进行编写; 98 | * 其他规范在项目中根据实际情况决定和编写; 99 | 100 | 101 | 102 | ### 1.3. 项目运行 103 | 104 | clone项目: 105 | 106 | ``` 107 | git clone https://github.com/coderwhy/hy-react-web-music.git 108 | ``` 109 | 110 | 安装项目依赖: 111 | 112 | ```shell 113 | yarn install 114 | ``` 115 | 116 | 项目运行: 117 | 118 | ```shell 119 | yarn start 120 | ``` 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /craco.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const resolve = dir => path.resolve(__dirname, dir); 4 | 5 | module.exports = { 6 | webpack: { 7 | alias: { 8 | '@': resolve("src"), 9 | 'components': resolve("src/components") 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hy-react-music", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@craco/craco": "^5.6.4", 7 | "@testing-library/jest-dom": "^4.2.4", 8 | "@testing-library/react": "^9.3.2", 9 | "@testing-library/user-event": "^7.1.2", 10 | "antd": "^4.2.4", 11 | "axios": "^0.19.2", 12 | "classnames": "^2.2.6", 13 | "immutable": "^4.0.0-rc.12", 14 | "normalize.css": "^8.0.1", 15 | "react": "^16.13.1", 16 | "react-dom": "^16.13.1", 17 | "react-redux": "^7.2.0", 18 | "react-router-config": "^5.1.1", 19 | "react-router-dom": "^5.2.0", 20 | "react-scripts": "3.4.1", 21 | "redux": "^4.0.5", 22 | "redux-immutable": "^4.0.0", 23 | "redux-thunk": "^2.3.0", 24 | "styled-components": "^5.1.0" 25 | }, 26 | "scripts": { 27 | "start": "craco start", 28 | "build": "craco build", 29 | "test": "craco test", 30 | "eject": "react-scripts eject" 31 | }, 32 | "eslintConfig": { 33 | "extends": "react-app" 34 | }, 35 | "browserslist": { 36 | "production": [ 37 | ">0.2%", 38 | "not dead", 39 | "not op_mini all" 40 | ], 41 | "development": [ 42 | "last 1 chrome version", 43 | "last 1 firefox version", 44 | "last 1 safari version" 45 | ] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderwhy/hy-react-web-music/0e1c3ad58d1a34277f7b34c4ea4656e3240f2017/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | coderwhy music 13 | 14 | 15 | 16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Provider } from "react-redux"; 4 | import store from "./store"; 5 | 6 | import HYMain from './pages/main'; 7 | 8 | function App() { 9 | return ( 10 | 11 | 12 | 13 | ); 14 | } 15 | 16 | export default App; 17 | -------------------------------------------------------------------------------- /src/assets/css/base.css: -------------------------------------------------------------------------------- 1 | @import '~antd/dist/antd.css'; 2 | @import "~normalize.css"; 3 | 4 | /* 样式的重置 */ 5 | body, html, h1, h2, h3, h4, h5, h6, ul, ol, li, dl, dt, dd, header, menu, section, p, input, td, th, ins { 6 | padding: 0; 7 | margin: 0; 8 | } 9 | 10 | ul, ol, li { 11 | list-style: none; 12 | } 13 | 14 | a { 15 | text-decoration: none; 16 | color: #666; 17 | } 18 | 19 | a:hover { 20 | color: #666; 21 | text-decoration: underline; 22 | } 23 | 24 | i, em { 25 | font-style: normal; 26 | } 27 | 28 | input, textarea, button, select, a { 29 | outline: none; 30 | border: none; 31 | } 32 | 33 | table { 34 | border-collapse: collapse; 35 | border-spacing: 0; 36 | } 37 | 38 | img { 39 | border: none; 40 | vertical-align: middle; 41 | } 42 | 43 | /* 全局样式 */ 44 | body, textarea, select, input, button { 45 | font-size: 12px; 46 | color: #333; 47 | font-family: Arial, Helvetica, sans-serif; 48 | background-color: #f5f5f5; 49 | } 50 | 51 | .text-nowrap { 52 | white-space: nowrap; 53 | text-overflow: ellipsis; 54 | overflow: hidden; 55 | } 56 | 57 | .wrap-v1 { 58 | width: 1100px; 59 | margin: 0 auto; 60 | } 61 | 62 | .wrap-v2 { 63 | width: 980px; 64 | margin: 0 auto; 65 | } 66 | 67 | .sprite_01 { 68 | background: url(../img/sprite_01.png) no-repeat 0 9999px; 69 | } 70 | 71 | .sprite_02 { 72 | background: url(../img/sprite_02.png) no-repeat 0 9999px; 73 | } 74 | 75 | .sprite_covor { 76 | background: url(../img/sprite_cover.png) no-repeat 0 9999px; 77 | } 78 | 79 | .sprite_icon { 80 | background: url(../img/sprite_icon.png) no-repeat 0 9999px; 81 | } 82 | 83 | .sprite_icon2 { 84 | background: url(../img/sprite_icon2.png) no-repeat 0 9999px; 85 | } 86 | 87 | .sprite_icon3 { 88 | background: url(../img/sprite_icon3.png) no-repeat 0 9999px; 89 | } 90 | 91 | .sprite_button { 92 | background: url(../img/sprite_button.png) no-repeat 0 9999px; 93 | } 94 | 95 | .sprite_button2 { 96 | background: url(../img/sprite_button2.png) no-repeat 0 9999px; 97 | } 98 | 99 | .sprite_table { 100 | background: url(../img/sprite_table.png) no-repeat 0 9999px; 101 | } 102 | 103 | .sprite_playbar { 104 | background: url(../img/playbar_sprite.png) no-repeat 0 9999px; 105 | } 106 | 107 | .sprite_playlist { 108 | background: url(../img/playlist_sprite.png) no-repeat 0 9999px; 109 | } 110 | 111 | .image_cover { 112 | position: absolute; 113 | left: 0; 114 | right: 0; 115 | top: 0; 116 | bottom: 0; 117 | text-indent: -9999px; 118 | background: url(../img/sprite_cover.png) no-repeat -145px -57px; 119 | } 120 | 121 | 122 | .ant-message .ant-message-notice-content { 123 | position: fixed; 124 | left: 50%; 125 | transform: translateX(-50%); 126 | bottom: 60px; 127 | background-color: rgba(0, 0, 0, .5); 128 | color: #fff; 129 | } 130 | 131 | span:hover.link { 132 | text-decoration: underline; 133 | cursor: pointer; 134 | } 135 | -------------------------------------------------------------------------------- /src/assets/img/banner-control-left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderwhy/hy-react-web-music/0e1c3ad58d1a34277f7b34c4ea4656e3240f2017/src/assets/img/banner-control-left.png -------------------------------------------------------------------------------- /src/assets/img/banner-control-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderwhy/hy-react-web-music/0e1c3ad58d1a34277f7b34c4ea4656e3240f2017/src/assets/img/banner-control-right.png -------------------------------------------------------------------------------- /src/assets/img/banner_sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderwhy/hy-react-web-music/0e1c3ad58d1a34277f7b34c4ea4656e3240f2017/src/assets/img/banner_sprite.png -------------------------------------------------------------------------------- /src/assets/img/download.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderwhy/hy-react-web-music/0e1c3ad58d1a34277f7b34c4ea4656e3240f2017/src/assets/img/download.png -------------------------------------------------------------------------------- /src/assets/img/friend_sprite.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderwhy/hy-react-web-music/0e1c3ad58d1a34277f7b34c4ea4656e3240f2017/src/assets/img/friend_sprite.jpg -------------------------------------------------------------------------------- /src/assets/img/mine_sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderwhy/hy-react-web-music/0e1c3ad58d1a34277f7b34c4ea4656e3240f2017/src/assets/img/mine_sprite.png -------------------------------------------------------------------------------- /src/assets/img/playbar_sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderwhy/hy-react-web-music/0e1c3ad58d1a34277f7b34c4ea4656e3240f2017/src/assets/img/playbar_sprite.png -------------------------------------------------------------------------------- /src/assets/img/playlist_sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderwhy/hy-react-web-music/0e1c3ad58d1a34277f7b34c4ea4656e3240f2017/src/assets/img/playlist_sprite.png -------------------------------------------------------------------------------- /src/assets/img/playpanel_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderwhy/hy-react-web-music/0e1c3ad58d1a34277f7b34c4ea4656e3240f2017/src/assets/img/playpanel_bg.png -------------------------------------------------------------------------------- /src/assets/img/progress_bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderwhy/hy-react-web-music/0e1c3ad58d1a34277f7b34c4ea4656e3240f2017/src/assets/img/progress_bar.png -------------------------------------------------------------------------------- /src/assets/img/radio_slide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderwhy/hy-react-web-music/0e1c3ad58d1a34277f7b34c4ea4656e3240f2017/src/assets/img/radio_slide.png -------------------------------------------------------------------------------- /src/assets/img/recommend-top-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderwhy/hy-react-web-music/0e1c3ad58d1a34277f7b34c4ea4656e3240f2017/src/assets/img/recommend-top-bg.png -------------------------------------------------------------------------------- /src/assets/img/singer_sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderwhy/hy-react-web-music/0e1c3ad58d1a34277f7b34c4ea4656e3240f2017/src/assets/img/singer_sprite.png -------------------------------------------------------------------------------- /src/assets/img/sprite_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderwhy/hy-react-web-music/0e1c3ad58d1a34277f7b34c4ea4656e3240f2017/src/assets/img/sprite_01.png -------------------------------------------------------------------------------- /src/assets/img/sprite_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderwhy/hy-react-web-music/0e1c3ad58d1a34277f7b34c4ea4656e3240f2017/src/assets/img/sprite_02.png -------------------------------------------------------------------------------- /src/assets/img/sprite_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderwhy/hy-react-web-music/0e1c3ad58d1a34277f7b34c4ea4656e3240f2017/src/assets/img/sprite_button.png -------------------------------------------------------------------------------- /src/assets/img/sprite_button2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderwhy/hy-react-web-music/0e1c3ad58d1a34277f7b34c4ea4656e3240f2017/src/assets/img/sprite_button2.png -------------------------------------------------------------------------------- /src/assets/img/sprite_cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderwhy/hy-react-web-music/0e1c3ad58d1a34277f7b34c4ea4656e3240f2017/src/assets/img/sprite_cover.png -------------------------------------------------------------------------------- /src/assets/img/sprite_footer_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderwhy/hy-react-web-music/0e1c3ad58d1a34277f7b34c4ea4656e3240f2017/src/assets/img/sprite_footer_01.png -------------------------------------------------------------------------------- /src/assets/img/sprite_footer_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderwhy/hy-react-web-music/0e1c3ad58d1a34277f7b34c4ea4656e3240f2017/src/assets/img/sprite_footer_02.png -------------------------------------------------------------------------------- /src/assets/img/sprite_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderwhy/hy-react-web-music/0e1c3ad58d1a34277f7b34c4ea4656e3240f2017/src/assets/img/sprite_icon.png -------------------------------------------------------------------------------- /src/assets/img/sprite_icon2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderwhy/hy-react-web-music/0e1c3ad58d1a34277f7b34c4ea4656e3240f2017/src/assets/img/sprite_icon2.png -------------------------------------------------------------------------------- /src/assets/img/sprite_icon3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderwhy/hy-react-web-music/0e1c3ad58d1a34277f7b34c4ea4656e3240f2017/src/assets/img/sprite_icon3.png -------------------------------------------------------------------------------- /src/assets/img/sprite_table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderwhy/hy-react-web-music/0e1c3ad58d1a34277f7b34c4ea4656e3240f2017/src/assets/img/sprite_table.png -------------------------------------------------------------------------------- /src/assets/img/wrap-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderwhy/hy-react-web-music/0e1c3ad58d1a34277f7b34c4ea4656e3240f2017/src/assets/img/wrap-bg.png -------------------------------------------------------------------------------- /src/common/global-style.js: -------------------------------------------------------------------------------- 1 | // 扩大可点击区域 2 | const extendClick = () => { 3 | return ` 4 | position: relative; 5 | &:before { 6 | content: ''; 7 | position: absolute; 8 | top: -10px; bottom: -10px; left: -10px; right: -10px; 9 | }; 10 | ` 11 | } 12 | // 一行文字溢出部分用... 代替 13 | const noWrap = () => { 14 | return ` 15 | text-overflow: ellipsis; 16 | overflow: hidden; 17 | white-space: nowrap; 18 | ` 19 | } 20 | 21 | export default { 22 | 'theme-color': '#C20C0C', 23 | 'theme-color-shadow': 'rgba(212, 68, 57, .5)', 24 | 'font-color-light': '#f1f1f1', 25 | 'font-color-desc': '#2E3030', 26 | 'font-color-desc-v2': '#bba8a8',// 略淡 27 | 'font-size-s': '12px', 28 | 'font-size-n': '14px', 29 | 'font-size-l': '16px', 30 | 'font-size-ll': '20px', 31 | "border-color": '#e4e4e4', 32 | 'background-color': '#f2f3f4', 33 | 'background-color-shadow': 'rgba(0, 0, 0, 0.3)', 34 | 'highlight-background-color': '#fff', 35 | extendClick, 36 | noWrap 37 | } -------------------------------------------------------------------------------- /src/components/album-cover/index.js: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | 3 | import { 4 | getSizeImage 5 | } from "@/utils/format-utils"; 6 | 7 | import { 8 | AlbumWrapper 9 | } from "./style"; 10 | 11 | 12 | export default memo(function HYAlbumCover(props) { 13 | const { info, size = "100px", width = "118px", bgp = "-570px" } = props; 14 | 15 | return ( 16 | 17 |
18 | 19 | {info.name} 20 |
21 |
22 |
{info.name}
23 |
{info.artist.name}
24 |
25 |
26 | ) 27 | }) 28 | -------------------------------------------------------------------------------- /src/components/album-cover/style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const AlbumWrapper = styled.div` 4 | .album-image { 5 | position: relative; 6 | width: ${props => props.width}; 7 | height: ${props => props.size}; 8 | overflow: hidden; 9 | margin-top: 15px; 10 | 11 | img { 12 | width: ${props => props.size}; 13 | height: ${props => props.size}; 14 | } 15 | 16 | .cover { 17 | position: absolute; 18 | left: 0; 19 | right: 0; 20 | top: 0; 21 | bottom: 0; 22 | background-position: 0 ${props => props.bgp}; 23 | text-indent: -9999px; 24 | } 25 | } 26 | 27 | .album-info { 28 | font-size: 12px; 29 | width: ${props => props.size}; 30 | .name { 31 | color: #000; 32 | white-space: nowrap; 33 | text-overflow: ellipsis; 34 | overflow: hidden; 35 | } 36 | 37 | .artist { 38 | color: #666; 39 | } 40 | } 41 | ` -------------------------------------------------------------------------------- /src/components/app-footer/index.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment, memo } from 'react'; 2 | 3 | import { 4 | footerLinks, 5 | footerImages 6 | } from "@/services/local-data"; 7 | 8 | import { 9 | AppFooterWrapper, 10 | FooterLeft, 11 | FooterRight 12 | } from "./style"; 13 | 14 | export default memo(function HYAppFooter() { 15 | return ( 16 | 17 |
18 | 19 |
20 | { 21 | footerLinks.map(item => { 22 | return ( 23 | 24 | {item.title} 25 | | 26 | 27 | ) 28 | }) 29 | } 30 |
31 |
32 | 网易公司版权所有©1997-2020 33 | 34 | 杭州乐读科技有限公司运营: 35 | 浙网文[2018]3506-263号 36 | 37 |
38 |
39 | 违法和不良信息举报电话:0571-89853516 40 | 41 | 举报邮箱: 42 | ncm5990@163.com 43 | 44 |
45 |
46 | 粤B2-20090191-18 47 | 48 | 工业和信息化部备案管理系统网站 49 | 50 |
51 |
52 | 53 | { 54 | footerImages.map((item, index) => { 55 | return ( 56 |
  • 57 | 58 | {item.title} 59 |
  • 60 | ) 61 | }) 62 | } 63 |
    64 |
    65 |
    66 | ) 67 | }) 68 | -------------------------------------------------------------------------------- /src/components/app-footer/style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const AppFooterWrapper = styled.div` 4 | height: 172px; 5 | background-color: #f2f2f2; 6 | color: #666; 7 | border-top: 1px solid #d3d3d3; 8 | 9 | .content { 10 | display: flex; 11 | justify-content: space-between; 12 | align-items: center; 13 | } 14 | ` 15 | 16 | export const FooterLeft = styled.div` 17 | padding-top: 15px; 18 | line-height: 24px; 19 | 20 | .link { 21 | a { 22 | color: #999; 23 | } 24 | 25 | .line { 26 | margin: 0 10px; 27 | color: #999; 28 | } 29 | } 30 | 31 | .copyright { 32 | span { 33 | margin-right: 15px; 34 | } 35 | } 36 | ` 37 | 38 | export const FooterRight = styled.ul` 39 | display: flex; 40 | 41 | .item { 42 | display: flex; 43 | flex-direction: column; 44 | align-items: center; 45 | margin-right: 40px; 46 | 47 | .link { 48 | display: block; 49 | width: 50px; 50 | height: 45px; 51 | 52 | background-image: url(${require("@/assets/img/sprite_footer_02.png")}); 53 | background-size: 110px 450px; 54 | } 55 | 56 | :nth-child(1) .link { 57 | background-position: -60px -101px; 58 | } 59 | :nth-child(2) .link { 60 | background-position: 0 0; 61 | } 62 | :nth-child(2) .link { 63 | background-position: -60px -50px; 64 | } 65 | :nth-child(2) .link { 66 | background-position: 0 -101px; 67 | } 68 | 69 | .title { 70 | margin-top: 5px; 71 | display: block; 72 | width: 52px; 73 | height: 10px; 74 | background-image: url(${require("@/assets/img/sprite_footer_01.png")}); 75 | background-size: 180px 100px; 76 | } 77 | 78 | :nth-child(1) .title { 79 | background-position: -1px -90px; 80 | } 81 | :nth-child(2) .title { 82 | background-position: 0 0; 83 | margin-top: 7px; 84 | } 85 | :nth-child(3) .title { 86 | background-position: 0 -54px; 87 | margin-top: 6px; 88 | } 89 | 90 | :nth-child(4) .title { 91 | background-position: -1px -72px; 92 | margin-top: 6px; 93 | } 94 | } 95 | ` 96 | -------------------------------------------------------------------------------- /src/components/app-header/index.js: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import { NavLink } from "react-router-dom"; 3 | import classnames from "classnames"; 4 | 5 | import { headerLinks } from "@/services/local-data"; 6 | 7 | import { Input } from "antd"; 8 | import { SearchOutlined } from '@ant-design/icons'; 9 | 10 | import { 11 | AppHeaderWrapper, 12 | HeaderLeft, 13 | HeaderRight 14 | } from "./style"; 15 | 16 | export default memo(function HYAppHeader() { 17 | 18 | const showItem = (item, index) => { 19 | if (index < 3) { 20 | return ( 21 | 22 | {item.title} 23 | 24 | 25 | ) 26 | } else { 27 | return {item.title} 28 | } 29 | } 30 | 31 | return ( 32 | 33 |
    34 | 35 | 网易云音乐 36 |
    37 | { 38 | headerLinks.map((item, index) => { 39 | return ( 40 |
    41 | {showItem(item, index)} 42 |
    43 | ) 44 | }) 45 | } 46 |
    47 |
    48 | 49 | } /> 50 |
    创作者中心
    51 |
    登录
    52 |
    53 |
    54 |
    55 |
    56 | ) 57 | }) 58 | -------------------------------------------------------------------------------- /src/components/app-header/style.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const AppHeaderWrapper = styled.div` 4 | height: 75px; 5 | background-color: #242424; 6 | font-size: 14px; 7 | 8 | .content { 9 | display: flex; 10 | justify-content: space-between; 11 | } 12 | 13 | 14 | .divider { 15 | height: 5px; 16 | background-color: #C20C0C; 17 | } 18 | ` 19 | 20 | export const HeaderLeft = styled.div` 21 | display: flex; 22 | 23 | .logo { 24 | display: block; 25 | width: 176px; 26 | height: 70px; 27 | background-position: 0 0; 28 | text-indent: -9999px; 29 | } 30 | 31 | .select-list { 32 | display: flex; 33 | line-height: 70px; 34 | 35 | .select-item { 36 | position: relative; 37 | 38 | a { 39 | display: block; 40 | padding: 0 20px; 41 | color: #ccc; 42 | } 43 | 44 | :last-of-type a { 45 | position: relative; 46 | ::after { 47 | position: absolute; 48 | content: ""; 49 | width: 28px; 50 | height: 19px; 51 | background-image: url(${require("@/assets/img/sprite_01.png")}); 52 | background-position: -190px 0; 53 | top: 20px; 54 | right: -15px; 55 | } 56 | } 57 | 58 | &:hover a, .active { 59 | color: #fff; 60 | background: #000; 61 | text-decoration: none; 62 | } 63 | 64 | .active .icon { 65 | position: absolute; 66 | display: inline-block; 67 | width: 12px; 68 | height: 7px; 69 | bottom: -1px; 70 | left: 50%; 71 | transform: translate(-50%, 0); 72 | background-position: -226px 0; 73 | } 74 | } 75 | } 76 | ` 77 | 78 | export const HeaderRight = styled.div` 79 | display: flex; 80 | align-items: center; 81 | color: #ccc; 82 | font-size: 12px; 83 | 84 | .search { 85 | width: 158px; 86 | height: 32px; 87 | border-radius: 16px; 88 | 89 | input { 90 | &::placeholder { 91 | font-size: 12px; 92 | } 93 | } 94 | } 95 | 96 | .center { 97 | width: 90px; 98 | height: 32px; 99 | line-height: 32px; 100 | text-align: center; 101 | border: 1px solid #666; 102 | border-radius: 16px; 103 | margin: 0 16px; 104 | } 105 | ` 106 | 107 | 108 | -------------------------------------------------------------------------------- /src/components/pagination/index.js: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | 3 | import { Pagination } from 'antd'; 4 | import { PaginationWrapper } from './style'; 5 | 6 | export default memo(function HYPagination(props) { 7 | const { currentPage, total, onPageChange } = props; 8 | 9 | // render function 10 | function itemRender(current, type, originalElement) { 11 | if (type === 'prev') { 12 | return ; 13 | } 14 | if (type === 'next') { 15 | return ; 16 | } 17 | return originalElement; 18 | } 19 | 20 | return ( 21 | 22 | 31 | 32 | ) 33 | }) 34 | -------------------------------------------------------------------------------- /src/components/pagination/style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const PaginationWrapper = styled.div` 4 | .pagination { 5 | margin: 30px 0; 6 | text-align: center; 7 | 8 | .control { 9 | width: 69px; 10 | height: 24px; 11 | line-height: 22px; 12 | text-align: center; 13 | border: 1px solid #ccc; 14 | border-radius: 2px; 15 | margin: 0 5px; 16 | color: #333; 17 | 18 | &:disabled { 19 | color: #999; 20 | } 21 | } 22 | 23 | .ant-pagination-item { 24 | border: 1px solid #ccc !important; 25 | margin: 0 5px; 26 | border-radius: 3px; 27 | 28 | a { 29 | font-size: 12px; 30 | } 31 | 32 | &.ant-pagination-item-active { 33 | background-color: #c20c0c; 34 | border-color: #A2161B; 35 | a { 36 | color: #fff; 37 | } 38 | } 39 | } 40 | } 41 | ` -------------------------------------------------------------------------------- /src/components/radio-ranking-cover/index.js: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | 3 | import { getSizeImage } from '@/utils/format-utils' 4 | 5 | import { 6 | CoverWrapper 7 | } from "./style" 8 | 9 | export default memo(function HYRadioRankingCover(props) { 10 | const { radio } = props; 11 | 12 | return ( 13 | 14 | 15 | 16 | 17 |
    18 |

    {radio.name}

    19 |
    20 | 21 | {radio.dj.nickname} 22 |
    23 |
    24 | 共{radio.programCount}期 25 | 订阅{radio.subCount}次 26 |
    27 |
    28 |
    29 | ) 30 | }) 31 | -------------------------------------------------------------------------------- /src/components/radio-ranking-cover/style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const CoverWrapper = styled.div` 4 | display: flex; 5 | align-items: center; 6 | width: 48%; 7 | padding: 20px 0; 8 | border-bottom: 1px solid #e7e7e7; 9 | 10 | img { 11 | width: 120px; 12 | height: 120px; 13 | } 14 | 15 | .info { 16 | margin-left: 20px; 17 | 18 | .nickname { 19 | margin-top: 15px; 20 | color: #000; 21 | 22 | i.left { 23 | display: inline-block; 24 | position: relative; 25 | top: 2px; 26 | width: 14px; 27 | height: 15px; 28 | margin-right: 2px; 29 | background-position: -50px -300px; 30 | } 31 | } 32 | 33 | .count { 34 | color: #666; 35 | margin-top: 5px; 36 | .subscribe { 37 | margin-left: 10px; 38 | } 39 | } 40 | } 41 | ` -------------------------------------------------------------------------------- /src/components/radio-recommend-cover/index.js: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | 3 | import { getSizeImage } from "@/utils/format-utils"; 4 | 5 | import { 6 | CoverWrapper 7 | } from "./style"; 8 | 9 | export default memo(function HYRadioRecommendCover(props) { 10 | const { info } = props; 11 | 12 | return ( 13 | 14 | 15 | 16 | 17 | {info.name} 18 |

    {info.desc}

    19 |
    20 | ) 21 | }) 22 | -------------------------------------------------------------------------------- /src/components/radio-recommend-cover/style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const CoverWrapper = styled.div` 4 | display: flex; 5 | flex-direction: column; 6 | width: 150px; 7 | 8 | .name { 9 | font-size: 14px; 10 | color: #333; 11 | margin: 5px 0; 12 | } 13 | 14 | img { 15 | width: 150px; 16 | height: 150px; 17 | } 18 | 19 | p { 20 | color: #666; 21 | } 22 | ` -------------------------------------------------------------------------------- /src/components/song-operation-bar/index.js: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | 3 | import { OperationBarWrapper } from './style'; 4 | 5 | export default memo(function HYSongOperationBar(props) { 6 | const { favorTitle, shareTitle, downloadTitle, commentTitle } = props; 7 | 8 | return ( 9 | 10 | 11 | 12 | 13 | 14 | 播放 15 | 16 | 17 | + 18 | 19 | 20 | {favorTitle} 21 | 22 | 23 | {shareTitle} 24 | 25 | 26 | {downloadTitle} 27 | 28 | 29 | {commentTitle} 30 | 31 | 32 | ) 33 | }) 34 | -------------------------------------------------------------------------------- /src/components/song-operation-bar/style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const OperationBarWrapper = styled.div` 4 | display: flex; 5 | align-items: center; 6 | 7 | .play { 8 | display: flex; 9 | align-items: center; 10 | margin-right: 5px; 11 | 12 | .play-icon { 13 | display: inline-block; 14 | height: 31px; 15 | line-height: 31px; 16 | background-position: right -428px; 17 | 18 | .play { 19 | color: #fff; 20 | display: flex; 21 | align-items: center; 22 | padding: 0 7px 0 8px; 23 | background-position: 0 -387px; 24 | 25 | i { 26 | display: inline-block; 27 | width: 20px; 28 | height: 18px; 29 | margin: -2px 2px 0; 30 | background-position: 0 -1622px; 31 | } 32 | } 33 | } 34 | 35 | .add-icon { 36 | display: inline-block; 37 | width: 31px; 38 | height: 31px; 39 | margin-left: -3px; 40 | padding-right: 0; 41 | background-position: 0 -1588px; 42 | text-indent: -9999px; 43 | } 44 | } 45 | 46 | .item { 47 | display: inline-block; 48 | height: 31px; 49 | margin-right: 6px; 50 | padding-right: 5px; 51 | background-position: right -1020px; 52 | 53 | .icon { 54 | display: inline-block; 55 | height: 31px; 56 | line-height: 31px; 57 | padding: 0 7px 0 28px; 58 | font-family: simsun; 59 | } 60 | 61 | .favor-icon { 62 | background-position: 0 -977px; 63 | } 64 | 65 | .share-icon { 66 | background-position: 0 -1225px; 67 | } 68 | 69 | .download-icon { 70 | background-position: 0 -2761px; 71 | } 72 | 73 | .comment-icon { 74 | background-position: 0 -1465px; 75 | } 76 | } 77 | ` -------------------------------------------------------------------------------- /src/components/theme-cover/index.js: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | 3 | import { 4 | getSizeImage, 5 | getCount 6 | } from "@/utils/format-utils"; 7 | 8 | import { 9 | ThemeCoverWrapper 10 | } from "./style"; 11 | 12 | export default memo(function HYThemeCover(props) { 13 | const { info, right } = props; 14 | 15 | return ( 16 | 17 |
    18 | 19 |
    20 |
    21 | 22 | 23 | {getCount(info.playCount)} 24 | 25 | 26 |
    27 |
    28 |
    29 |
    30 | {info.name} 31 |
    32 | {/*
    33 | by {info.copywriter || info.creator.nickname} 34 |
    */} 35 |
    36 | ) 37 | }) 38 | -------------------------------------------------------------------------------- /src/components/theme-cover/style.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const ThemeCoverWrapper = styled.div` 4 | width: 140px; 5 | margin: 20px ${props => (props.right || 0)} 20px 0; 6 | 7 | .cover-top { 8 | position: relative; 9 | 10 | &>img { 11 | width: 140px; 12 | height: 140px; 13 | } 14 | 15 | .cover { 16 | position: absolute; 17 | top: 0; 18 | left: 0; 19 | width: 100%; 20 | height: 100%; 21 | background-position: 0 0; 22 | 23 | .info { 24 | display: flex; 25 | justify-content: space-between; 26 | align-items: center; 27 | padding: 0 10px; 28 | position: absolute; 29 | bottom: 0; 30 | left: 0; 31 | right: 0; 32 | background-position: 0 -537px; 33 | color: #ccc; 34 | height: 27px; 35 | 36 | .erji { 37 | margin-right: 5px; 38 | display: inline-block; 39 | width: 14px; 40 | height: 11px; 41 | background-position: 0 -24px; 42 | } 43 | 44 | .play { 45 | display: inline-block; 46 | width: 16px; 47 | height: 17px; 48 | background-position: 0 0; 49 | } 50 | } 51 | } 52 | } 53 | 54 | .cover-bottom { 55 | font-size: 14px; 56 | color: #000; 57 | margin-top: 5px; 58 | } 59 | 60 | .cover-source { 61 | color: #666; 62 | } 63 | ` -------------------------------------------------------------------------------- /src/components/theme-header-normal/index.js: -------------------------------------------------------------------------------- 1 | import React, {memo} from 'react'; 2 | 3 | import { 4 | HeaderWrapper 5 | } from "./style"; 6 | 7 | export default memo(function HYThemeHeaderNormal(props) { 8 | const { title, rightSlot } = props; 9 | 10 | return ( 11 | 12 |
    {title}
    13 |
    14 | {rightSlot} 15 |
    16 |
    17 | ) 18 | }) 19 | -------------------------------------------------------------------------------- /src/components/theme-header-normal/style.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const HeaderWrapper = styled.div` 4 | display: flex; 5 | padding-bottom: 5px; 6 | border-bottom: 2px solid #c20c0c; 7 | font-family: "Microsoft Yahei", Arial, Helvetica, sans-serif; 8 | font-size: 24px; 9 | ` 10 | -------------------------------------------------------------------------------- /src/components/theme-header-player/index.js: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | 3 | import { HeaderWrapper } from './style'; 4 | 5 | export default memo(function HYThemeHeaderPlayer(props) { 6 | const { title } = props; 7 | 8 | return ( 9 | 10 |

    {title}

    11 |
    12 | ) 13 | }) 14 | -------------------------------------------------------------------------------- /src/components/theme-header-player/style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const HeaderWrapper = styled.div` 4 | h3 { 5 | height: 23px; 6 | margin-bottom: 20px; 7 | border-bottom: 1px solid #ccc; 8 | font-size: 12px; 9 | font-weight: 700; 10 | color: #333; 11 | } 12 | ` -------------------------------------------------------------------------------- /src/components/theme-header-rcm/index.js: -------------------------------------------------------------------------------- 1 | import React, {memo} from 'react'; 2 | import PropTypes from "prop-types"; 3 | 4 | import { Link } from 'react-router-dom'; 5 | import { 6 | HeaderWrapper 7 | } from "./style"; 8 | 9 | const HYThemeHeaderRCM = memo(function(props) { 10 | const { title, keywords, moreLink, keywordClick } = props; 11 | 12 | return ( 13 | 14 |
    15 |

    {title}

    16 |
    17 | { 18 | keywords.map((item, index) => { 19 | return ( 20 |
    21 | keywordClick(item)}>{item} 22 | | 23 |
    24 | ) 25 | }) 26 | } 27 |
    28 |
    29 |
    30 | 更多 31 | 32 |
    33 |
    34 | ) 35 | }) 36 | 37 | HYThemeHeaderRCM.defaultProps = { 38 | keywords: [] 39 | } 40 | 41 | HYThemeHeaderRCM.propTypes = { 42 | title: PropTypes.string.isRequired, 43 | keywords: PropTypes.array 44 | } 45 | 46 | export default HYThemeHeaderRCM; 47 | -------------------------------------------------------------------------------- /src/components/theme-header-rcm/style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const HeaderWrapper = styled.div` 4 | height: 33px; 5 | border-bottom: 2px solid #C10D0C; 6 | padding: 0 10px 4px 34px; 7 | background-position: -225px -156px; 8 | 9 | display: flex; 10 | justify-content: space-between; 11 | align-items: center; 12 | 13 | .left { 14 | display: flex; 15 | align-items: center; 16 | 17 | .title { 18 | font-size: 20px; 19 | font-family: "Microsoft Yahei", Arial, Helvetica, sans-serif; 20 | margin-right: 20px; 21 | } 22 | 23 | .keyword { 24 | display: flex; 25 | 26 | .item { 27 | .divider { 28 | margin: 0 15px; 29 | color: #ccc; 30 | } 31 | } 32 | } 33 | } 34 | 35 | .right { 36 | display: flex; 37 | align-items: center; 38 | .icon { 39 | display: inline-block; 40 | width: 12px; 41 | height: 12px; 42 | margin-left: 4px; 43 | background-position: 0 -240px; 44 | } 45 | } 46 | ` -------------------------------------------------------------------------------- /src/components/theme-header-small/index.js: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { 5 | HeaderWrapper 6 | } from "./style"; 7 | 8 | const HYThemeHeaderSmall = memo(function (props) { 9 | const { title, more } = props; 10 | 11 | return ( 12 | 13 |

    {title}

    14 | {more} 15 |
    16 | ) 17 | }) 18 | 19 | HYThemeHeaderSmall.defaultProps = { 20 | 21 | } 22 | 23 | HYThemeHeaderSmall.propTypes = { 24 | title: PropTypes.string.isRequired, 25 | more: PropTypes.string 26 | } 27 | 28 | export default HYThemeHeaderSmall; 29 | -------------------------------------------------------------------------------- /src/components/theme-header-small/style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const HeaderWrapper = styled.div` 4 | display: flex; 5 | justify-content: space-between; 6 | align-items: flex-end; 7 | padding-bottom: 5px; 8 | border-bottom: 1px solid #ccc; 9 | 10 | h3 { 11 | font-size: 12px; 12 | } 13 | ` 14 | 15 | -------------------------------------------------------------------------------- /src/components/theme-header-song/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useSelector, shallowEqual } from "react-redux"; 3 | 4 | import { 5 | HeaderWrapper 6 | } from "./style" 7 | import { memo } from 'react'; 8 | 9 | export default memo(function HYThemeHeaderSong() { 10 | // redux 11 | const state = useSelector(state => ({ 12 | playList: state.getIn(["ranking", "playList"]) 13 | }), shallowEqual) 14 | 15 | return ( 16 | 17 |
    18 |

    歌曲列表

    19 |
    {state.playList.trackCount}首歌
    20 |
    21 |
    22 | 播放: 23 | {state.playList.playCount} 24 | 25 |
    26 |
    27 | ) 28 | }) 29 | -------------------------------------------------------------------------------- /src/components/theme-header-song/style.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const HeaderWrapper = styled.div` 4 | display: flex; 5 | justify-content: space-between; 6 | align-items: center; 7 | padding-bottom: 5px; 8 | border-bottom: 2px solid #c20c0c; 9 | 10 | .left { 11 | display: flex; 12 | align-items: flex-end; 13 | 14 | .title { 15 | font-size: 20px; 16 | font-family: "Microsoft Yahei", Arial, Helvetica, sans-serif; 17 | } 18 | 19 | .count { 20 | margin-bottom: 5px; 21 | margin-left: 20px; 22 | } 23 | } 24 | 25 | .right { 26 | .count { 27 | color: #c20c0c; 28 | } 29 | } 30 | ` 31 | -------------------------------------------------------------------------------- /src/components/top-ranking/index.js: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import { useDispatch } from 'react-redux'; 3 | 4 | import { getSizeImage } from '@/utils/format-utils'; 5 | import { getSongDetailAction } from '@/pages/player/store'; 6 | 7 | import { TopRankingWrapper } from './style'; 8 | 9 | export default memo(function HYTopRanking(props) { 10 | // props and state 11 | const { info } = props; 12 | const { tracks = [] } = info; 13 | 14 | // redux hooks 15 | const dispatch = useDispatch(); 16 | 17 | // other handle 18 | const playMusic = (item) => { 19 | dispatch(getSongDetailAction(item.id)); 20 | } 21 | 22 | return ( 23 | 24 |
    25 |
    26 | 27 | ranking 28 |
    29 |
    30 | {info.name} 31 |
    32 | 33 | 34 |
    35 |
    36 |
    37 |
    38 | { 39 | tracks.slice(0, 10).map((item, index) => { 40 | return ( 41 |
    42 |
    {index + 1}
    43 |
    44 | {item.name} 45 |
    46 | 48 | 49 | 50 |
    51 |
    52 |
    53 | ) 54 | }) 55 | } 56 |
    57 |
    58 | 查看全部 > 59 |
    60 |
    61 | ) 62 | }) 63 | -------------------------------------------------------------------------------- /src/components/top-ranking/style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const TopRankingWrapper = styled.div` 4 | flex: 1; 5 | 6 | .header { 7 | height: 100px; 8 | display: flex; 9 | 10 | margin: 20px 0 0 20px; 11 | 12 | .image { 13 | width: 80px; 14 | height: 80px; 15 | position: relative; 16 | 17 | img { 18 | width: 80px; 19 | height: 80px; 20 | } 21 | } 22 | 23 | .info { 24 | margin: 5px 0 0 10px; 25 | 26 | a { 27 | font-size: 14px; 28 | color: #333; 29 | font-weight: 700; 30 | } 31 | 32 | .btn { 33 | display: inline-block; 34 | text-indent: -9999px; 35 | width: 22px; 36 | height: 22px; 37 | margin: 8px 10px 0 0; 38 | cursor: pointer; 39 | } 40 | 41 | .play { 42 | background-position: -267px -205px; 43 | } 44 | 45 | .favor { 46 | background-position: -300px -205px; 47 | } 48 | } 49 | } 50 | 51 | .list { 52 | .list-item { 53 | position: relative; 54 | display: flex; 55 | align-items: center; 56 | height: 32px; 57 | 58 | :nth-child(-n+3) .rank { 59 | color: #c10d0c; 60 | } 61 | 62 | .rank { 63 | width: 35px; 64 | text-align: center; 65 | margin-left: 10px; 66 | font-size: 16px; 67 | } 68 | 69 | .info { 70 | color: #000; 71 | width: 170px; 72 | height: 17px; 73 | line-height: 17px; 74 | display: flex; 75 | justify-content: space-between; 76 | 77 | .name { 78 | flex: 1; 79 | } 80 | 81 | .operate { 82 | display: flex; 83 | align-items: center; 84 | display: none; 85 | width: 82px; 86 | 87 | .btn { 88 | width: 17px; 89 | height: 17px; 90 | margin-left: 8px; 91 | cursor: pointer; 92 | } 93 | 94 | .play { 95 | background-position: -267px -268px; 96 | } 97 | 98 | .addto { 99 | position: relative; 100 | top: 2px; 101 | background-position: 0 -700px; 102 | } 103 | 104 | .favor { 105 | background-position: -297px -268px; 106 | } 107 | } 108 | } 109 | 110 | 111 | 112 | &:hover { 113 | .operate { 114 | display: block; 115 | } 116 | } 117 | } 118 | } 119 | 120 | .footer { 121 | height: 32px; 122 | display: flex; 123 | align-items: center; 124 | margin-right: 32px; 125 | justify-content: flex-end; 126 | 127 | a { 128 | color: #000; 129 | } 130 | } 131 | ` -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import "./assets/css/base.css"; 4 | import App from './App'; 5 | 6 | 7 | ReactDOM.render( 8 | , 9 | document.getElementById('root') 10 | ); 11 | 12 | -------------------------------------------------------------------------------- /src/pages/discover/c-pages/album/c-cpns/hot-album/index.js: -------------------------------------------------------------------------------- 1 | import React, { memo, useEffect } from 'react'; 2 | import { useDispatch, useSelector, shallowEqual } from 'react-redux'; 3 | 4 | import { getHotAlbumsAction } from '../../store/actionCreators'; 5 | 6 | import HYAlbumCover from '@/components/album-cover'; 7 | import HYThemHeaderNormal from '@/components/theme-header-normal'; 8 | import { 9 | HotAlbumWrapper 10 | } from './style'; 11 | 12 | export default memo(function HYHotAlbum() { 13 | 14 | const { hotAlbums } = useSelector(state => ({ 15 | hotAlbums: state.getIn(["album", "hotAlbums"]) 16 | }), shallowEqual) 17 | const dispatch = useDispatch(); 18 | 19 | useEffect(() => { 20 | dispatch(getHotAlbumsAction()); 21 | }, [dispatch]); 22 | 23 | return ( 24 | 25 | 26 |
    27 | { 28 | hotAlbums.slice(0, 10).map((item, index) => { 29 | return 34 | }) 35 | } 36 |
    37 |
    38 | ) 39 | }); 40 | -------------------------------------------------------------------------------- /src/pages/discover/c-pages/album/c-cpns/hot-album/style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const HotAlbumWrapper = styled.div` 4 | .album-list { 5 | display: flex; 6 | flex-wrap: wrap; 7 | justify-content: space-between; 8 | } 9 | ` -------------------------------------------------------------------------------- /src/pages/discover/c-pages/album/c-cpns/top-album/index.js: -------------------------------------------------------------------------------- 1 | import React, { memo, useEffect, useState } from 'react'; 2 | import { useDispatch, useSelector, shallowEqual } from 'react-redux'; 3 | 4 | import { getTopAlbumsAction } from '../../store/actionCreators'; 5 | 6 | import HYThemeHeaderNormal from "@/components/theme-header-normal"; 7 | import HYAlbumCover from "@/components/album-cover"; 8 | import HYPagination from '@/components/pagination'; 9 | import { 10 | TopAlbumWrapper 11 | } from './style'; 12 | 13 | export default memo(function HYTopAlbum() { 14 | const [currentPage, setCurrentPage] = useState(1); 15 | 16 | const { topAlbums, total } = useSelector(state => ({ 17 | topAlbums: state.getIn(["album", "topAlbums"]), 18 | total: state.getIn(["album", "topTotal"]) 19 | }), shallowEqual); 20 | const dispatch = useDispatch(); 21 | 22 | useEffect(() => { 23 | dispatch(getTopAlbumsAction(1)); 24 | }, [dispatch]); 25 | 26 | 27 | const onPageChange = (page, pageSize) => { 28 | setCurrentPage(page); 29 | dispatch(getTopAlbumsAction(page)) 30 | } 31 | 32 | return ( 33 | 34 | 35 |
    36 | { 37 | topAlbums.map((item, index) => { 38 | return 43 | }) 44 | } 45 |
    46 | 50 |
    51 | ) 52 | }) 53 | -------------------------------------------------------------------------------- /src/pages/discover/c-pages/album/c-cpns/top-album/style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const TopAlbumWrapper = styled.div` 4 | margin-top: 30px; 5 | .album-list { 6 | display: flex; 7 | flex-wrap: wrap; 8 | justify-content: space-between; 9 | } 10 | ` -------------------------------------------------------------------------------- /src/pages/discover/c-pages/album/index.js: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | 3 | import HYHotAlbum from './c-cpns/hot-album'; 4 | import HYTopAlbum from './c-cpns/top-album'; 5 | import { 6 | AblumWrapper 7 | } from './style'; 8 | 9 | export default memo(function HYAlbum() { 10 | return ( 11 | 12 | 13 | 14 | 15 | ) 16 | }) 17 | -------------------------------------------------------------------------------- /src/pages/discover/c-pages/album/store/actionCreators.js: -------------------------------------------------------------------------------- 1 | import * as actionTypes from './constants'; 2 | 3 | import { 4 | getHotAlbums, 5 | getTopAlbums 6 | } from '@/services/album.js'; 7 | 8 | const changeHotAlbumsAction = (res) => ({ 9 | type: actionTypes.CHANGE_HOT_ALBUMS, 10 | hotAlbums: res.albums 11 | }) 12 | 13 | const changeTopAlbumAction = (res) => ({ 14 | type: actionTypes.CHANGE_TOP_ALBUMS, 15 | topAlbums: res.albums 16 | }) 17 | 18 | const changeTopTotalAction = (total) => ({ 19 | type: actionTypes.CHANGE_TOP_TOTAL, 20 | total: total 21 | }) 22 | 23 | export const getHotAlbumsAction = () => { 24 | return dispatch => { 25 | getHotAlbums().then(res => { 26 | dispatch(changeHotAlbumsAction(res)); 27 | }) 28 | } 29 | } 30 | 31 | export const getTopAlbumsAction = (page) => { 32 | return dispatch => { 33 | getTopAlbums(30, (page-1) * 30).then(res => { 34 | dispatch(changeTopAlbumAction(res)); 35 | dispatch(changeTopTotalAction(res.total)); 36 | 37 | console.log(res) 38 | }) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/pages/discover/c-pages/album/store/constants.js: -------------------------------------------------------------------------------- 1 | export const CHANGE_HOT_ALBUMS = "ablum/CHANGE_HOT_ALBUMS"; 2 | export const CHANGE_TOP_ALBUMS = "album/CHANGE_TOP_ALBUMS"; 3 | export const CHANGE_TOP_TOTAL = "album/CHANGE_TOP_TOTAL"; 4 | -------------------------------------------------------------------------------- /src/pages/discover/c-pages/album/store/index.js: -------------------------------------------------------------------------------- 1 | import reducer from './reducer'; 2 | 3 | export { 4 | reducer 5 | } -------------------------------------------------------------------------------- /src/pages/discover/c-pages/album/store/reducer.js: -------------------------------------------------------------------------------- 1 | import { Map } from 'immutable'; 2 | 3 | import * as actionTypes from './constants'; 4 | 5 | const defaultState = Map({ 6 | hotAlbums: [], 7 | topAlbums: [], 8 | topTotal: 0 9 | }) 10 | 11 | export default function reducer(state = defaultState, action) { 12 | switch (action.type) { 13 | case actionTypes.CHANGE_HOT_ALBUMS: 14 | return state.set("hotAlbums", action.hotAlbums); 15 | case actionTypes.CHANGE_TOP_ALBUMS: 16 | return state.set("topAlbums", action.topAlbums); 17 | case actionTypes.CHANGE_TOP_TOTAL: 18 | return state.set("topTotal", action.total); 19 | default: 20 | return state; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/pages/discover/c-pages/album/style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const AblumWrapper = styled.div` 4 | padding: 40px; 5 | background-color: #fff; 6 | border: 1px solid #d3d3d3; 7 | border-width: 0 1px; 8 | ` -------------------------------------------------------------------------------- /src/pages/discover/c-pages/artist/c-cpns/artist-category/index.js: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import { useDispatch, useSelector, shallowEqual } from 'react-redux'; 3 | import classNames from 'classnames'; 4 | 5 | import { artistCategories } from '@/services/local-data'; 6 | 7 | import { CategoryWrapper, CategoryItem } from './style'; 8 | import { changeCurrentAreaAction, changeCurrentTypeAction } from '../../store/actionCreators'; 9 | 10 | export default memo(function HYArtistCategory(props) { 11 | 12 | // redux hooks 13 | const {currentArea, currentType} = useSelector(state => ({ 14 | currentArea: state.getIn(["artist", "currentArea"]), 15 | currentType: state.getIn(["artist", "currentType"]) 16 | }), shallowEqual); 17 | const dispatch = useDispatch(); 18 | 19 | // handle function 20 | const selectArtist = (area, type) => { 21 | dispatch(changeCurrentAreaAction(area)); 22 | dispatch(changeCurrentTypeAction(type)); 23 | } 24 | 25 | // render jsx 26 | const renderArtist = (artists, area) => { 27 | return ( 28 |
    29 | { 30 | artists.map((item, index) => { 31 | const isSelect = currentArea === area && currentType.type === item.type; 32 | return ( 33 | 35 | selectArtist(area, item)}>{item.name} 36 | 37 | ) 38 | }) 39 | } 40 |
    41 | ) 42 | } 43 | 44 | return ( 45 | 46 | { 47 | artistCategories.map((item, index) => { 48 | return ( 49 |
    50 |

    {item.title}

    51 | {renderArtist(item.artists, item.area)} 52 |
    53 | ) 54 | }) 55 | } 56 |
    57 | ) 58 | }) 59 | -------------------------------------------------------------------------------- /src/pages/discover/c-pages/artist/c-cpns/artist-category/style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const CategoryWrapper = styled.div` 4 | width: 180px; 5 | padding: 50px 10px 40px; 6 | border: 1px solid #d3d3d3; 7 | border-width: 0 1px; 8 | 9 | background-color: #fafafa; 10 | 11 | .section { 12 | border-bottom: 1px solid #d3d3d3; 13 | padding: 10px 0; 14 | 15 | &:last-of-type { 16 | border-bottom: none; 17 | } 18 | 19 | .title { 20 | height: 25px; 21 | padding-left: 14px; 22 | font-size: 16px; 23 | margin-bottom: 5px; 24 | font-family: "Microsoft Yahei"; 25 | } 26 | } 27 | 28 | 29 | ` 30 | 31 | export const CategoryItem = styled.div` 32 | width: 160px; 33 | color: #333; 34 | height: 29px; 35 | line-height: 29px; 36 | margin-bottom: 2px; 37 | cursor: pointer; 38 | 39 | &.active { 40 | span { 41 | color: red; 42 | background-position: 0 0; 43 | } 44 | } 45 | 46 | span { 47 | display: inline-block; 48 | width: 160px; 49 | padding-left: 27px; 50 | background: url(${require("@/assets/img/singer_sprite.png")}) no-repeat 0 -30px; 51 | cursor: pointer; 52 | 53 | &:hover { 54 | text-decoration: underline; 55 | } 56 | ` -------------------------------------------------------------------------------- /src/pages/discover/c-pages/artist/c-cpns/artist-list/c-cpns/alpha-list/index.js: -------------------------------------------------------------------------------- 1 | import React, { memo, useState, useEffect } from 'react'; 2 | import { useSelector, shallowEqual, useDispatch } from 'react-redux'; 3 | import classNames from 'classnames'; 4 | 5 | import { singerAlphas } from '@/utils/handle-data'; 6 | import { getArtistListAction } from '../../../../store/actionCreators'; 7 | 8 | import { 9 | AlphaListWrapper 10 | } from './style'; 11 | 12 | export default memo(function HYAlphaList() { 13 | const [currentAlpha, setCurrentAlpha] = useState("-1"); 14 | 15 | const { currentType, currentArea } = useSelector(state => ({ 16 | currentType: state.getIn(["artist", "currentType"]), 17 | currentArea: state.getIn(["artist", "currentArea"]) 18 | }), shallowEqual); 19 | const dispatch = useDispatch(); 20 | 21 | useEffect(() => { 22 | setCurrentAlpha("-1"); 23 | }, [currentType, currentArea]); 24 | useEffect(() => { 25 | dispatch(getArtistListAction(currentArea, currentType.type, currentAlpha)); 26 | }, [currentAlpha, currentType, currentArea, dispatch]); 27 | 28 | return ( 29 | 30 | { 31 | currentArea !== -1 && singerAlphas.map((item, index) => { 32 | const isActive = currentAlpha === item; 33 | if (item === "0") item = "其他"; 34 | if (item === "-1") item = "热门"; 35 | return ( 36 |
    38 | setCurrentAlpha(item)}>{item.toUpperCase()} 39 |
    40 | ) 41 | }) 42 | } 43 |
    44 | ) 45 | }) 46 | -------------------------------------------------------------------------------- /src/pages/discover/c-pages/artist/c-cpns/artist-list/c-cpns/alpha-list/style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const AlphaListWrapper = styled.div` 4 | display: flex; 5 | justify-content: space-between; 6 | margin-top: ${props => props.hasTop ? "20px": 0}; 7 | 8 | .item { 9 | padding: 1px 4px; 10 | border-radius: 3px; 11 | span { 12 | font-size: 14px; 13 | color: #333; 14 | cursor: pointer; 15 | } 16 | 17 | span:hover { 18 | text-decoration: underline; 19 | } 20 | } 21 | 22 | .active { 23 | background-color: #c20c0c; 24 | span { 25 | color: #fff; 26 | } 27 | } 28 | ` -------------------------------------------------------------------------------- /src/pages/discover/c-pages/artist/c-cpns/artist-list/c-cpns/artist-item/index.js: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | 3 | import { getSizeImage } from '@/utils/format-utils'; 4 | 5 | import { ItemWrapper } from './style'; 6 | 7 | export default memo(function HYArtistItemV1(props) { 8 | const { info, index } = props; 9 | 10 | return ( 11 | 12 | { 13 | index < 10 && ( 14 |
    15 | 16 |
    17 | ) 18 | } 19 |
    20 | {info.name} 21 | 22 |
    23 |
    24 | ) 25 | }) 26 | -------------------------------------------------------------------------------- /src/pages/discover/c-pages/artist/c-cpns/artist-list/c-cpns/artist-item/style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const ItemWrapper = styled.div` 4 | width: 130px; 5 | margin-top: 15px; 6 | 7 | .image { 8 | img { 9 | width: 130px; 10 | height: 130px; 11 | } 12 | } 13 | 14 | .info { 15 | margin: 10px 0; 16 | display: flex; 17 | justify-content: space-between; 18 | 19 | .name { 20 | cursor: pointer; 21 | 22 | &:hover { 23 | color: red; 24 | text-decoration: underline; 25 | } 26 | } 27 | 28 | .icon { 29 | display: inline-block; 30 | width: 17px; 31 | height: 18px; 32 | background-position: 0 -740px; 33 | } 34 | } 35 | ` -------------------------------------------------------------------------------- /src/pages/discover/c-pages/artist/c-cpns/artist-list/index.js: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import { useSelector, shallowEqual } from 'react-redux'; 3 | 4 | 5 | import HYThemeHeaderNormal from '@/components/theme-header-normal'; 6 | import HYAlphaList from './c-cpns/alpha-list'; 7 | import HYArtistItem from './c-cpns/artist-item'; 8 | import { 9 | ArtistListWrapper 10 | } from './style'; 11 | 12 | export default memo(function HYArtistList() { 13 | // redux hooks 14 | const { currentType, artistList } = useSelector(state => ({ 15 | currentType: state.getIn(["artist", "currentType"]), 16 | artistList: state.getIn(["artist", "artistList"]) 17 | }), shallowEqual); 18 | 19 | return ( 20 | 21 | 22 | 23 |
    24 | { 25 | artistList.map((item, index) => { 26 | return 27 | }) 28 | } 29 |
    30 |
    31 | ) 32 | }) 33 | -------------------------------------------------------------------------------- /src/pages/discover/c-pages/artist/c-cpns/artist-list/style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const ArtistListWrapper = styled.div` 4 | flex: 1; 5 | padding: 40px; 6 | 7 | .artist-list { 8 | display: flex; 9 | flex-wrap: wrap; 10 | justify-content: space-between; 11 | 12 | padding: 5px 0 40px; 13 | } 14 | ` -------------------------------------------------------------------------------- /src/pages/discover/c-pages/artist/index.js: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | 3 | import HYArtistCategory from './c-cpns/artist-category'; 4 | import HYArtistList from './c-cpns/artist-list'; 5 | import { HYArtistWrapper } from './style'; 6 | 7 | export default memo(function HYArtist() { 8 | return ( 9 | 10 |
    11 | 12 | 13 |
    14 |
    15 | ) 16 | }) 17 | -------------------------------------------------------------------------------- /src/pages/discover/c-pages/artist/store/actionCreators.js: -------------------------------------------------------------------------------- 1 | import * as actionTypes from './constants'; 2 | 3 | import { getArtistList } from '@/services/artist'; 4 | 5 | const changeArtistListAction = (artistList) => ({ 6 | type: actionTypes.CHANGE_ARTIST_LIST, 7 | artistList 8 | }) 9 | 10 | export const changeCurrentAreaAction = (area) => ({ 11 | type: actionTypes.CHANGE_CURRENT_AREA, 12 | currentArea: area 13 | }); 14 | 15 | export const changeCurrentTypeAction = (type) => ({ 16 | type: actionTypes.CHANGE_CURRENT_TYPE, 17 | currentType: type 18 | }); 19 | 20 | export const getArtistListAction = (area, type, alpha) => { 21 | return dispatch => { 22 | getArtistList(area, type, alpha).then(res => { 23 | console.log(res); 24 | dispatch(changeArtistListAction(res.artists)) 25 | }) 26 | } 27 | } -------------------------------------------------------------------------------- /src/pages/discover/c-pages/artist/store/constants.js: -------------------------------------------------------------------------------- 1 | export const CHANGE_CURRENT_AREA = "artist/CHANGE_CURRENT_AREA"; 2 | export const CHANGE_CURRENT_TYPE = "artist/CHANGE_CURRENT_TYPE"; 3 | 4 | export const CHANGE_ARTIST_LIST = "artist/CHANGE_ARTIST_LIST"; 5 | -------------------------------------------------------------------------------- /src/pages/discover/c-pages/artist/store/index.js: -------------------------------------------------------------------------------- 1 | import reducer from './reducer'; 2 | 3 | export { 4 | reducer 5 | } -------------------------------------------------------------------------------- /src/pages/discover/c-pages/artist/store/reducer.js: -------------------------------------------------------------------------------- 1 | import { Map } from 'immutable'; 2 | 3 | import * as actionTypes from './constants'; 4 | 5 | const defaultState = Map({ 6 | currentArea: 7, 7 | currentType: { 8 | name: "推荐歌手", 9 | type: 1 10 | }, 11 | artistList: [] 12 | }); 13 | 14 | function reducer(state = defaultState, action) { 15 | switch(action.type) { 16 | case actionTypes.CHANGE_CURRENT_AREA: 17 | return state.set("currentArea", action.currentArea); 18 | case actionTypes.CHANGE_CURRENT_TYPE: 19 | return state.set("currentType", action.currentType); 20 | case actionTypes.CHANGE_ARTIST_LIST: 21 | return state.set("artistList", action.artistList); 22 | default: 23 | return state; 24 | } 25 | } 26 | 27 | export default reducer; 28 | -------------------------------------------------------------------------------- /src/pages/discover/c-pages/artist/style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const HYArtistWrapper = styled.div` 4 | .content { 5 | display: flex; 6 | background-color: #fff; 7 | } 8 | ` -------------------------------------------------------------------------------- /src/pages/discover/c-pages/djradio/c-cpns/radio-category/index.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, memo } from 'react'; 2 | import { useSelector, useDispatch, shallowEqual } from 'react-redux'; 3 | import classnames from 'classnames'; 4 | 5 | import { 6 | getRadioCategories, 7 | changeCurrentIdActio 8 | } from "../../store/actionCreators"; 9 | 10 | import { Carousel } from 'antd'; 11 | import { 12 | CategoryWrapper, 13 | CategoryContent, 14 | CategoryItemImage 15 | } from "./style"; 16 | 17 | const PAGE_SIZE = 16; 18 | 19 | export default memo(function HYRadioCategory() { 20 | // redux 21 | const dispatch = useDispatch(); 22 | const { categories, currentId } = useSelector(state => ({ 23 | categories: state.getIn(["djradio", "categories"]), 24 | currentId: state.getIn(["djradio", "currentId"]) 25 | }), shallowEqual); 26 | 27 | // data handle 28 | const page = Math.ceil(categories.length / PAGE_SIZE) || 1; 29 | 30 | // hooks 31 | useEffect(() => { 32 | dispatch(getRadioCategories()); 33 | }, [dispatch]); 34 | const carouselRef = useRef(); 35 | 36 | // handle function 37 | function getSize(index) { 38 | return index * PAGE_SIZE > categories.length ? index * PAGE_SIZE : categories.length; 39 | } 40 | 41 | return ( 42 | 43 |
    carouselRef.current.prev()}>
    44 | 45 | 46 | { 47 | Array(page).fill(0).map((_, index) => { 48 | return ( 49 |
    50 | { 51 | categories.slice(index * PAGE_SIZE, getSize(index + 1)).map((item, indey) => { 52 | return ( 53 |
    dispatch(changeCurrentIdActio(item.id))} 55 | className={classnames("category-item", {"active": currentId === item.id})}> 56 | 57 | {item.name} 58 |
    59 | ) 60 | }) 61 | } 62 |
    63 | ) 64 | }) 65 | } 66 |
    67 |
    68 |
    carouselRef.current.next()}>
    69 |
    70 | ) 71 | }) 72 | -------------------------------------------------------------------------------- /src/pages/discover/c-pages/djradio/c-cpns/radio-category/style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const CategoryWrapper = styled.div` 4 | display: flex; 5 | align-items: center; 6 | margin: 0 -40px; 7 | 8 | .arrow { 9 | width: 20px; 10 | height: 30px; 11 | background-image: url(${require("@/assets/img/radio_slide.png")}); 12 | cursor: pointer; 13 | } 14 | 15 | .arrow-left { 16 | margin-left: 15px; 17 | background-position: 0 -30px; 18 | } 19 | 20 | .arrow-right { 21 | margin-right: 15px; 22 | background-position: -30px -30px; 23 | } 24 | ` 25 | 26 | export const CategoryContent = styled.div` 27 | flex: 1; 28 | width: 900px; 29 | 30 | .category-page { 31 | display: flex !important; 32 | flex-wrap: wrap; 33 | padding-bottom: 20px; 34 | 35 | .category-item { 36 | display: flex; 37 | flex-direction: column; 38 | align-items: center; 39 | margin: 10px; 40 | width: 70px; 41 | height: 70px; 42 | font-size: 12px; 43 | cursor: pointer; 44 | border-radius: 5px; 45 | text-align: center; 46 | border: 2px solid transparent; 47 | 48 | :hover { 49 | background-color: #eee; 50 | } 51 | 52 | &.active { 53 | color: #C20C0C; 54 | border: 2px solid #d35757; 55 | 56 | .image { 57 | background-position: -48px 0; 58 | } 59 | } 60 | } 61 | } 62 | 63 | .dots { 64 | bottom: 5px; 65 | li { 66 | width: 20px; 67 | height: 20px; 68 | display: flex; 69 | align-items: center; 70 | 71 | button { 72 | width: 6px; 73 | height: 6px; 74 | border-radius: 50%; 75 | background-color: #aaa; 76 | } 77 | } 78 | 79 | li.slick-active { 80 | width: 20px; 81 | button { 82 | background-color: #C20C0C; 83 | } 84 | } 85 | } 86 | ` 87 | 88 | export const CategoryItemImage = styled.div` 89 | width: 48px; 90 | height: 48px; 91 | background-image: url(${props => props.imgUrl}); 92 | ` -------------------------------------------------------------------------------- /src/pages/discover/c-pages/djradio/c-cpns/radio-ranking/index.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, memo } from 'react'; 2 | import { useSelector, useDispatch, shallowEqual } from 'react-redux' 3 | 4 | import { 5 | getRadios 6 | } from "../../store/actionCreators"; 7 | 8 | import HYThemeHeaderNormal from '@/components/theme-header-normal'; 9 | import HYRadioRankingCover from '@/components/radio-ranking-cover'; 10 | import HYPagination from '@/components/pagination'; 11 | import { 12 | RankingWraper 13 | } from "./style"; 14 | import { useState } from 'react'; 15 | 16 | export default memo(function HYRadioRanking() { 17 | // state 18 | const [currentPage, setCurrentPage] = useState(1); 19 | 20 | // redux 21 | const { currentId, radios } = useSelector(state => ({ 22 | currentId: state.getIn(["djradio", "currentId"]), 23 | radios: state.getIn(["djradio", "radios"]) 24 | }), shallowEqual) 25 | const dispatch = useDispatch(); 26 | 27 | // hooks 28 | useEffect(() => { 29 | if (currentId === 0) return; 30 | dispatch(getRadios(currentId, 0)) 31 | }, [dispatch, currentId]); 32 | 33 | // hanlde function 34 | const onPageChange = (page, pageSize) => { 35 | setCurrentPage(page); 36 | dispatch(getRadios(currentId, page * 30)); 37 | } 38 | 39 | return ( 40 | 41 | 42 |
    43 | { 44 | radios.map((item, index) => { 45 | return () 46 | }) 47 | } 48 |
    49 | 53 |
    54 | ) 55 | }) 56 | -------------------------------------------------------------------------------- /src/pages/discover/c-pages/djradio/c-cpns/radio-ranking/style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const RankingWraper = styled.div` 4 | .ranking-list { 5 | display: flex; 6 | justify-content: space-between; 7 | flex-wrap: wrap; 8 | } 9 | ` -------------------------------------------------------------------------------- /src/pages/discover/c-pages/djradio/c-cpns/radio-recommend/index.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, memo } from 'react'; 2 | import { useSelector, useDispatch, shallowEqual } from 'react-redux'; 3 | 4 | import { 5 | getRadioRecommend 6 | } from "../../store/actionCreators"; 7 | 8 | import HYThemeHeaderNormal from '@/components/theme-header-normal'; 9 | import HYRadioRecomendCover from '@/components/radio-recommend-cover'; 10 | import { 11 | RecommendWrapper 12 | } from "./style"; 13 | 14 | export default memo(function HYRadioRecommend() { 15 | // redux 16 | const { currentId, recommends } = useSelector(state => ({ 17 | currentId: state.getIn(["djradio", "currentId"]), 18 | recommends: state.getIn(["djradio", "recommends"]) 19 | }), shallowEqual); 20 | const dispatch = useDispatch(); 21 | 22 | // hooks 23 | useEffect(() => { 24 | if (currentId === 0) return; 25 | dispatch(getRadioRecommend(currentId)); 26 | }, [dispatch, currentId]) 27 | 28 | return ( 29 | 30 | 31 |
    32 | { 33 | recommends.slice(0, 5).map((item) => { 34 | return () 35 | }) 36 | } 37 |
    38 |
    39 | ) 40 | }) 41 | -------------------------------------------------------------------------------- /src/pages/discover/c-pages/djradio/c-cpns/radio-recommend/style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const RecommendWrapper = styled.div` 4 | .radio-list { 5 | margin: 20px 0 40px; 6 | display: flex; 7 | justify-content: space-between; 8 | } 9 | ` -------------------------------------------------------------------------------- /src/pages/discover/c-pages/djradio/index.js: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | 3 | import HYRadioCategory from './c-cpns/radio-category'; 4 | import HYRadioRecommend from './c-cpns/radio-recommend'; 5 | import HYRadioRanking from './c-cpns/radio-ranking'; 6 | import { 7 | DjRadioWrapper 8 | } from "./style"; 9 | 10 | export default memo(function HYDjradio() { 11 | return ( 12 | 13 | 14 | 15 | 16 | 17 | ) 18 | }) 19 | -------------------------------------------------------------------------------- /src/pages/discover/c-pages/djradio/store/actionCreators.js: -------------------------------------------------------------------------------- 1 | import { 2 | getDjRadioCatelist, 3 | getDjRadioRecommend, 4 | getDjRadios 5 | } from "@/services/djradio"; 6 | import * as actionTypes from './constants'; 7 | 8 | const changeCategoryAction = (res) => ({ 9 | type: actionTypes.CHANGE_RADIO_CATEGORY, 10 | categories: res.categories 11 | }) 12 | 13 | const changeRecommendsAction = (res) => ({ 14 | type: actionTypes.CHANGE_RECOMMENDS, 15 | recommends: res.djRadios 16 | }) 17 | 18 | const changeRadiosAction = (res) => ({ 19 | type: actionTypes.CHANGE_RADIOS, 20 | radios: res.djRadios 21 | }) 22 | 23 | export const changeCurrentIdActio = (id) => ({ 24 | type: actionTypes.CHANGE_CURRENT_ID, 25 | currentId: id 26 | }) 27 | 28 | export const getRadioCategories = () => { 29 | return dispatch => { 30 | getDjRadioCatelist().then(res => { 31 | dispatch(changeCategoryAction(res)); 32 | const currentId = res.categories[0].id; 33 | dispatch(changeCurrentIdActio(currentId)); 34 | }) 35 | } 36 | } 37 | 38 | export const getRadioRecommend = (currentId) => { 39 | return dispatch => { 40 | getDjRadioRecommend(currentId).then(res => { 41 | dispatch(changeRecommendsAction(res)); 42 | }) 43 | } 44 | } 45 | 46 | export const getRadios = (currentId, offset) => { 47 | return dispatch => { 48 | getDjRadios(currentId, 30, offset).then(res => { 49 | dispatch(changeRadiosAction(res)); 50 | }) 51 | } 52 | } -------------------------------------------------------------------------------- /src/pages/discover/c-pages/djradio/store/constants.js: -------------------------------------------------------------------------------- 1 | export const CHANGE_RADIO_CATEGORY = "djradio/CHANGE_RADIO_CATEGORY"; 2 | export const CHANGE_CURRENT_ID = "djradio/CHANGE_CURRENT_ID"; 3 | export const CHANGE_RECOMMENDS = "djradio/CHANGE_RECOMMENDS"; 4 | export const CHANGE_RADIOS = "djradio/CHANGE_RADIOS"; 5 | -------------------------------------------------------------------------------- /src/pages/discover/c-pages/djradio/store/index.js: -------------------------------------------------------------------------------- 1 | import reducer from './reducer'; 2 | 3 | export { 4 | reducer 5 | } 6 | -------------------------------------------------------------------------------- /src/pages/discover/c-pages/djradio/store/reducer.js: -------------------------------------------------------------------------------- 1 | import { Map } from "immutable"; 2 | import * as actionTypes from './constants'; 3 | 4 | const defaultState = Map({ 5 | categories: [], 6 | currentId: 0, 7 | recommends: [], 8 | radios: [] 9 | }) 10 | 11 | export default (state = defaultState, action) => { 12 | switch(action.type) { 13 | case actionTypes.CHANGE_RADIO_CATEGORY: 14 | return state.set("categories", action.categories); 15 | case actionTypes.CHANGE_CURRENT_ID: 16 | return state.set("currentId", action.currentId); 17 | case actionTypes.CHANGE_RECOMMENDS: 18 | return state.set("recommends", action.recommends); 19 | case actionTypes.CHANGE_RADIOS: 20 | return state.set("radios", action.radios); 21 | default: 22 | return state; 23 | } 24 | } -------------------------------------------------------------------------------- /src/pages/discover/c-pages/djradio/style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const DjRadioWrapper = styled.div` 4 | border: 1px solid #d3d3d3; 5 | border-width: 0 1px; 6 | background-color: #fff; 7 | padding: 40px; 8 | ` -------------------------------------------------------------------------------- /src/pages/discover/c-pages/ranking/c-cpns/ranking-header/index.js: -------------------------------------------------------------------------------- 1 | import React, {memo} from 'react'; 2 | import { useSelector, shallowEqual } from "react-redux"; 3 | 4 | import { formatMonthDay } from "@/utils/format-utils"; 5 | 6 | import HYSongOperationBar from '@/components/song-operation-bar'; 7 | import { 8 | RankingHeaderWrapper 9 | } from './style'; 10 | 11 | export default memo(function HYRankingHeader() { 12 | // redux 13 | const state = useSelector(state => ({ 14 | playList: state.getIn(["ranking", "playList"]), 15 | }), shallowEqual); 16 | const topInfo = state.playList; 17 | 18 | return ( 19 | 20 |
    21 | 22 | 封面 23 |
    24 |
    25 |
    {topInfo.name}
    26 |
    27 | 28 |
    最近更新:{formatMonthDay(topInfo.updateTime)}
    29 |
    ({"每日更新:TODO"})
    30 |
    31 | 35 |
    36 |
    37 | ) 38 | }) 39 | -------------------------------------------------------------------------------- /src/pages/discover/c-pages/ranking/c-cpns/ranking-header/style.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const RankingHeaderWrapper = styled.div` 4 | display: flex; 5 | align-items: center; 6 | padding: 40px; 7 | 8 | .image { 9 | padding: 3px; 10 | border: 1px solid #ccc; 11 | position: relative; 12 | img { 13 | width: 150px; 14 | height: 150px; 15 | } 16 | 17 | .image_cover { 18 | background-position: -230px -380px; 19 | } 20 | } 21 | 22 | .info { 23 | margin-left: 30px; 24 | .title { 25 | color: #333; 26 | font-size: 20px; 27 | font-family: "Microsoft Yahei", Arial, Helvetica, sans-serif; 28 | } 29 | 30 | .time { 31 | display: flex; 32 | align-items: center; 33 | color: #666; 34 | margin: 8px 0 30px; 35 | 36 | .clock { 37 | display: inline-block; 38 | width: 13px; 39 | height: 13px; 40 | background-position: -18px -682px; 41 | position: relative; 42 | top: -2px; 43 | margin-right: 3px; 44 | } 45 | 46 | .update-f { 47 | color: #999; 48 | } 49 | } 50 | } 51 | ` -------------------------------------------------------------------------------- /src/pages/discover/c-pages/ranking/c-cpns/ranking-list/index.js: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import { useSelector, shallowEqual } from "react-redux"; 3 | 4 | import { 5 | getSizeImage, 6 | formatMinuteSecond 7 | } from "@/utils/format-utils.js" 8 | 9 | import HYThemeHeaderSong from '@/components/theme-header-song'; 10 | import { 11 | RankingListWrapper 12 | } from './style'; 13 | 14 | export default memo(function HYRankingList() { 15 | const state = useSelector(state => ({ 16 | playList: state.getIn(["ranking", "playList"]) 17 | }), shallowEqual); 18 | const tracks = state.playList.tracks || []; 19 | 20 | return ( 21 | 22 | 23 |
    24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | { 35 | tracks.map((item, index) => { 36 | return ( 37 | 38 | 44 | 54 | 55 | 56 | 57 | ) 58 | }) 59 | } 60 | 61 |
    标题时长歌手
    39 |
    40 | {index + 1} 41 | 42 |
    43 |
    45 |
    46 | { 47 | index < 3 ? 48 | () : null 49 | } 50 | 51 | {item.name} 52 |
    53 |
    {formatMinuteSecond(item.dt)}{item.ar[0].name}
    62 |
    63 |
    64 | ) 65 | }) 66 | 67 | -------------------------------------------------------------------------------- /src/pages/discover/c-pages/ranking/c-cpns/ranking-list/style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const RankingListWrapper = styled.div` 4 | padding: 0 40px; 5 | 6 | .play-list { 7 | table { 8 | width: 100%; 9 | border: 1px solid #d9d9d9; 10 | 11 | thead { 12 | th { 13 | height: 34px; 14 | line-height: 34px; 15 | background-image: url(${require("@/assets/img/sprite_table.png")}); 16 | color: #666; 17 | border: 1px solid #ddd; 18 | border-width: 0 0 1px 1px; 19 | padding-left: 10px; 20 | } 21 | 22 | .ranking { 23 | width: 78px; 24 | border-left: none; 25 | } 26 | 27 | .duration { 28 | width: 91px; 29 | } 30 | 31 | .singer { 32 | width: 173px; 33 | } 34 | } 35 | 36 | tbody { 37 | td { 38 | padding: 6px 10px; 39 | } 40 | 41 | tr:nth-child(2n) { 42 | background-color: #fff; 43 | } 44 | 45 | tr:nth-child(2n+1) { 46 | background-color: #f7f7f7; 47 | } 48 | 49 | .rank-num { 50 | display: flex; 51 | 52 | .num { 53 | width: 25px; 54 | height: 18px; 55 | text-align: center; 56 | color: #999; 57 | } 58 | 59 | .new { 60 | width: 16px; 61 | height: 17px; 62 | margin-left: 12px; 63 | background-position: -67px -283px; 64 | } 65 | } 66 | 67 | .song-name { 68 | display: flex; 69 | align-items: center; 70 | img { 71 | width: 50px; 72 | height: 50px; 73 | margin-right: 10px; 74 | } 75 | 76 | .play { 77 | width: 17px; 78 | height: 17px; 79 | background-position: 0 -103px; 80 | } 81 | 82 | .name { 83 | margin-left: 10px; 84 | } 85 | } 86 | } 87 | } 88 | } 89 | ` -------------------------------------------------------------------------------- /src/pages/discover/c-pages/ranking/c-cpns/top-ranking/index.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, memo } from 'react'; 2 | import classNames from "classnames"; 3 | import { useSelector, useDispatch, shallowEqual } from "react-redux"; 4 | 5 | import { getSizeImage } from "@/utils/format-utils"; 6 | import { 7 | changeCurrentIndex, 8 | getRanking 9 | } from "../../store/actionCreators" 10 | 11 | import { 12 | TopRankingWrapper 13 | } from "./style"; 14 | 15 | export default memo(function HYTopRanking() { 16 | // redux 17 | const state = useSelector(state => ({ 18 | topList: state.getIn(["ranking", "topList"]), 19 | currentIndex: state.getIn(["ranking", "currentIndex"]) 20 | }), shallowEqual); 21 | const currentIndex = state.currentIndex; 22 | const dispatch = useDispatch(); 23 | 24 | // hooks 25 | useEffect(() => { 26 | const id = (state.topList[currentIndex] && state.topList[currentIndex].id); 27 | if (!id) return; 28 | dispatch(getRanking(id)) 29 | }, [state, dispatch, currentIndex]) 30 | 31 | // handle function 32 | const hanldeItemClick = (index) => { 33 | dispatch(changeCurrentIndex(index)); 34 | const id = state.topList[currentIndex].id; 35 | dispatch(getRanking(id)) 36 | } 37 | 38 | return ( 39 | 40 | { 41 | state.topList.map((item, index) => { 42 | let header; 43 | if (index === 0 || index === 4) { 44 | header =
    {index === 0 ? "云音乐特色榜" : "全球媒体榜"}
    45 | } 46 | return ( 47 |
    48 | {header} 49 |
    hanldeItemClick(index)}> 51 | 52 |
    53 |
    {item.name}
    54 |
    {item.updateFrequency}
    55 |
    56 |
    57 |
    58 | ) 59 | }) 60 | } 61 |
    62 | ) 63 | }) 64 | -------------------------------------------------------------------------------- /src/pages/discover/c-pages/ranking/c-cpns/top-ranking/style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const TopRankingWrapper = styled.div` 4 | padding: 25px 0; 5 | 6 | .header { 7 | padding: 12px 12px 10px; 8 | font-size: 14px; 9 | color: #000; 10 | font-family: simsun; 11 | } 12 | 13 | .item { 14 | height: 62px; 15 | padding-left: 20px; 16 | display: flex; 17 | align-items: center; 18 | cursor: pointer; 19 | 20 | &:hover, &.active { 21 | background-color: #e6e6e6; 22 | } 23 | 24 | img { 25 | width: 40px; 26 | height: 40px; 27 | } 28 | 29 | .info { 30 | margin-left: 10px; 31 | 32 | .name { 33 | color: #000; 34 | } 35 | 36 | .update { 37 | margin-top: 5px; 38 | color: #999; 39 | } 40 | } 41 | } 42 | ` -------------------------------------------------------------------------------- /src/pages/discover/c-pages/ranking/index.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, memo } from 'react'; 2 | import { useDispatch } from "react-redux"; 3 | 4 | import { getTops } from "./store/actionCreators"; 5 | 6 | import HYTopRanking from "./c-cpns/top-ranking"; 7 | import HYRankingHeader from './c-cpns/ranking-header'; 8 | import HYRankingList from './c-cpns/ranking-list'; 9 | import { 10 | RankingWrapper, 11 | RankingLeft, 12 | RankingRight, 13 | } from "./style"; 14 | 15 | export default memo(function HYRanking() { 16 | // redux 17 | const dispatch = useDispatch(); 18 | 19 | // hooks 20 | useEffect(() => { 21 | dispatch(getTops()); 22 | }, [dispatch]) 23 | 24 | return ( 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | ) 35 | }) 36 | -------------------------------------------------------------------------------- /src/pages/discover/c-pages/ranking/store/actionCreators.js: -------------------------------------------------------------------------------- 1 | import * as actionTypes from "./constants"; 2 | 3 | import { 4 | getTopList, 5 | getRankingList 6 | } from "@/services/ranking"; 7 | 8 | 9 | const changeTopListAction = (res) => ({ 10 | type: actionTypes.CHANGE_TOP_LIST, 11 | topList: res.list 12 | }) 13 | 14 | const changePlayListAction = (res) => ({ 15 | type: actionTypes.CHANGE_PLAY_LIST, 16 | playList: res.playlist 17 | }) 18 | 19 | export const changeCurrentIndex = (index) => ({ 20 | type: actionTypes.CHANGE_CURRENT_INDEX, 21 | currentIndex: index 22 | }) 23 | 24 | export const getTops = () => { 25 | return dispatch => { 26 | getTopList().then(res => { 27 | dispatch(changeTopListAction(res)); 28 | }) 29 | } 30 | } 31 | 32 | export const getRanking = (id) => { 33 | return dispatch => { 34 | getRankingList(id).then(res => { 35 | dispatch(changePlayListAction(res)) 36 | }) 37 | } 38 | } 39 | 40 | -------------------------------------------------------------------------------- /src/pages/discover/c-pages/ranking/store/constants.js: -------------------------------------------------------------------------------- 1 | export const CHANGE_TOP_LIST = "ranking/CHANGE_TOP_LIST"; 2 | export const CHANGE_CURRENT_INDEX = "rangking/CHANGE_CURRENT_INDEX"; 3 | export const CHANGE_PLAY_LIST = "ranking/CHANGE_PLAY_LIST"; 4 | -------------------------------------------------------------------------------- /src/pages/discover/c-pages/ranking/store/index.js: -------------------------------------------------------------------------------- 1 | import reducer from "./reducer"; 2 | 3 | export { 4 | reducer 5 | } 6 | -------------------------------------------------------------------------------- /src/pages/discover/c-pages/ranking/store/reducer.js: -------------------------------------------------------------------------------- 1 | import { Map } from "immutable"; 2 | import * as actionTypes from './constants'; 3 | 4 | const defaultState = Map({ 5 | topList: [], 6 | currentIndex: 0, 7 | playList: {} 8 | }) 9 | 10 | export default (state = defaultState, action) => { 11 | switch(action.type) { 12 | case actionTypes.CHANGE_TOP_LIST: 13 | return state.set("topList", action.topList); 14 | case actionTypes.CHANGE_CURRENT_INDEX: 15 | return state.set("currentIndex", action.currentIndex); 16 | case actionTypes.CHANGE_PLAY_LIST: 17 | return state.set("playList", action.playList); 18 | default: 19 | return state; 20 | } 21 | } -------------------------------------------------------------------------------- /src/pages/discover/c-pages/ranking/style.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const RankingWrapper = styled.div` 4 | display: flex; 5 | ` 6 | 7 | export const RankingLeft = styled.div` 8 | width: 240px; 9 | border: 1px solid #d3d3d3; 10 | border-width: 0 1px; 11 | ` 12 | 13 | export const RankingRight = styled.div` 14 | width: 740px; 15 | border-right: 1px solid #d3d3d3; 16 | background-color: #fafafa; 17 | ` -------------------------------------------------------------------------------- /src/pages/discover/c-pages/recommend/c-cpns/hot-radio/index.js: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | 3 | import { 4 | hotRadios 5 | } from "@/services/local-data"; 6 | 7 | import HYThemeHeaderSmall from '@/components/theme-header-small'; 8 | import { 9 | HotRadioWrapper 10 | } from './style'; 11 | 12 | export default memo(function HYHotRadio() { 13 | return ( 14 | 15 | 16 |
    17 | { 18 | hotRadios.map((item, index) => { 19 | return ( 20 |
    21 | 22 | 23 | 24 |
    25 |
    {item.name}
    26 |
    {item.position}
    27 |
    28 |
    29 | ) 30 | }) 31 | } 32 |
    33 |
    34 | ) 35 | }) 36 | -------------------------------------------------------------------------------- /src/pages/discover/c-pages/recommend/c-cpns/hot-radio/style.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const HotRadioWrapper = styled.div` 4 | padding: 20px; 5 | 6 | .radio-list { 7 | margin-top: 20px; 8 | 9 | .item { 10 | display: flex; 11 | margin-bottom: 10px; 12 | width: 210px; 13 | .image { 14 | img { 15 | width: 40px; 16 | height: 40px; 17 | } 18 | } 19 | 20 | .info { 21 | width: 160px; 22 | margin-left: 8px; 23 | .name { 24 | color: #000; 25 | font-weight: 700; 26 | margin-top: 3px; 27 | } 28 | 29 | .position { 30 | color: #666; 31 | } 32 | } 33 | } 34 | } 35 | ` 36 | 37 | -------------------------------------------------------------------------------- /src/pages/discover/c-pages/recommend/c-cpns/hot-recommend/index.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, memo, useCallback } from 'react'; 2 | import { useDispatch, useSelector, shallowEqual } from "react-redux"; 3 | import { useHistory } from 'react-router-dom'; 4 | 5 | import { 6 | getRecommend 7 | } from "../../store/actionCreators"; 8 | 9 | import { 10 | RecommendWrapper 11 | } from "./style"; 12 | 13 | import HYThemeHeaderRCM from '@/components/theme-header-rcm'; 14 | import HYThemeCover from '@/components/theme-cover'; 15 | 16 | export default memo(function HYHotRecommend() { 17 | // redux 18 | const state = useSelector(state => ({ 19 | recommends: state.getIn(["recommend", "hotRecommends"]) 20 | }), shallowEqual); 21 | const dispatch = useDispatch(); 22 | const history = useHistory(); 23 | 24 | useEffect(() => { 25 | dispatch(getRecommend()) 26 | }, [dispatch]); 27 | 28 | const keywordClick = useCallback((keyword) => { 29 | history.push({pathname: "/discover/songs", cat: keyword}); 30 | }, [history]); 31 | 32 | return ( 33 | 34 | 38 |
    39 | { 40 | state.recommends.slice(0, 8).map((item, index) => { 41 | return ( 42 | 43 | ) 44 | }) 45 | } 46 |
    47 |
    48 | ) 49 | }) 50 | -------------------------------------------------------------------------------- /src/pages/discover/c-pages/recommend/c-cpns/hot-recommend/style.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const RecommendWrapper = styled.div` 4 | .recommend-list { 5 | display: flex; 6 | flex-wrap: wrap; 7 | justify-content: space-between; 8 | } 9 | ` -------------------------------------------------------------------------------- /src/pages/discover/c-pages/recommend/c-cpns/new-album/index.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, memo } from 'react'; 2 | import { useSelector, useDispatch, shallowEqual } from 'react-redux'; 3 | 4 | import { 5 | getAlbum 6 | } from "../../store/actionCreators"; 7 | 8 | import { Carousel } from 'antd'; 9 | import HYThemeHeaderRCM from '@/components/theme-header-rcm'; 10 | import HYAlbumCover from "@/components/album-cover"; 11 | import { 12 | AlbumWrapper 13 | } from "./style"; 14 | 15 | export default memo(function HYNewAlbum(props) { 16 | // redux 17 | const state = useSelector(state => ({ 18 | newAlbum: state.getIn(["recommend", "newAlbum"]) 19 | }), shallowEqual); 20 | const dispatch = useDispatch(); 21 | 22 | // hooks 23 | const carouselRef = useRef(); 24 | useEffect(() => { 25 | dispatch(getAlbum()); 26 | }, [dispatch]); 27 | 28 | return ( 29 | 30 | 31 |
    32 |
    carouselRef.current.prev()}>
    34 |
    35 | 36 | { 37 | [0, 1].map(item => { 38 | return ( 39 |
    40 | { 41 | state.newAlbum.slice(item*5, (item+1)*5).map(item => { 42 | return ( 43 | 44 | ) 45 | }) 46 | } 47 |
    48 | ) 49 | }) 50 | } 51 |
    52 |
    53 |
    carouselRef.current.next()}>
    55 |
    56 |
    57 | ) 58 | }) 59 | -------------------------------------------------------------------------------- /src/pages/discover/c-pages/recommend/c-cpns/new-album/style.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const AlbumWrapper = styled.div` 4 | margin-top: 50px; 5 | 6 | .content { 7 | height: 186px; 8 | background-color: #f5f5f5; 9 | border: 1px solid #d3d3d3; 10 | margin: 20px 0 37px; 11 | display: flex; 12 | align-items: center; 13 | 14 | .arrow { 15 | width: 30px; 16 | height: 17px; 17 | cursor: pointer; 18 | } 19 | 20 | .arrow-left { 21 | background-position: -260px -75px; 22 | } 23 | 24 | .arrow-right { 25 | background-position: -300px -75px; 26 | } 27 | 28 | .album { 29 | width: 640px; 30 | height: 150px; 31 | 32 | .ant-carousel .slick-slide { 33 | height: 150px; 34 | overflow: hidden; 35 | } 36 | 37 | .page { 38 | display: flex !important; 39 | justify-content: space-between; 40 | align-items: center; 41 | } 42 | } 43 | } 44 | ` -------------------------------------------------------------------------------- /src/pages/discover/c-pages/recommend/c-cpns/ranking-list/index.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, memo } from 'react'; 2 | import { useDispatch, useSelector, shallowEqual } from "react-redux"; 3 | 4 | import { 5 | getTopData 6 | } from "../../store/actionCreators"; 7 | 8 | import HYThemeHeaderRCM from '@/components/theme-header-rcm'; 9 | import HYTopRanking from "@/components/top-ranking"; 10 | import { 11 | RankingWrapper 12 | } from "./style"; 13 | 14 | export default memo(function HYRankingList() { 15 | // redux 16 | const dispatch = useDispatch(); 17 | const state = useSelector((state) => ({ 18 | topUpList: state.getIn(["recommend", "topUpList"]), 19 | topNewList: state.getIn(["recommend", "topNewList"]), 20 | topOriginList: state.getIn(["recommend", "topOriginList"]) 21 | }), shallowEqual); 22 | 23 | // hooks 24 | useEffect(() => { 25 | dispatch(getTopData(19723756)); 26 | dispatch(getTopData(3779629)); 27 | dispatch(getTopData(2884035)); 28 | }, [dispatch]) 29 | 30 | return ( 31 | 32 | 33 |
    34 | 35 | 36 | 37 |
    38 |
    39 | ) 40 | }) 41 | -------------------------------------------------------------------------------- /src/pages/discover/c-pages/recommend/c-cpns/ranking-list/style.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const RankingWrapper = styled.div` 4 | .tops { 5 | margin: 30px 0; 6 | display: flex; 7 | background-image: url(${require("@/assets/img/recommend-top-bg.png")}); 8 | height: 472px; 9 | } 10 | ` 11 | -------------------------------------------------------------------------------- /src/pages/discover/c-pages/recommend/c-cpns/settle-singer/index.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, memo } from 'react'; 2 | import { useDispatch, useSelector, shallowEqual } from 'react-redux'; 3 | 4 | import { 5 | getSettleSingers 6 | } from "../../store/actionCreators"; 7 | import { 8 | getSizeImage 9 | } from "@/utils/format-utils"; 10 | 11 | import HYThemeHeaderSmall from '@/components/theme-header-small'; 12 | import { 13 | SetterSongerWrapper 14 | } from "./style"; 15 | 16 | export default memo(function HYSettleSinger() { 17 | // redux 18 | const dispatch = useDispatch(); 19 | const state = useSelector((state) => ({ 20 | settleSings: state.getIn(["recommend", "settleSings"]) 21 | }), shallowEqual); 22 | 23 | // hooks 24 | useEffect(() => { 25 | dispatch(getSettleSingers()); 26 | }, [dispatch]) 27 | 28 | return ( 29 | 30 | 31 |
    32 | { 33 | state.settleSings.map((item, index) => { 34 | return ( 35 | 36 | 37 |
    38 |
    {item.alias.join("") || item.name}
    39 |
    {item.name}
    40 |
    41 |
    42 | ) 43 | }) 44 | } 45 |
    46 |
    47 | 申请成为网易音乐人 48 |
    49 |
    50 | ) 51 | }) 52 | -------------------------------------------------------------------------------- /src/pages/discover/c-pages/recommend/c-cpns/settle-singer/style.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const SetterSongerWrapper = styled.div` 4 | padding: 20px; 5 | 6 | .singer-list { 7 | .item { 8 | display: flex; 9 | height: 62px; 10 | margin-top: 14px; 11 | background-color: #fafafa; 12 | text-decoration: none; 13 | 14 | :hover { 15 | background-color: #f4f4f4; 16 | } 17 | 18 | img { 19 | width: 62px; 20 | height: 62px; 21 | } 22 | 23 | .info { 24 | margin: 8px 0 0 10px; 25 | .title { 26 | color: #333; 27 | font-size: 14px; 28 | font-weight: 700; 29 | } 30 | 31 | .name { 32 | margin-top: 5px; 33 | } 34 | } 35 | } 36 | } 37 | 38 | .apply-for { 39 | margin-top: 12px; 40 | a { 41 | color: #333; 42 | font-weight: 700; 43 | text-align: center; 44 | display: block; 45 | height: 31px; 46 | line-height: 31px; 47 | border-radius: 4px; 48 | background-color: #fafafa; 49 | border: 1px solid #c3c3c3; 50 | text-decoration: none; 51 | } 52 | } 53 | ` 54 | -------------------------------------------------------------------------------- /src/pages/discover/c-pages/recommend/c-cpns/top-banner/index.js: -------------------------------------------------------------------------------- 1 | import React, { memo, useEffect, useCallback, useState, useRef } from 'react'; 2 | import { useDispatch, useSelector, shallowEqual } from 'react-redux'; 3 | 4 | import { 5 | getBanner 6 | } from '../../store/actionCreators'; 7 | 8 | import { Carousel } from 'antd'; 9 | import { 10 | BannerWrapper, 11 | BannerLeft, 12 | BannerRight, 13 | BannerControl 14 | } from './style'; 15 | 16 | export default memo(function HYTopBanner() { 17 | const [currentIndex, setCurrentIndex] = useState(0); 18 | 19 | const dispatch = useDispatch(); 20 | const state = useSelector(state => ({ 21 | banners: state.getIn(["recommend", "topBanners"]) 22 | }), shallowEqual) 23 | 24 | const bannerRef = useRef(); 25 | useEffect(() => { 26 | dispatch(getBanner()); 27 | }, [dispatch]); 28 | 29 | const bannerChange = useCallback((from, to) => { 30 | setTimeout(() => { 31 | setCurrentIndex(from); 32 | }, 0); 33 | }, []); 34 | 35 | const bgImage = state.banners[currentIndex] && (state.banners[currentIndex].imageUrl + "?imageView&blur=40x20") 36 | 37 | return ( 38 | 39 |
    40 | 41 | 42 | { 43 | state.banners.map((item, index) => { 44 | return ( 45 |
    46 | {item.typeTitle} 47 |
    48 | ) 49 | }) 50 | } 51 |
    52 |
    53 | 54 | 55 | 56 | 57 | 58 | 59 |
    60 |
    61 | ) 62 | }) 63 | -------------------------------------------------------------------------------- /src/pages/discover/c-pages/recommend/c-cpns/top-banner/style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const BannerWrapper = styled.div` 4 | background: url(${props => props.bgImage}) center center/6000px; 5 | 6 | .banner { 7 | height: 270px; 8 | background-color: red; 9 | 10 | display: flex; 11 | position: relative; 12 | } 13 | ` 14 | 15 | export const BannerLeft = styled.div` 16 | width: 730px; 17 | 18 | .banner-item { 19 | overflow: hidden; 20 | height: 270px; 21 | .image { 22 | width: 100%; 23 | } 24 | } 25 | ` 26 | 27 | export const BannerRight = styled.a.attrs({ 28 | href: "https://music.163.com/#/download", 29 | target: "_blank" 30 | })` 31 | width: 254px; 32 | height: 270px; 33 | background: url(${require("@/assets/img/download.png")}); 34 | ` 35 | 36 | export const BannerControl = styled.div` 37 | position: absolute; 38 | left: 0; 39 | right: 0; 40 | top: 50%; 41 | transform: translateY(-50%); 42 | 43 | .btn { 44 | position: absolute; 45 | width: 37px; 46 | height: 63px; 47 | background-image: url(${require("@/assets/img/banner_sprite.png")}); 48 | background-color: transparent; 49 | cursor: pointer; 50 | 51 | &:hover { 52 | background-color: rgba(0, 0, 0, .1); 53 | } 54 | } 55 | 56 | .left { 57 | left: -68px; 58 | background-position: 0 -360px; 59 | } 60 | 61 | .right { 62 | right: -68px; 63 | background-position: 0 -508px; 64 | } 65 | ` 66 | -------------------------------------------------------------------------------- /src/pages/discover/c-pages/recommend/c-cpns/user-login/index.js: -------------------------------------------------------------------------------- 1 | import React, {memo} from 'react'; 2 | 3 | import { 4 | UserLoginWrapper 5 | } from "./style"; 6 | 7 | export default memo(function HYUserLogin() { 8 | return ( 9 | 10 |

    登录网易云音乐,可以享受无限收藏的乐趣,并且无限同步到手机

    11 | 用户登录 12 |
    13 | ) 14 | }) 15 | 16 | -------------------------------------------------------------------------------- /src/pages/discover/c-pages/recommend/c-cpns/user-login/style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const UserLoginWrapper = styled.div` 4 | height: 126px; 5 | background-position: 0 0; 6 | padding: 16px 22px; 7 | display: flex; 8 | flex-direction: column; 9 | align-items: center; 10 | 11 | p { 12 | line-height: 25px; 13 | } 14 | 15 | a { 16 | margin-top: 10px; 17 | display: inline-block; 18 | width: 100px; 19 | height: 31px; 20 | line-height: 31px; 21 | text-align: center; 22 | color: #fff; 23 | text-decoration: none; 24 | background-position: 0 -195px; 25 | text-shadow: 0 1px 0 #8a060b; 26 | 27 | :hover { 28 | background-position: -110px -195px; 29 | } 30 | } 31 | ` 32 | -------------------------------------------------------------------------------- /src/pages/discover/c-pages/recommend/index.js: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | 3 | import HYTopBanner from './c-cpns/top-banner'; 4 | import HYHotRecommend from './c-cpns/hot-recommend'; 5 | import HYNewAlbum from './c-cpns/new-album'; 6 | import HYRankingList from './c-cpns/ranking-list'; 7 | import HYUserLogin from './c-cpns/user-login'; 8 | import HYSettleSinger from './c-cpns/settle-singer'; 9 | import HYHotRadio from './c-cpns/hot-radio'; 10 | import { 11 | RecommendWraper, 12 | Content, 13 | RecommendLeft, 14 | RecommendRight 15 | } from "./style"; 16 | 17 | export default memo(function HYRecommend() { 18 | return ( 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | ) 35 | }) 36 | -------------------------------------------------------------------------------- /src/pages/discover/c-pages/recommend/store/actionCreators.js: -------------------------------------------------------------------------------- 1 | import * as actionTypes from "./constants"; 2 | 3 | import { 4 | getTopBanner, 5 | getHotRecommend, 6 | getNewAlbum, 7 | getTopList, 8 | getArtistList 9 | } from "@/services/recommend"; 10 | 11 | const changeBannerAction = (res) => ({ 12 | type: actionTypes.CHANGE_TOP_BNNAER, 13 | banners: res.banners 14 | }) 15 | 16 | const changeRecommendAction = (res) => ({ 17 | type: actionTypes.CHANGE_HOT_RECOMMEND, 18 | recommends: res.result 19 | }) 20 | 21 | const changeNewAlbumAction = (res) => ({ 22 | type: actionTypes.CHANGE_NEW_ALBUM, 23 | newAlbum: res.albums 24 | }) 25 | 26 | const changeUpListAction = (res) => ({ 27 | type: actionTypes.CHANGE_UP_LIST, 28 | topUpList: res.playlist 29 | }) 30 | 31 | const changeNewListAction = (res) => ({ 32 | type: actionTypes.CHANGE_NEW_LIST, 33 | topNewList: res.playlist 34 | }) 35 | 36 | const changeOriginListAction = (res) => ({ 37 | type: actionTypes.CHANGE_ORIGIN_LIST, 38 | topOriginList: res.playlist 39 | }) 40 | 41 | const changeSettleSingsAction = (res) => ({ 42 | type: actionTypes.CHANGE_SETTLE_SONGER, 43 | settleSings: res.artists 44 | }) 45 | 46 | 47 | export const getBanner = () => { 48 | return dispatch => { 49 | getTopBanner().then(res => { 50 | dispatch(changeBannerAction(res)); 51 | }) 52 | } 53 | } 54 | 55 | export const getRecommend = () => { 56 | return dispatch => { 57 | getHotRecommend().then(res => { 58 | dispatch(changeRecommendAction(res)) 59 | }) 60 | } 61 | } 62 | 63 | export const getAlbum = () => { 64 | return dispatch => { 65 | getNewAlbum(10, 0).then(res => { 66 | dispatch(changeNewAlbumAction(res)) 67 | }) 68 | } 69 | } 70 | 71 | export const getTopData = (idx) => { 72 | return dispatch => { 73 | getTopList(idx).then(res => { 74 | switch (idx) { 75 | case 19723756: 76 | dispatch(changeNewListAction(res)); 77 | break; 78 | case 3779629: 79 | dispatch(changeOriginListAction(res)); 80 | break; 81 | case 2884035: 82 | dispatch(changeUpListAction(res)); 83 | break; 84 | default: 85 | console.log("其他数据处理"); 86 | } 87 | }) 88 | } 89 | } 90 | 91 | export const getSettleSingers = () => { 92 | return dispath => { 93 | getArtistList(5, 5001).then(res => { 94 | dispath(changeSettleSingsAction(res)) 95 | }) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/pages/discover/c-pages/recommend/store/constants.js: -------------------------------------------------------------------------------- 1 | export const CHANGE_TOP_BNNAER = "recommend/CHANGE_TOP_BNNAER"; 2 | export const CHANGE_HOT_RECOMMEND = "recommend/CHANGE_HOT_RECOMMEND"; 3 | export const CHANGE_NEW_ALBUM = "recommend/CHANGE_NEW_ALBUM"; 4 | export const CHANGE_UP_LIST = "recommend/CHAGNE_UP_LIST"; 5 | export const CHANGE_NEW_LIST = "recommend/CHANGE_NEW_LIST"; 6 | export const CHANGE_ORIGIN_LIST = "recommend/CHANGE_ORIGIN_LIST"; 7 | 8 | export const CHANGE_SETTLE_SONGER = "recommend/CHANGE_SETTLE_SONGER"; 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/pages/discover/c-pages/recommend/store/index.js: -------------------------------------------------------------------------------- 1 | import reducer from "./reducer"; 2 | 3 | export { 4 | reducer 5 | } 6 | -------------------------------------------------------------------------------- /src/pages/discover/c-pages/recommend/store/reducer.js: -------------------------------------------------------------------------------- 1 | import { Map } from "immutable"; 2 | import * as actionTypes from './constants'; 3 | 4 | const defaultState = Map({ 5 | topBanners: [], 6 | hotRecommends: [], 7 | newAlbum: [], 8 | topUpList: {}, 9 | topNewList: {}, 10 | topOriginList: {}, 11 | 12 | settleSings: [], 13 | }) 14 | 15 | export default (state = defaultState, action) => { 16 | switch(action.type) { 17 | case actionTypes.CHANGE_TOP_BNNAER: 18 | return state.set("topBanners", action.banners); 19 | case actionTypes.CHANGE_HOT_RECOMMEND: 20 | return state.set("hotRecommends", action.recommends); 21 | case actionTypes.CHANGE_NEW_ALBUM: 22 | return state.set("newAlbum", action.newAlbum); 23 | case actionTypes.CHANGE_UP_LIST: 24 | return state.set("topUpList", action.topUpList); 25 | case actionTypes.CHANGE_NEW_LIST: 26 | return state.set("topNewList", action.topNewList); 27 | case actionTypes.CHANGE_ORIGIN_LIST: 28 | return state.set("topOriginList", action.topOriginList); 29 | case actionTypes.CHANGE_SETTLE_SONGER: 30 | return state.set("settleSings", action.settleSings) 31 | default: 32 | return state; 33 | } 34 | } 35 | 36 | -------------------------------------------------------------------------------- /src/pages/discover/c-pages/recommend/style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const RecommendWraper = styled.div` 4 | 5 | ` 6 | 7 | export const Content = styled.div` 8 | background-color: #fff; 9 | display: flex; 10 | ` 11 | 12 | export const RecommendLeft = styled.div` 13 | padding: 20px; 14 | width: 729px; 15 | ` 16 | 17 | export const RecommendRight = styled.div` 18 | width: 250px; 19 | border: 1px solid #d3d3d3; 20 | border-width: 0 1px; 21 | ` 22 | -------------------------------------------------------------------------------- /src/pages/discover/c-pages/songs/c-cpns/songs-category/index.js: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import { useSelector, useDispatch, shallowEqual } from "react-redux"; 3 | 4 | import { 5 | changeCurrentCategoryAction, 6 | getSongList 7 | } from "../../store/actionCreators"; 8 | 9 | import { 10 | CategoryWrapper 11 | } from "./style"; 12 | 13 | export default memo(function HYSongsCategory() { 14 | // redux 15 | const { category } = useSelector(state => ({ 16 | category: state.getIn(["songs", "category"]) 17 | }), shallowEqual); 18 | const dispatch = useDispatch(); 19 | 20 | function selectCategory(name) { 21 | dispatch(changeCurrentCategoryAction(name)); 22 | dispatch(getSongList(0)); 23 | } 24 | 25 | return ( 26 | 27 |
    28 |
    29 | selectCategory("全部")}>全部风格 30 |
    31 |
    32 | { 33 | category.map((item, index) => { 34 | return ( 35 |
    36 |
    37 | 38 | {item.name} 39 |
    40 |
    41 | { 42 | item.subs.map(sItem => { 43 | return ( 44 |
    45 | selectCategory(sItem.name)}>{sItem.name} 46 | | 47 |
    48 | ) 49 | }) 50 | } 51 |
    52 |
    53 | ) 54 | }) 55 | } 56 |
    57 |
    58 | ) 59 | }) 60 | -------------------------------------------------------------------------------- /src/pages/discover/c-pages/songs/c-cpns/songs-category/style.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const CategoryWrapper = styled.div` 4 | position: absolute; 5 | z-index: 99; 6 | top: 55px; 7 | left: -25px; 8 | width: 700px; 9 | border: 1px solid #ccc; 10 | background-color: #fefefe; 11 | box-shadow: 0 0 10px 2px #d3d3d3; 12 | border-radius: 5px; 13 | padding-top: 10px; 14 | 15 | .arrow { 16 | position: absolute; 17 | top: -11px; 18 | left: 110px; 19 | width: 24px; 20 | height: 11px; 21 | background-position: -48px 0; 22 | } 23 | 24 | .all { 25 | padding: 10px 25px; 26 | border-bottom: 1px solid #e2e2e2; 27 | a { 28 | display: inline-block; 29 | text-align: center; 30 | width: 75px; 31 | height: 26px; 32 | line-height: 26px; 33 | border: 1px solid #d3d3d3; 34 | border-radius: 3px; 35 | background-color: #fafafa; 36 | } 37 | } 38 | 39 | .category { 40 | padding-left: 25px; 41 | dl { 42 | display: flex; 43 | align-items: flex-start; 44 | } 45 | 46 | dt { 47 | display: inline-flex; 48 | align-items: center; 49 | padding: 15px 0 10px; 50 | width: 70px; 51 | text-align: center; 52 | 53 | i { 54 | display: inline-block; 55 | width: 24px; 56 | height: 24px; 57 | background-position: -20px -735px; 58 | margin-right: 8px; 59 | } 60 | } 61 | 62 | dl.item1 { 63 | i { 64 | background-position: 0 -60px; 65 | } 66 | } 67 | 68 | dl.item2 { 69 | i { 70 | background-position: 0 -88px; 71 | } 72 | } 73 | 74 | dl.item3 { 75 | i { 76 | background-position: 0 -117px; 77 | } 78 | } 79 | 80 | dl.item4 { 81 | i { 82 | background-position: 0 -141px; 83 | } 84 | 85 | dd { 86 | padding-bottom: 25px; 87 | } 88 | } 89 | 90 | dd { 91 | padding-top: 18px; 92 | padding-left: 15px; 93 | flex: 1; 94 | display: flex; 95 | flex-wrap: wrap; 96 | border-left: 1px solid #e2e2e2; 97 | 98 | .item { 99 | margin-bottom: 8px; 100 | } 101 | 102 | a { 103 | color: #333; 104 | } 105 | 106 | .divider { 107 | margin: 0 12px; 108 | color: #e2e2e2; 109 | } 110 | } 111 | } 112 | ` -------------------------------------------------------------------------------- /src/pages/discover/c-pages/songs/c-cpns/songs-header/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, memo } from 'react'; 2 | import { useSelector, shallowEqual } from 'react-redux'; 3 | 4 | import HYSongsCategory from '../songs-category' 5 | import { 6 | HeaderWrapper, 7 | HeaderLeft, 8 | HeaderRight 9 | } from "./style"; 10 | 11 | export default memo(function HYSongsHeader() { 12 | // hooks 13 | const [showCategory, setShowCategory] = useState(false); 14 | 15 | // redux 16 | const { currentCategory } = useSelector(state => ({ 17 | currentCategory: state.getIn(["songs", "currentCategory"]) 18 | }), shallowEqual); 19 | 20 | return ( 21 | 22 | 23 | {currentCategory} 24 | 28 | {showCategory ? : null} 29 | 30 | 31 | 32 | 33 | 34 | ) 35 | }) 36 | -------------------------------------------------------------------------------- /src/pages/discover/c-pages/songs/c-cpns/songs-header/style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const HeaderWrapper = styled.div` 4 | display: flex; 5 | justify-content: space-between; 6 | align-items: center; 7 | border-bottom: 2px solid #c20c0c; 8 | padding-bottom: 6px; 9 | ` 10 | 11 | export const HeaderLeft = styled.div` 12 | display: flex; 13 | align-items: center; 14 | position: relative; 15 | .title { 16 | font-size: 24px; 17 | font-family: "Microsoft Yahei", Arial, Helvetica, sans-serif; 18 | } 19 | 20 | .select { 21 | position: relative; 22 | top: 2px; 23 | width: 91px; 24 | height: 31px; 25 | line-height: 31px; 26 | background-color: #fafafa; 27 | border: 1px solid #d3d3d3; 28 | border-radius: 3px; 29 | color: #0c73c2; 30 | margin-left: 10px; 31 | cursor: pointer; 32 | &:hover { 33 | background-color: #fff; 34 | } 35 | 36 | i { 37 | position: relative; 38 | left: 5px; 39 | bottom: 2px; 40 | display: inline-block; 41 | width: 8px; 42 | height: 5px; 43 | background-position: -70px -543px; 44 | } 45 | } 46 | 47 | .show { 48 | /* display: block !important; */ 49 | } 50 | 51 | .category { 52 | display: block; 53 | .cover { 54 | position: fixed; 55 | left: 0; 56 | right: 0; 57 | top: 105px; 58 | bottom: 0; 59 | background-color: rgba(0,0,0,.05); 60 | } 61 | } 62 | ` 63 | 64 | export const HeaderRight = styled.div` 65 | .hot { 66 | width: 46px; 67 | height: 29px; 68 | background-color: #c20c0c; 69 | color: #fff; 70 | border-radius: 3px; 71 | border: 1px solid #aaa; 72 | } 73 | ` -------------------------------------------------------------------------------- /src/pages/discover/c-pages/songs/c-cpns/songs-list/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, memo } from 'react'; 2 | import { useSelector, useDispatch, shallowEqual } from "react-redux"; 3 | 4 | import { PER_PAGE_NUMBER } from '../../store/constants'; 5 | import { getSongList } from "../../store/actionCreators"; 6 | 7 | import HYThemeCover from '@/components/theme-cover'; 8 | import HYPagination from '@/components/pagination'; 9 | import { 10 | SongListWrapper 11 | } from "./style"; 12 | 13 | export default memo(function HYSongsList() { 14 | // hooks 15 | const [currentPage, setCurrentPage] = useState(1); 16 | 17 | // redux 18 | const { categorySongs } = useSelector(state => ({ 19 | categorySongs: state.getIn(["songs", "categorySongs"]) 20 | }), shallowEqual); 21 | const songList = categorySongs.playlists || []; 22 | const total = categorySongs.total || 0; 23 | const dispatch = useDispatch(); 24 | 25 | function onPageChange(page, pageSize) { 26 | setCurrentPage(page); 27 | dispatch(getSongList(page)); 28 | } 29 | 30 | return ( 31 | 32 |
    33 | { 34 | songList.map((item, index) => { 35 | return ( 36 | 37 | ) 38 | }) 39 | } 40 |
    41 | 45 |
    46 | ) 47 | }) 48 | -------------------------------------------------------------------------------- /src/pages/discover/c-pages/songs/c-cpns/songs-list/style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const SongListWrapper = styled.div` 4 | .songs-list { 5 | display: flex; 6 | flex-wrap: wrap; 7 | justify-content: space-between; 8 | margin-right: -30px; 9 | } 10 | ` -------------------------------------------------------------------------------- /src/pages/discover/c-pages/songs/index.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, memo } from 'react'; 2 | import { useDispatch } from "react-redux"; 3 | import { useLocation } from 'react-router-dom'; 4 | 5 | import { 6 | getCategory, 7 | getSongList, 8 | changeCurrentCategoryAction 9 | } from "./store/actionCreators"; 10 | 11 | import HYSongsHeader from "./c-cpns/songs-header"; 12 | import HYSongsList from './c-cpns/songs-list'; 13 | import { 14 | SongsWrapper 15 | } from "./style" 16 | 17 | export default memo(function HYSongs() { 18 | // redux 19 | const dispatch = useDispatch(); 20 | const cat = useLocation().cat; 21 | 22 | useEffect(() => { 23 | dispatch(changeCurrentCategoryAction(cat)); 24 | }, [dispatch, cat]); 25 | 26 | // hooks 27 | useEffect(() => { 28 | dispatch(getCategory()); 29 | dispatch(getSongList(0)); 30 | }, [dispatch]) 31 | 32 | return ( 33 | 34 | 35 | 36 | 37 | ) 38 | }) 39 | -------------------------------------------------------------------------------- /src/pages/discover/c-pages/songs/store/actionCreators.js: -------------------------------------------------------------------------------- 1 | import * as actionTypes from './constants'; 2 | import { PER_PAGE_NUMBER } from './constants'; 3 | 4 | import { 5 | getSongCategory, 6 | getSongCategoryList 7 | } from "@/services/songs"; 8 | import { 9 | handleSongsCategory 10 | } from "@/utils/handle-data"; 11 | 12 | const changeCategoryAction = (res) => ({ 13 | type: actionTypes.CHANGE_CATEGORY, 14 | category: res 15 | }) 16 | 17 | const changeSongListAction = (res) => ({ 18 | type: actionTypes.CHANGE_CATEGORY_SONGS, 19 | categorySongs: res 20 | }) 21 | 22 | export const changeCurrentCategoryAction = (name) => ({ 23 | type: actionTypes.CHANGE_CURRENT_CATEGORY, 24 | currentCategory: name 25 | }) 26 | 27 | export const getCategory = () => { 28 | return dispatch => { 29 | getSongCategory().then(res => { 30 | const categoryData = handleSongsCategory(res); 31 | dispatch(changeCategoryAction(categoryData)) 32 | }) 33 | } 34 | } 35 | 36 | export const getSongList = (page) => { 37 | return (dispatch, getState) => { 38 | // 1.获取currentCategory 39 | const name = getState().getIn(["songs", "currentCategory"]); 40 | 41 | // 2.获取数据 42 | getSongCategoryList(name, page * PER_PAGE_NUMBER).then(res => { 43 | dispatch(changeSongListAction(res)); 44 | }) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/pages/discover/c-pages/songs/store/constants.js: -------------------------------------------------------------------------------- 1 | export const CHANGE_CATEGORY = "songs/CHANGE_CATEGORY"; 2 | export const CHANGE_CURRENT_CATEGORY = "songs/CHANGE_CURRENT_CATEGORY"; 3 | export const CHANGE_CATEGORY_SONGS = "songs/CHANGE_CATEGORY_SONGS"; 4 | 5 | export const PER_PAGE_NUMBER = 35; 6 | -------------------------------------------------------------------------------- /src/pages/discover/c-pages/songs/store/index.js: -------------------------------------------------------------------------------- 1 | import reducer from './reducer'; 2 | 3 | export { 4 | reducer 5 | } -------------------------------------------------------------------------------- /src/pages/discover/c-pages/songs/store/reducer.js: -------------------------------------------------------------------------------- 1 | import { Map } from "immutable"; 2 | import * as actionTypes from "./constants"; 3 | 4 | const defaultState = Map({ 5 | category: [], 6 | currentCategory: "全部", 7 | categorySongs: {} 8 | }) 9 | 10 | export default (state = defaultState, action) => { 11 | switch(action.type) { 12 | case actionTypes.CHANGE_CATEGORY: 13 | return state.set("category", action.category); 14 | case actionTypes.CHANGE_CURRENT_CATEGORY: 15 | return state.set("currentCategory", action.currentCategory); 16 | case actionTypes.CHANGE_CATEGORY_SONGS: 17 | return state.set("categorySongs", action.categorySongs); 18 | default: 19 | return state; 20 | } 21 | } -------------------------------------------------------------------------------- /src/pages/discover/c-pages/songs/style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const SongsWrapper = styled.div` 4 | padding: 40px; 5 | border: 1px solid #d3d3d3; 6 | border-width: 0 1px; 7 | background-color: #fff; 8 | ` -------------------------------------------------------------------------------- /src/pages/discover/index.js: -------------------------------------------------------------------------------- 1 | import React, {memo} from 'react'; 2 | import { NavLink } from "react-router-dom"; 3 | import { renderRoutes } from "react-router-config"; 4 | 5 | import { 6 | dicoverMenu 7 | } from "@/services/local-data"; 8 | 9 | import { 10 | DiscoverWrapper, 11 | TopMenu 12 | } from "./style"; 13 | 14 | export default memo(function HYDiscover(props) { 15 | const { route } = props; 16 | 17 | return ( 18 | 19 |
    20 | 21 | { 22 | dicoverMenu.map((item, index) => { 23 | return ( 24 |
    25 | {item.title} 26 |
    27 | ); 28 | }) 29 | } 30 |
    31 |
    32 | {renderRoutes(route.routes)} 33 |
    34 | ) 35 | }) 36 | -------------------------------------------------------------------------------- /src/pages/discover/style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const DiscoverWrapper = styled.div` 4 | .top { 5 | height: 30px; 6 | background-color: #C20C0C; 7 | } 8 | ` 9 | 10 | export const TopMenu = styled.div` 11 | display: flex; 12 | padding-left: 180px; 13 | position: relative; 14 | top: -4px; 15 | 16 | .item { 17 | a { 18 | display: inline-block; 19 | height: 20px; 20 | line-height: 20px; 21 | padding: 0 13px; 22 | margin: 7px 17px 0; 23 | color: #fff; 24 | 25 | &:hover, &.active { 26 | text-decoration: none; 27 | background-color: #9B0909; 28 | border-radius: 20px; 29 | } 30 | } 31 | } 32 | ` -------------------------------------------------------------------------------- /src/pages/friend/index.js: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | 3 | import { FriendWrapper } from './style'; 4 | 5 | export default memo(function HYFriend() { 6 | return ( 7 | 8 |
    9 |
    10 | 立即登录 11 |
    12 |
    13 |
    14 | ) 15 | }) 16 | -------------------------------------------------------------------------------- /src/pages/friend/style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const FriendWrapper = styled.div` 4 | .content { 5 | background-color: #fff; 6 | min-height: 700px; 7 | 8 | .pic { 9 | position: relative; 10 | width: 807px; 11 | height: 484px; 12 | margin: 0 auto; 13 | background: url(${require("@/assets/img/friend_sprite.jpg")}) 0 104px no-repeat; 14 | 15 | .login { 16 | position: absolute; 17 | width: 167px; 18 | height: 45px; 19 | left: 482px; 20 | top: 368px; 21 | text-indent: -9999px; 22 | } 23 | } 24 | } 25 | ` -------------------------------------------------------------------------------- /src/pages/main/index.js: -------------------------------------------------------------------------------- 1 | import React, { memo, Suspense } from 'react'; 2 | import { HashRouter } from "react-router-dom"; 3 | import { renderRoutes } from "react-router-config"; 4 | 5 | import routes from "@/router" 6 | 7 | import HYAppHeader from '@/components/app-header'; 8 | import HYAppFooter from '@/components/app-footer'; 9 | import HYAppPlayBar from '@/pages/player/app-play-bar'; 10 | 11 | export default memo(function HYMain() { 12 | return ( 13 | 14 | 15 | loading}> 16 | {renderRoutes(routes)} 17 | 18 | 19 | 20 | 21 | ) 22 | }) 23 | -------------------------------------------------------------------------------- /src/pages/mine/index.js: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | 3 | import { MineWrapper } from './style'; 4 | 5 | export default memo(function HYMine() { 6 | return ( 7 | 8 |
    9 |
    10 | 立即登录 11 |
    12 |
    13 |
    14 | ) 15 | }) 16 | -------------------------------------------------------------------------------- /src/pages/mine/style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const MineWrapper = styled.div` 4 | .content { 5 | background-color: #fff; 6 | min-height: 700px; 7 | 8 | .pic { 9 | position: relative; 10 | width: 807px; 11 | height: 372px; 12 | margin: 0 auto; 13 | background: url(${require("@/assets/img/mine_sprite.png")}) 0 104px no-repeat; 14 | 15 | .login { 16 | position: absolute; 17 | width: 167px; 18 | height: 45px; 19 | left: 482px; 20 | top: 302px; 21 | text-indent: -9999px; 22 | } 23 | } 24 | } 25 | ` -------------------------------------------------------------------------------- /src/pages/player/app-play-bar/index.js: -------------------------------------------------------------------------------- 1 | import React, { memo, useRef, useEffect, useState, useCallback } from 'react'; 2 | import { useSelector, shallowEqual, useDispatch } from 'react-redux'; 3 | 4 | import { message } from 'antd'; 5 | 6 | import { getPlayUrl, formatMinuteSecond } from '@/utils/format-utils'; 7 | import { 8 | getSongDetailAction, 9 | changeCurrentLyricIndexAction, 10 | changePlaySequenceAction, 11 | changePlaySongAction 12 | } from '../store/actionCreators'; 13 | 14 | import HYAppPlayPanel from '../app-play-panel' 15 | import { NavLink } from 'react-router-dom'; 16 | import { Slider } from 'antd'; 17 | import { 18 | PlaybarWrapper, 19 | Control, 20 | PlayInfo, 21 | Operator 22 | } from './style'; 23 | 24 | export default memo(function HYAppPlaybar() { 25 | // props and state 26 | const [isPlaying, setIsPlaying] = useState(false); 27 | const [duration, setDuration] = useState(0); 28 | const [currentTime, setCurrentTime] = useState(0); 29 | const [progress, setProgress] = useState(0); 30 | const [isChanging, setIsChanging] = useState(false); 31 | const [showPanel, setShowPanel] = useState(false); 32 | 33 | // redux hooks 34 | const { 35 | currentSong, 36 | currentLyrics, 37 | currentLyricIndex, 38 | playList, 39 | playSequence 40 | } = useSelector(state => ({ 41 | currentSong: state.getIn(["player", "currentSong"]), 42 | currentLyrics: state.getIn(["player", "currentLyrics"]), 43 | currentLyricIndex: state.getIn(["player", "currentLyricIndex"]), 44 | playList: state.getIn(["player", "playList"]), 45 | playSequence: state.getIn(["player", "playSequence"]) 46 | }), shallowEqual); 47 | const dispatch = useDispatch(); 48 | 49 | // other hooks 50 | const audioRef = useRef(); 51 | useEffect(() => { 52 | dispatch(getSongDetailAction(167876)); 53 | }, [dispatch]); 54 | 55 | useEffect(() => { 56 | audioRef.current.src = getPlayUrl(currentSong.id); 57 | audioRef.current.play().then(res => { 58 | setIsPlaying(true); 59 | }).catch(err => { 60 | setIsPlaying(false); 61 | }); 62 | setDuration(currentSong.dt); 63 | }, [currentSong]); 64 | 65 | // 其他业务 66 | const play = useCallback(() => { 67 | setIsPlaying(!isPlaying); 68 | isPlaying ? audioRef.current.pause() : audioRef.current.play().catch(err => { 69 | setIsPlaying(false); 70 | }); 71 | }, [isPlaying]); 72 | 73 | const timeUpdate = (e) => { 74 | const currentTime = e.target.currentTime; 75 | if (!isChanging) { 76 | setCurrentTime(currentTime); 77 | setProgress((currentTime * 1000) / duration * 100); 78 | } 79 | 80 | let lrcLength = currentLyrics.length; 81 | let i = 0; 82 | for (; i < lrcLength; i++) { 83 | const lrcTime = currentLyrics[i].time; 84 | if (currentTime * 1000 < lrcTime) { 85 | break 86 | } 87 | } 88 | const finalIndex = i - 1; 89 | if (finalIndex !== currentLyricIndex) { 90 | dispatch(changeCurrentLyricIndexAction(finalIndex)); 91 | message.open({ 92 | content: currentLyrics[finalIndex].content, 93 | key: "lyric", 94 | duration: 0, 95 | className: 'lyric-message', 96 | }) 97 | } 98 | } 99 | 100 | const timeEnded = () => { 101 | if (playSequence === 2 || playList.length === 1) { 102 | audioRef.current.currentTime = 0; 103 | audioRef.current.play(); 104 | } else { 105 | dispatch(changePlaySongAction(1)); 106 | } 107 | } 108 | 109 | const sliderChange = useCallback((value) => { 110 | setProgress(value); 111 | const time = value / 100.0 * duration / 1000; 112 | setCurrentTime(time); 113 | setIsChanging(true); 114 | }, [duration]) 115 | 116 | const sliderAfterChange = useCallback((value) => { 117 | const time = value / 100.0 * duration / 1000; 118 | audioRef.current.currentTime = time; 119 | setCurrentTime(time); 120 | setIsChanging(false); 121 | 122 | if (!isPlaying) { 123 | play(); 124 | } 125 | }, [duration, isPlaying, play]); 126 | 127 | return ( 128 | 129 |
    130 | 131 | 133 | 134 | 136 | 137 | 138 |
    139 | 140 | 141 | 142 |
    143 |
    144 |
    145 | {currentSong.name} 146 | {currentSong.ar[0].name} 147 |
    148 |
    149 | 150 |
    151 | {formatMinuteSecond(currentTime * 1000)} 152 | / 153 | {formatMinuteSecond(duration)} 154 |
    155 |
    156 |
    157 |
    158 | 159 |
    160 | 161 | 162 |
    163 |
    164 | 165 | 167 | 171 |
    172 |
    173 |
    174 |
    177 | ) 178 | }) 179 | -------------------------------------------------------------------------------- /src/pages/player/app-play-bar/style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const PlaybarWrapper = styled.div` 4 | position: fixed; 5 | z-index: 99; 6 | left: 0; 7 | right: 0; 8 | bottom: 0; 9 | height: 52px; 10 | background-position: 0 0; 11 | background-repeat: repeat; 12 | 13 | .content { 14 | display: flex; 15 | align-items: center; 16 | justify-content: space-between; 17 | position: absolute; 18 | left: 50%; 19 | transform: translateX(-50%); 20 | bottom: 0; 21 | height: 47px; 22 | } 23 | ` 24 | 25 | export const Control = styled.div` 26 | display: flex; 27 | align-items: center; 28 | 29 | .prev, .next { 30 | width: 28px; 31 | height: 28px; 32 | } 33 | 34 | .prev { 35 | background-position: 0 -130px; 36 | } 37 | 38 | .play { 39 | width: 36px; 40 | height: 36px; 41 | margin: 0 8px; 42 | background-position: 0 ${props => props.isPlaying ? "-165px": "-204px"}; 43 | } 44 | 45 | .next { 46 | background-position: -80px -130px; 47 | } 48 | ` 49 | 50 | export const PlayInfo = styled.div` 51 | display: flex; 52 | width: 642px; 53 | align-items: center; 54 | 55 | .image { 56 | width: 34px; 57 | height: 34px; 58 | border-radius: 5px; 59 | } 60 | 61 | .info { 62 | flex: 1; 63 | color: #a1a1a1; 64 | margin-left: 10px; 65 | 66 | .song { 67 | color: #e1e1e1; 68 | position: relative; 69 | top: 8px; 70 | left: 8px; 71 | 72 | .singer-name { 73 | color: #a1a1a1; 74 | margin-left: 10px; 75 | } 76 | } 77 | 78 | .progress { 79 | display: flex; 80 | align-items: center; 81 | 82 | .ant-slider { 83 | width: 493px; 84 | margin-right: 10px; 85 | 86 | .ant-slider-rail { 87 | height: 9px; 88 | background: url(${require("@/assets/img/progress_bar.png")}) right 0; 89 | } 90 | 91 | .ant-slider-track { 92 | height: 9px; 93 | background: url(${require("@/assets/img/progress_bar.png")}) left -66px; 94 | } 95 | 96 | .ant-slider-handle { 97 | width: 22px; 98 | height: 24px; 99 | border: none; 100 | margin-top: -7px; 101 | background: url(${require("@/assets/img/sprite_icon.png")}) 0 -250px; 102 | } 103 | } 104 | 105 | .time { 106 | .now-time { 107 | color: #e1e1e1; 108 | } 109 | .divider { 110 | margin: 0 3px; 111 | } 112 | } 113 | } 114 | } 115 | 116 | ` 117 | 118 | export const Operator = styled.div` 119 | display: flex; 120 | position: relative; 121 | top: 5px; 122 | 123 | .btn { 124 | width: 25px; 125 | height: 25px; 126 | } 127 | 128 | .favor { 129 | background-position: -88px -163px; 130 | } 131 | 132 | .share { 133 | background-position: -114px -163px; 134 | } 135 | 136 | .right { 137 | display: flex; 138 | align-items: center; 139 | width: 126px; 140 | padding-left: 13px; 141 | background-position: -147px -248px; 142 | 143 | .volume { 144 | background-position: -2px -248px; 145 | } 146 | 147 | .loop { 148 | background-position: ${props => { 149 | switch(props.sequence) { 150 | case 1: 151 | return "-66px -248px"; 152 | case 2: 153 | return "-66px -344px"; 154 | default: 155 | return "-3px -344px"; 156 | } 157 | }}; 158 | } 159 | 160 | .playlist { 161 | padding-left: 18px; 162 | text-align: center; 163 | color: #ccc; 164 | width: 59px; 165 | background-position: -42px -68px; 166 | } 167 | } 168 | ` 169 | -------------------------------------------------------------------------------- /src/pages/player/app-play-panel/c-cpns/lyric-panel/index.js: -------------------------------------------------------------------------------- 1 | import React, { memo, useRef, useEffect } from 'react'; 2 | import { useSelector, shallowEqual } from 'react-redux'; 3 | import classNames from 'classnames'; 4 | 5 | import { scrollTo } from "@/utils/ui-helper"; 6 | 7 | import { PannelWrapper } from './style'; 8 | 9 | export default memo(function HYLyricPanel() { 10 | const { currentLyrics, currentLyricIndex } = useSelector(state => ({ 11 | currentLyrics: state.getIn(["player", "currentLyrics"]), 12 | currentLyricIndex: state.getIn(["player", "currentLyricIndex"]) 13 | }), shallowEqual); 14 | 15 | // other hooks 16 | const panelRef = useRef(); 17 | useEffect(() => { 18 | if (currentLyricIndex > 0 && currentLyricIndex < 3) return; 19 | scrollTo(panelRef.current, (currentLyricIndex - 3) * 32, 300) 20 | }, [currentLyricIndex]); 21 | 22 | return ( 23 | 24 |
    25 | { 26 | currentLyrics.map((item, index) => { 27 | return ( 28 |
    30 | {item.content} 31 |
    32 | ) 33 | }) 34 | } 35 |
    36 |
    37 | ) 38 | }) 39 | -------------------------------------------------------------------------------- /src/pages/player/app-play-panel/c-cpns/lyric-panel/style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const PannelWrapper = styled.div` 4 | position: relative; 5 | flex: 1; 6 | margin: 21px 0 20px 0; 7 | overflow: scroll; 8 | 9 | &::-webkit-scrollbar { 10 | display: none; 11 | } 12 | 13 | .lrc-content { 14 | .lrc-item { 15 | height: 32px; 16 | text-align: center; 17 | color: #989898; 18 | 19 | &.active { 20 | color: #fff; 21 | font-size: 14px; 22 | } 23 | } 24 | } 25 | 26 | 27 | ` -------------------------------------------------------------------------------- /src/pages/player/app-play-panel/c-cpns/play-header/index.js: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import { useSelector, shallowEqual } from 'react-redux'; 3 | 4 | import { 5 | HeaderWrapper, 6 | HeaderLeft, 7 | HeaderRight 8 | } from './style'; 9 | 10 | export default memo(function HYPlayHeader() { 11 | const { playList, currentSong } = useSelector(state => ({ 12 | playList: state.getIn(["player", "playList"]), 13 | currentSong: state.getIn(["player", "currentSong"]) 14 | }), shallowEqual); 15 | 16 | return ( 17 | 18 | 19 |

    播放列表({playList.length})

    20 |
    21 | 25 | 29 |
    30 |
    31 | 32 | {currentSong.name} 33 | 34 |
    35 | ) 36 | }) 37 | -------------------------------------------------------------------------------- /src/pages/player/app-play-panel/c-cpns/play-header/style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const HeaderWrapper = styled.div` 4 | display: flex; 5 | height: 41px; 6 | line-height: 41px; 7 | background: url(${require("@/assets/img/playpanel_bg.png")}) 0 0; 8 | ` 9 | 10 | export const HeaderLeft = styled.div` 11 | display: flex; 12 | justify-content: space-between; 13 | width: 553px; 14 | padding: 0 25px; 15 | 16 | h3 { 17 | color: #e2e2e2; 18 | font-weight: 700; 19 | } 20 | 21 | .operator { 22 | color: #ccc; 23 | 24 | button { 25 | background-color: transparent; 26 | color: #ccc; 27 | } 28 | 29 | .icon { 30 | display: inline-block; 31 | width: 16px; 32 | height: 16px; 33 | position: relative; 34 | top: 4px; 35 | right: 2px; 36 | } 37 | 38 | .favor { 39 | background-position: -24px 0; 40 | } 41 | 42 | .remove { 43 | width: 13px; 44 | background-position: -51px 0; 45 | } 46 | } 47 | ` 48 | 49 | export const HeaderRight = styled.div` 50 | flex: 1; 51 | text-align: center; 52 | color: #fff; 53 | font-size: 14px; 54 | ` 55 | -------------------------------------------------------------------------------- /src/pages/player/app-play-panel/c-cpns/play-list/index.js: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import { useSelector, shallowEqual } from 'react-redux'; 3 | import classNames from 'classnames'; 4 | 5 | import { formatMinuteSecond } from '@/utils/format-utils'; 6 | 7 | import { PlayListWrapper } from './style'; 8 | 9 | export default memo(function HYPlayList() { 10 | const { playList, currentSongIndex } = useSelector(state => ({ 11 | playList: state.getIn(["player", "playList"]), 12 | currentSongIndex: state.getIn(["player", "currentSongIndex"]) 13 | }), shallowEqual); 14 | 15 | return ( 16 | 17 | { 18 | playList.map((item, index) => { 19 | return ( 20 |
    22 |
    {item.name}
    23 |
    24 | {item.ar[0].name} 25 | {formatMinuteSecond(item.dt)} 26 | 27 |
    28 |
    29 | ) 30 | }) 31 | } 32 |
    33 | ) 34 | }) 35 | -------------------------------------------------------------------------------- /src/pages/player/app-play-panel/c-cpns/play-list/style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const PlayListWrapper = styled.div` 4 | position: relative; 5 | width: 553px; 6 | padding: 2px; 7 | 8 | .play-item { 9 | padding: 0 8px 0 25px; 10 | display: flex; 11 | position: relative; 12 | justify-content: space-between; 13 | align-items: center; 14 | height: 28px; 15 | line-height: 28px; 16 | color: #ccc; 17 | 18 | &.active { 19 | color: #fff; 20 | background-color: #000; 21 | 22 | ::before { 23 | content: ""; 24 | position: absolute; 25 | left: 8px; 26 | width: 10px; 27 | height: 13px; 28 | background: url(${require("@/assets/img/playlist_sprite.png")}) -182px 0; 29 | } 30 | } 31 | 32 | .right { 33 | display: flex; 34 | align-items: center; 35 | 36 | .singer { 37 | width: 80px; 38 | } 39 | 40 | .duration { 41 | width: 45px; 42 | } 43 | 44 | .link { 45 | margin-left: 20px; 46 | width: 14px; 47 | height: 16px; 48 | background-position: -100px 0; 49 | } 50 | } 51 | } 52 | ` -------------------------------------------------------------------------------- /src/pages/player/app-play-panel/index.js: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | 3 | import HYPlayHeader from './c-cpns/play-header'; 4 | import HYPlayList from './c-cpns/play-list'; 5 | import HYLyricPanel from './c-cpns/lyric-panel'; 6 | import { PanelWrapper } from './style'; 7 | 8 | export default memo(function HYAppPlayList() { 9 | return ( 10 | 11 | 12 |
    13 | 14 | 15 | 16 |
    17 |
    18 | ) 19 | }) 20 | -------------------------------------------------------------------------------- /src/pages/player/app-play-panel/style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const PanelWrapper = styled.div` 4 | position: absolute; 5 | left: 50%; 6 | bottom: 46px; 7 | transform: translateX(-50%); 8 | width: 986px; 9 | height: 301px; 10 | color: #e2e2e2; 11 | 12 | .main { 13 | position: relative; 14 | display: flex; 15 | height: 260px; 16 | overflow: hidden; 17 | background: url(${require("@/assets/img/playpanel_bg.png")}) -1014px 0 repeat-y; 18 | 19 | .image { 20 | position: absolute; 21 | left: 2px; 22 | top: -360px; 23 | width: 980px; 24 | height: auto; 25 | opacity: .2; 26 | } 27 | } 28 | ` 29 | -------------------------------------------------------------------------------- /src/pages/player/c-cpns/player-comment/index.js: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react' 2 | 3 | export default memo(function HYPlayerComment() { 4 | return ( 5 |
    6 | 7 |
    8 | ) 9 | }) 10 | -------------------------------------------------------------------------------- /src/pages/player/c-cpns/player-info/index.js: -------------------------------------------------------------------------------- 1 | import React, { memo, useState } from 'react'; 2 | import { useSelector, shallowEqual } from 'react-redux'; 3 | 4 | import { getSizeImage } from '@/utils/format-utils'; 5 | 6 | import HYSongOperationBar from '@/components/song-operation-bar'; 7 | import { 8 | InfoWrapper, 9 | InfoLeft, 10 | InfoRight 11 | } from './style'; 12 | 13 | export default memo(function HYPlayerInfo() { 14 | // props and hooks 15 | const [isSpread, setIsSpread] = useState(false); 16 | 17 | // redux hooks 18 | const { currentSong, currentLyrics } = useSelector(state => ({ 19 | currentSong: state.getIn(["player", "currentSong"]), 20 | currentLyrics: state.getIn(["player", "currentLyrics"]) 21 | }), shallowEqual); 22 | 23 | // handle code 24 | const totalLyricCount = isSpread ? currentLyrics.length : 13; 25 | 26 | return ( 27 | 28 | 29 |
    30 | 31 | 32 |
    33 |
    34 | 35 | 生成外联播放器 36 |
    37 |
    38 | 39 |
    40 | 41 |

    {currentSong.name}

    42 |
    43 |
    44 | 歌手: 45 | {currentSong.ar[0].name} 46 |
    47 |
    48 | 所属专辑: 49 | {currentSong.al.name} 50 |
    51 | 52 | 56 | 57 |
    58 |
    59 | { 60 | currentLyrics.slice(0, totalLyricCount).map((item, index) => { 61 | return

    {item.content}

    62 | }) 63 | } 64 |
    65 | 70 |
    71 |
    72 |
    73 | ) 74 | }) 75 | -------------------------------------------------------------------------------- /src/pages/player/c-cpns/player-info/style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const InfoWrapper = styled.div` 4 | display: flex; 5 | padding: 47px 30px 40px 39px; 6 | ` 7 | 8 | export const InfoLeft = styled.div` 9 | width: 206px; 10 | 11 | .image { 12 | display: flex; 13 | position: relative; 14 | justify-content: center; 15 | align-items: center; 16 | width: 198px; 17 | height: 198px; 18 | 19 | .cover { 20 | background-position: -140px -580px; 21 | width: 206px; 22 | height: 205px; 23 | top: -4px; 24 | left: -4px; 25 | } 26 | } 27 | 28 | .link { 29 | margin: 20px; 30 | display: flex; 31 | justify-content: center; 32 | align-items: center; 33 | i { 34 | display: inline-block; 35 | width: 16px; 36 | height: 16px; 37 | background-position: -34px -863px; 38 | } 39 | 40 | a { 41 | color: #0c73c2; 42 | text-decoration: underline; 43 | } 44 | } 45 | ` 46 | 47 | export const InfoRight = styled.div` 48 | width: 414px; 49 | margin-left: 20px; 50 | 51 | .header { 52 | display: flex; 53 | align-items: center; 54 | i { 55 | display: inline-block; 56 | width: 54px; 57 | height: 24px; 58 | background-position: 0 -463px; 59 | } 60 | 61 | .title { 62 | font-size: 24px; 63 | font-weight: 400; 64 | margin-left: 10px; 65 | } 66 | } 67 | 68 | .singer, .album { 69 | margin: 10px; 70 | 71 | a { 72 | color: #0c73c2; 73 | } 74 | } 75 | 76 | .lyric { 77 | padding: 30px 0 50px; 78 | 79 | .lyric-info { 80 | .text { 81 | margin: 6px 0; 82 | } 83 | } 84 | 85 | .lyric-control { 86 | position: relative; 87 | color: #0c73c2; 88 | background-color: #fff; 89 | text-decoration: underline; 90 | cursor: pointer; 91 | 92 | i { 93 | position: absolute; 94 | width: 11px; 95 | height: 8px; 96 | right: -8px; 97 | top: 2px; 98 | background-position: ${props => props.isSpread ? "-45px": "-65px"} -520px; 99 | } 100 | } 101 | } 102 | ` -------------------------------------------------------------------------------- /src/pages/player/c-cpns/player-relevant/index.js: -------------------------------------------------------------------------------- 1 | import React, { memo, useEffect } from 'react'; 2 | import { useDispatch, useSelector, shallowEqual } from 'react-redux'; 3 | 4 | import { getSimiSongAction } from '../../store/actionCreators'; 5 | 6 | import HYThemeHeaderPlayer from '@/components/theme-header-player'; 7 | import { RelevantWrapper } from './style'; 8 | 9 | export default memo(function HYRelevant() { 10 | const { simiSongs } = useSelector(state => ({ 11 | simiSongs: state.getIn(["player", "simiSongs"]) 12 | }), shallowEqual); 13 | const dispatch = useDispatch(); 14 | 15 | useEffect(() => { 16 | dispatch(getSimiSongAction()); 17 | }, [dispatch]); 18 | 19 | return ( 20 | 21 | 22 |
    23 | { 24 | simiSongs.map((item, index) => { 25 | return ( 26 |
    27 |
    28 |
    29 | {item.name} 30 |
    31 | 34 |
    35 |
    36 | 37 | 38 |
    39 |
    40 | ) 41 | }) 42 | } 43 |
    44 |
    45 | ) 46 | }) 47 | -------------------------------------------------------------------------------- /src/pages/player/c-cpns/player-relevant/style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const RelevantWrapper = styled.div` 4 | margin-top: 40px; 5 | 6 | .songs { 7 | .song-item { 8 | display: flex; 9 | justify-content: space-between; 10 | align-items: center; 11 | margin-bottom: 12px; 12 | 13 | .info { 14 | .title { 15 | a { 16 | color: #666; 17 | } 18 | } 19 | 20 | .artist { 21 | a { 22 | color: #999; 23 | } 24 | } 25 | } 26 | 27 | .operate { 28 | .item { 29 | display: inline-block; 30 | width: 10px; 31 | height: 11px; 32 | } 33 | 34 | .play { 35 | background-position: -69px -455px; 36 | margin-right: 10px; 37 | } 38 | 39 | .add { 40 | background-position: -87px -454px; 41 | } 42 | } 43 | } 44 | } 45 | ` -------------------------------------------------------------------------------- /src/pages/player/c-cpns/player-songs/index.js: -------------------------------------------------------------------------------- 1 | import React, { memo, useEffect } from 'react'; 2 | import { useDispatch, useSelector, shallowEqual } from 'react-redux'; 3 | 4 | import { getSizeImage } from '@/utils/format-utils'; 5 | 6 | import HYThemeHeaderPlayer from '@/components/theme-header-player'; 7 | import { PlayerSongsWrapper } from './style'; 8 | import { getSimiPlaylistAction } from '../../store/actionCreators'; 9 | 10 | export default memo(function HYPlayerSongs() { 11 | 12 | // redux hooks 13 | const { simiPlaylist } = useSelector(state => ({ 14 | simiPlaylist: state.getIn(["player", "simiPlaylist"]) 15 | }), shallowEqual); 16 | const dispatch = useDispatch(); 17 | 18 | // other hooks 19 | useEffect(() => { 20 | dispatch(getSimiPlaylistAction()) 21 | }, [dispatch]); 22 | 23 | return ( 24 | 25 | 26 |
    27 | { 28 | simiPlaylist.map((item, index) => { 29 | return ( 30 |
    31 | 32 | 33 | 34 |
    35 | {item.name} 36 |
    37 | by 38 | {item.creator.nickname} 39 |
    40 |
    41 |
    42 | ) 43 | }) 44 | } 45 |
    46 |
    47 | ) 48 | }) 49 | -------------------------------------------------------------------------------- /src/pages/player/c-cpns/player-songs/style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const PlayerSongsWrapper = styled.div` 4 | .songs { 5 | .song-item { 6 | display: flex; 7 | align-items: center; 8 | margin-top: 15px; 9 | width: 200px; 10 | 11 | .image { 12 | width: 50px; 13 | height: 50px; 14 | } 15 | 16 | .info { 17 | margin-left: 10px; 18 | .name { 19 | font-size: 14px; 20 | color: #000; 21 | } 22 | 23 | .auchor { 24 | color: #999; 25 | 26 | .nickname { 27 | color: #666; 28 | margin-left: 5px; 29 | } 30 | } 31 | } 32 | } 33 | } 34 | ` -------------------------------------------------------------------------------- /src/pages/player/index.js: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | 3 | import HYPlayerInfo from './c-cpns/player-info'; 4 | import HYPlayerComment from './c-cpns/player-comment'; 5 | import HYPlayerSongs from './c-cpns/player-songs'; 6 | import HYPlayerRelevant from './c-cpns/player-relevant'; 7 | import { 8 | PlayerWrapper, 9 | PlayerLeft, 10 | PlayerRight 11 | } from './style'; 12 | 13 | export default memo(function HYPlayer() { 14 | return ( 15 | 16 |
    17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
    26 |
    27 | ) 28 | }) 29 | -------------------------------------------------------------------------------- /src/pages/player/store/actionCreators.js: -------------------------------------------------------------------------------- 1 | import * as actionTypes from './constants'; 2 | 3 | import { getSongDetail, getLyric, getSimiPlaylist, getSimiSong } from '@/services/player'; 4 | import { parseLyric } from '@/utils/lrc-parse'; 5 | 6 | const changeCurrentSongAction = (song) => ({ 7 | type: actionTypes.CHANGE_CURRENT_SONG, 8 | song 9 | }); 10 | 11 | const changeCurrentSongIndexAction = (index) => ({ 12 | type: actionTypes.CHANGE_CURRENT_SONG_INDEX, 13 | index 14 | }); 15 | 16 | const changePlayListAction = (playList) => ({ 17 | type: actionTypes.CHANGE_PLAY_LIST, 18 | playList: playList 19 | }) 20 | 21 | const changeLyricsAction = (lyrics) => ({ 22 | type: actionTypes.CHANGE_LYRICS, 23 | lyrics 24 | }) 25 | 26 | const changeSimiPlaylistAction = (res) => ({ 27 | type: actionTypes.CHANGE_SIMI_PLAYLIST, 28 | simiPlaylist: res.playlists 29 | }) 30 | 31 | const changeSimiSongsAction = (res) => ({ 32 | type: actionTypes.CHANGE_SIMI_SONGS, 33 | simiSongs: res.songs 34 | }) 35 | 36 | export const changeCurrentLyricIndexAction = (index) => ({ 37 | type: actionTypes.CHANGE_CURRENT_LYRIC_INDEX, 38 | index 39 | }) 40 | 41 | export const changePlaySequenceAction = (currentSequence) => { 42 | if (currentSequence === 3) currentSequence = 0; 43 | return { 44 | type: actionTypes.CHANGE_PLAY_SEQUENCE, 45 | sequence: currentSequence 46 | } 47 | } 48 | 49 | export const changePlaySongAction = (tag) => { 50 | return (dispatch, getState) => { 51 | // 1.获取当前的index 52 | let currentSongIndex = getState().getIn(["player", "currentSongIndex"]); 53 | const playSequence = getState().getIn(["player", "playSequence"]); 54 | const playList = getState().getIn(["player", "playList"]); 55 | 56 | // 2.判断当前播放列表 57 | switch (playSequence) { 58 | case 1: 59 | currentSongIndex = Math.floor(Math.random() * playList.length); 60 | break; 61 | default: 62 | currentSongIndex += tag; 63 | if (currentSongIndex === playList.length) currentSongIndex = 0; 64 | if (currentSongIndex === -1) currentSongIndex = playList.length - 1; 65 | } 66 | 67 | // 3.处理修改数据 68 | const currentSong = playList[currentSongIndex]; 69 | dispatch(changeCurrentSongIndexAction(currentSongIndex)); 70 | dispatch(changeCurrentSongAction(currentSong)); 71 | 72 | // 获取当前的歌词,并且解析 73 | getLyric(currentSong.id).then(res => { 74 | const lrcString = res.lrc.lyric; 75 | const lyrics = parseLyric(lrcString); 76 | dispatch(changeLyricsAction(lyrics)); 77 | }); 78 | } 79 | } 80 | 81 | export const getSongDetailAction = (ids) => { 82 | return (dispatch, getState) => { 83 | // 1.判断是否歌曲存在playList中 84 | const playList = getState().getIn(["player", "playList"]); 85 | 86 | const songIndex = playList.findIndex(song => song.id === ids); 87 | if (songIndex !== -1) { // 找到数据 88 | const currentSong = playList[songIndex]; 89 | dispatch(changeCurrentSongIndexAction(songIndex)); 90 | dispatch(changeCurrentSongAction(currentSong)); 91 | } else { // 未找到数据 92 | getSongDetail(ids).then(res => { 93 | const song = res.songs && res.songs[0]; 94 | if (!song) return; 95 | // 1.添加到playList中 96 | const newPlayList = [...playList]; 97 | newPlayList.push(song); 98 | dispatch(changePlayListAction(newPlayList)); 99 | // 2.改变当前index 100 | dispatch(changeCurrentSongIndexAction(newPlayList.length - 1)); 101 | dispatch(changeCurrentSongAction(song)); 102 | }); 103 | } 104 | 105 | // 获取当前的歌词,并且解析 106 | getLyric(ids).then(res => { 107 | const lrcString = res.lrc.lyric; 108 | const lyrics = parseLyric(lrcString); 109 | dispatch(changeLyricsAction(lyrics)); 110 | }); 111 | } 112 | } 113 | 114 | export const getSimiPlaylistAction = () => { 115 | return (dispatch, getState) => { 116 | const id = getState().getIn(["player", "currentSong"]).id; 117 | if (!id) return; 118 | 119 | getSimiPlaylist(id).then(res => { 120 | dispatch(changeSimiPlaylistAction(res)); 121 | }) 122 | } 123 | } 124 | 125 | export const getSimiSongAction = () => { 126 | return (dispatch, getState) => { 127 | const id = getState().getIn(["player", "currentSong"]).id; 128 | if (!id) return; 129 | 130 | getSimiSong(id).then(res => { 131 | dispatch(changeSimiSongsAction(res)); 132 | }) 133 | } 134 | } 135 | 136 | -------------------------------------------------------------------------------- /src/pages/player/store/constants.js: -------------------------------------------------------------------------------- 1 | export const CHANGE_CURRENT_SONG = "player/CHANGE_CURRENT_SONG"; 2 | export const CHANGE_LYRICS = "player/CHANGE_LYRICS"; 3 | 4 | export const CHANGE_SIMI_PLAYLIST = "player/CHANGE_SIMI_PLAYLIST"; 5 | export const CHANGE_SIMI_SONGS = "player/CHANGE_SIMI_SONGS"; 6 | 7 | export const CHANGE_PLAY_LIST = "player/CHANGE_PLAY_LIST"; 8 | export const CHANGE_CURRENT_SONG_INDEX = "player/CHANGE_CURRENT_SONG_INDEX"; 9 | export const CHANGE_PLAY_SEQUENCE = "player/CHANGE_PLAY_SEQUENCE"; 10 | 11 | export const CHANGE_CURRENT_LYRIC_INDEX = "player/CHANGE_CURRENT_LYRIC_INDEX"; 12 | 13 | -------------------------------------------------------------------------------- /src/pages/player/store/index.js: -------------------------------------------------------------------------------- 1 | import reducer from './reducer'; 2 | import { getSongDetailAction } from './actionCreators'; 3 | 4 | export { 5 | reducer, 6 | getSongDetailAction 7 | } -------------------------------------------------------------------------------- /src/pages/player/store/reducer.js: -------------------------------------------------------------------------------- 1 | import { Map } from 'immutable'; 2 | 3 | import * as actionTypes from './constants'; 4 | 5 | const defaultState = Map({ 6 | playList: [ 7 | { 8 | "name": "有何不可", 9 | "id": 167876, 10 | "pst": 0, 11 | "t": 0, 12 | "ar": [ 13 | { 14 | "id": 5771, 15 | "name": "许嵩", 16 | "tns": [], 17 | "alias": [] 18 | } 19 | ], 20 | "alia": [], 21 | "pop": 100, 22 | "st": 0, 23 | "rt": "600902000007916021", 24 | "fee": 8, 25 | "v": 49, 26 | "crbt": null, 27 | "cf": "", 28 | "al": { 29 | "id": 16953, 30 | "name": "自定义", 31 | "picUrl": "https://p1.music.126.net/Md3RLH0fe2a_3dMDnfqoQg==/18590542604286213.jpg", 32 | "tns": [], 33 | "pic_str": "18590542604286213", 34 | "pic": 18590542604286212 35 | }, 36 | "dt": 241840, 37 | "h": { 38 | "br": 320000, 39 | "fid": 0, 40 | "size": 9675799, 41 | "vd": -21099 42 | }, 43 | "m": { 44 | "br": 192000, 45 | "fid": 0, 46 | "size": 5805497, 47 | "vd": -18400 48 | }, 49 | "l": { 50 | "br": 128000, 51 | "fid": 0, 52 | "size": 3870346, 53 | "vd": -16900 54 | }, 55 | "a": null, 56 | "cd": "1", 57 | "no": 3, 58 | "rtUrl": null, 59 | "ftype": 0, 60 | "rtUrls": [], 61 | "djId": 0, 62 | "copyright": 2, 63 | "s_id": 0, 64 | "mark": 8192, 65 | "originCoverType": 0, 66 | "single": 0, 67 | "noCopyrightRcmd": null, 68 | "mv": 0, 69 | "mst": 9, 70 | "cp": 14026, 71 | "rtype": 0, 72 | "rurl": null, 73 | "publishTime": 1231516800000 74 | }, 75 | { 76 | "name": "雅俗共赏", 77 | "id": 411214279, 78 | "pst": 0, 79 | "t": 0, 80 | "ar": [ 81 | { 82 | "id": 5771, 83 | "name": "许嵩", 84 | "tns": [], 85 | "alias": [] 86 | } 87 | ], 88 | "alia": [], 89 | "pop": 100, 90 | "st": 0, 91 | "rt": null, 92 | "fee": 8, 93 | "v": 31, 94 | "crbt": null, 95 | "cf": "", 96 | "al": { 97 | "id": 34749138, 98 | "name": "青年晚报", 99 | "picUrl": "https://p1.music.126.net/Wcs2dbukFx3TUWkRuxVCpw==/3431575794705764.jpg", 100 | "tns": [], 101 | "pic": 3431575794705764 102 | }, 103 | "dt": 249621, 104 | "h": { 105 | "br": 320000, 106 | "fid": 0, 107 | "size": 9987177, 108 | "vd": -22200 109 | }, 110 | "m": { 111 | "br": 192000, 112 | "fid": 0, 113 | "size": 5992323, 114 | "vd": -19600 115 | }, 116 | "l": { 117 | "br": 128000, 118 | "fid": 0, 119 | "size": 3994896, 120 | "vd": -17800 121 | }, 122 | "a": null, 123 | "cd": "1", 124 | "no": 2, 125 | "rtUrl": null, 126 | "ftype": 0, 127 | "rtUrls": [], 128 | "djId": 0, 129 | "copyright": 0, 130 | "s_id": 0, 131 | "mark": 8192, 132 | "originCoverType": 0, 133 | "single": 0, 134 | "noCopyrightRcmd": null, 135 | "mv": 5302271, 136 | "rtype": 0, 137 | "rurl": null, 138 | "mst": 9, 139 | "cp": 14026, 140 | "publishTime": 1461723397683 141 | }, { 142 | "name": "嚣张", 143 | "id": 1382596189, 144 | "pst": 0, 145 | "t": 0, 146 | "ar": [ 147 | { 148 | "id": 32220939, 149 | "name": "en", 150 | "tns": [], 151 | "alias": [] 152 | } 153 | ], 154 | "alia": [], 155 | "pop": 100, 156 | "st": 0, 157 | "rt": "", 158 | "fee": 8, 159 | "v": 10, 160 | "crbt": null, 161 | "cf": "", 162 | "al": { 163 | "id": 80816891, 164 | "name": "嚣张", 165 | "picUrl": "https://p2.music.126.net/NhkuFBphLFaSmYMeW1-frQ==/109951164271814514.jpg", 166 | "tns": [], 167 | "pic_str": "109951164271814514", 168 | "pic": 109951164271814510 169 | }, 170 | "dt": 253994, 171 | "h": { 172 | "br": 320000, 173 | "fid": 0, 174 | "size": 10162605, 175 | "vd": -55669 176 | }, 177 | "m": { 178 | "br": 192000, 179 | "fid": 0, 180 | "size": 6097581, 181 | "vd": -53082 182 | }, 183 | "l": { 184 | "br": 128000, 185 | "fid": 0, 186 | "size": 4065069, 187 | "vd": -51369 188 | }, 189 | "a": null, 190 | "cd": "01", 191 | "no": 1, 192 | "rtUrl": null, 193 | "ftype": 0, 194 | "rtUrls": [], 195 | "djId": 0, 196 | "copyright": 0, 197 | "s_id": 0, 198 | "mark": 0, 199 | "originCoverType": 0, 200 | "single": 0, 201 | "noCopyrightRcmd": null, 202 | "mv": 0, 203 | "rtype": 0, 204 | "rurl": null, 205 | "mst": 9, 206 | "cp": 1372818, 207 | "publishTime": 0 208 | } 209 | ], 210 | playSequence: 0, // 0 顺序播放 1 随机播放 2 单曲循环 211 | currentSongIndex: [], 212 | currentSong: { 213 | "name": "有何不可", 214 | "id": 167876, 215 | "pst": 0, 216 | "t": 0, 217 | "ar": [ 218 | { 219 | "id": 5771, 220 | "name": "许嵩", 221 | "tns": [], 222 | "alias": [] 223 | } 224 | ], 225 | "alia": [], 226 | "pop": 100, 227 | "st": 0, 228 | "rt": "600902000007916021", 229 | "fee": 8, 230 | "v": 49, 231 | "crbt": null, 232 | "cf": "", 233 | "al": { 234 | "id": 16953, 235 | "name": "自定义", 236 | "picUrl": "https://p2.music.126.net/Md3RLH0fe2a_3dMDnfqoQg==/18590542604286213.jpg", 237 | "tns": [], 238 | "pic_str": "18590542604286213", 239 | "pic": 18590542604286212 240 | }, 241 | "dt": 241840, 242 | "h": { 243 | "br": 320000, 244 | "fid": 0, 245 | "size": 9675799, 246 | "vd": -21099 247 | }, 248 | "m": { 249 | "br": 192000, 250 | "fid": 0, 251 | "size": 5805497, 252 | "vd": -18400 253 | }, 254 | "l": { 255 | "br": 128000, 256 | "fid": 0, 257 | "size": 3870346, 258 | "vd": -16900 259 | }, 260 | "a": null, 261 | "cd": "1", 262 | "no": 3, 263 | "rtUrl": null, 264 | "ftype": 0, 265 | "rtUrls": [], 266 | "djId": 0, 267 | "copyright": 2, 268 | "s_id": 0, 269 | "mark": 8192, 270 | "originCoverType": 0, 271 | "single": 0, 272 | "noCopyrightRcmd": null, 273 | "mv": 0, 274 | "rtype": 0, 275 | "rurl": null, 276 | "mst": 9, 277 | "cp": 14026, 278 | "publishTime": 1231516800000 279 | }, 280 | currentLyrics: [], 281 | simiPlaylist: [], 282 | simiSongs: [], 283 | currentLyricIndex: -1 284 | }); 285 | 286 | function reducer(state = defaultState, action) { 287 | switch (action.type) { 288 | case actionTypes.CHANGE_CURRENT_SONG: 289 | return state.set("currentSong", action.song); 290 | case actionTypes.CHANGE_LYRICS: 291 | return state.set("currentLyrics", action.lyrics); 292 | case actionTypes.CHANGE_SIMI_PLAYLIST: 293 | return state.set("simiPlaylist", action.simiPlaylist); 294 | case actionTypes.CHANGE_SIMI_SONGS: 295 | return state.set("simiSongs", action.simiSongs); 296 | case actionTypes.CHANGE_PLAY_LIST: 297 | return state.set("playList", action.playList); 298 | case actionTypes.CHANGE_CURRENT_SONG_INDEX: 299 | return state.set("currentSongIndex", action.index); 300 | case actionTypes.CHANGE_CURRENT_LYRIC_INDEX: 301 | return state.set("currentLyricIndex", action.index); 302 | case actionTypes.CHANGE_PLAY_SEQUENCE: 303 | return state.set("playSequence", action.sequence); 304 | default: 305 | return state; 306 | } 307 | } 308 | 309 | export default reducer; 310 | -------------------------------------------------------------------------------- /src/pages/player/style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const PlayerWrapper = styled.div` 4 | .content { 5 | background: url(${require("@/assets/img/wrap-bg.png")}) repeat-y; 6 | background-color: #fff; 7 | display: flex; 8 | } 9 | ` 10 | 11 | export const PlayerLeft = styled.div` 12 | width: 710px; 13 | ` 14 | 15 | export const PlayerRight = styled.div` 16 | width: 270px; 17 | padding: 20px 40px 40px 30px; 18 | ` 19 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Redirect } from "react-router-dom"; 3 | 4 | // import HYDiscover from "../pages/discover"; 5 | // import HYRecommend from "../pages/discover/c-pages/recommend"; 6 | // import HYRanking from "../pages/discover/c-pages/ranking"; 7 | // import HYSongs from "../pages/discover/c-pages/songs"; 8 | // import HYDjradio from "../pages/discover/c-pages/djradio"; 9 | // import HYArtist from "../pages/discover/c-pages/artist"; 10 | // import HYAlbum from "../pages/discover/c-pages/album"; 11 | // import HYPlayer from "../pages/player"; 12 | 13 | // import HYFriend from "../pages/friend"; 14 | // import HYMine from "../pages/mine"; 15 | 16 | const HYDiscover = React.lazy(_ => import("../pages/discover")); 17 | const HYRecommend = React.lazy(_ => import("../pages/discover/c-pages/recommend")); 18 | const HYRanking = React.lazy(_ => import("../pages/discover/c-pages/ranking")); 19 | const HYSongs = React.lazy(_ => import("../pages/discover/c-pages/songs")); 20 | const HYDjradio = React.lazy(_ => import("../pages/discover/c-pages/djradio")); 21 | const HYArtist = React.lazy(_ => import("../pages/discover/c-pages/artist")); 22 | const HYAlbum = React.lazy(_ => import("../pages/discover/c-pages/album")); 23 | const HYPlayer = React.lazy(_ => import("../pages/player")); 24 | 25 | 26 | const HYFriend = React.lazy(_ => import("../pages/friend")); 27 | const HYMine = React.lazy(_ => import("../pages/mine")); 28 | 29 | 30 | export default [ 31 | { 32 | path: "/", 33 | exact: true, 34 | render: () => ( 35 | 36 | ) 37 | }, 38 | { 39 | path: "/discover", 40 | component: HYDiscover, 41 | routes: [ 42 | { 43 | path: "/discover", 44 | exact: true, 45 | render: () => ( 46 | 47 | ) 48 | }, 49 | { 50 | path: "/discover/recommend", 51 | component: HYRecommend 52 | }, 53 | { 54 | path: "/discover/ranking", 55 | component: HYRanking 56 | }, 57 | { 58 | path: "/discover/songs", 59 | component: HYSongs 60 | }, 61 | { 62 | path: "/discover/djradio", 63 | exact: true, 64 | component: HYDjradio 65 | }, 66 | { 67 | path: "/discover/artist", 68 | component: HYArtist 69 | }, 70 | { 71 | path: "/discover/album", 72 | component: HYAlbum 73 | }, 74 | { 75 | path: "/discover/player", 76 | component: HYPlayer 77 | } 78 | ] 79 | }, 80 | { 81 | path: "/friend", 82 | component: HYFriend 83 | }, 84 | { 85 | path: "/mine", 86 | component: HYMine 87 | } 88 | ] -------------------------------------------------------------------------------- /src/services/album.js: -------------------------------------------------------------------------------- 1 | import request from './axios'; 2 | 3 | export function getHotAlbums() { 4 | return request({ 5 | url: "/album/newest" 6 | }) 7 | } 8 | 9 | export function getTopAlbums(limit, offset) { 10 | return request({ 11 | url: "/album/new", 12 | params: { 13 | limit, 14 | offset 15 | } 16 | }) 17 | } -------------------------------------------------------------------------------- /src/services/artist.js: -------------------------------------------------------------------------------- 1 | import request from './axios'; 2 | 3 | export function getArtistList(area, type, initial) { 4 | let url = "/artist/list"; 5 | let params = { limit: 100 } 6 | if (area === -1 && type === 1) { 7 | url = "/top/artists" 8 | } else { 9 | if (area === -1) { 10 | params = {limit: 100, cat: 5001} 11 | } else { 12 | params = { 13 | type, 14 | area, 15 | initial, 16 | limit: 100 17 | } 18 | } 19 | } 20 | 21 | console.log("url:", url, "params:", params); 22 | 23 | return request({ 24 | url, 25 | params 26 | }) 27 | } 28 | 29 | -------------------------------------------------------------------------------- /src/services/axios.js: -------------------------------------------------------------------------------- 1 | import originAxios from 'axios'; 2 | 3 | export default function request(option) { 4 | return new Promise((resolve, reject) => { 5 | // 1.创建axios的实例 6 | const instance = originAxios.create({ 7 | baseURL: 'http://123.207.32.32:9002/', 8 | timeout: 10000 9 | }); 10 | 11 | // 配置请求和响应拦截 12 | instance.interceptors.request.use(config => { 13 | // console.log('来到了request拦截success中'); 14 | // 1.当发送网络请求时, 在页面中添加一个loading组件, 作为动画 15 | 16 | // 2.某些请求要求用户必须登录, 判断用户是否有token, 如果没有token跳转到login页面 17 | 18 | // 3.对请求的参数进行序列化(看服务器是否需要序列化) 19 | // config.data = qs.stringify(config.data) 20 | // console.log(config); 21 | 22 | // 4.等等 23 | return config 24 | }, err => { 25 | // console.log('来到了request拦截failure中'); 26 | return err 27 | }) 28 | 29 | instance.interceptors.response.use(response => { 30 | // console.log('来到了response拦截success中'); 31 | return response.data 32 | }, err => { 33 | console.log('来到了response拦截failure中'); 34 | console.log(err); 35 | if (err && err.response) { 36 | switch (err.response.status) { 37 | case 400: 38 | err.message = '请求错误' 39 | break 40 | case 401: 41 | err.message = '未授权的访问' 42 | break 43 | default: 44 | err.message = "其他错误信息" 45 | } 46 | } 47 | return err 48 | }) 49 | 50 | // 2.传入对象进行网络请求 51 | instance(option).then(res => { 52 | resolve(res) 53 | }).catch(err => { 54 | reject(err) 55 | }) 56 | }) 57 | } 58 | -------------------------------------------------------------------------------- /src/services/djradio.js: -------------------------------------------------------------------------------- 1 | import request from './axios'; 2 | 3 | export function getDjRadioCatelist() { 4 | return request({ 5 | url: "/dj/catelist" 6 | }) 7 | } 8 | 9 | export function getDjRadioRecommend(type) { 10 | return request({ 11 | url: "/dj/recommend/type", 12 | params: { 13 | type 14 | } 15 | }) 16 | } 17 | 18 | export function getDjRadios(cateId, limit, offset) { 19 | return request({ 20 | url: "/dj/radio/hot", 21 | params: { 22 | cateId, 23 | limit, 24 | offset 25 | } 26 | }) 27 | } -------------------------------------------------------------------------------- /src/services/local-data.js: -------------------------------------------------------------------------------- 1 | export const headerLinks = [ 2 | { 3 | title: "发现音乐", 4 | link: "/discover" 5 | }, 6 | { 7 | title: "我的音乐", 8 | link: "/mine" 9 | }, 10 | { 11 | title: "朋友", 12 | link: "/friend" 13 | }, 14 | { 15 | title: "商城", 16 | link: "https://music.163.com/store/product" 17 | }, 18 | { 19 | title: "音乐人", 20 | link: "https://music.163.com/nmusician/web/index#/" 21 | }, 22 | { 23 | title: "下载客户端", 24 | link: "https://music.163.com/#/download" 25 | } 26 | ] 27 | 28 | 29 | export const footerLinks = [ 30 | { 31 | title: "服务条款", 32 | link: "https://st.music.163.com/official-terms/service" 33 | }, 34 | { 35 | title: "隐私政策", 36 | link: "https://st.music.163.com/official-terms/privacy" 37 | }, 38 | { 39 | title: "儿童隐私政策", 40 | link: "https://st.music.163.com/official-terms/children" 41 | }, 42 | { 43 | title: "版权投诉指引", 44 | link: "https://music.163.com/st/staticdeal/complaints.html" 45 | }, 46 | { 47 | title: "意见反馈", 48 | link: "#" 49 | } 50 | ] 51 | 52 | export const footerImages = [ 53 | { 54 | link: "https://music.163.com/st/userbasic#/auth" 55 | }, 56 | { 57 | link: "https://music.163.com/recruit" 58 | }, 59 | { 60 | link: "https://music.163.com/web/reward" 61 | }, 62 | { 63 | link: "https://music.163.com/uservideo#/plan" 64 | } 65 | ] 66 | 67 | // discover中的数据 68 | export const dicoverMenu = [ 69 | { 70 | title: "推荐", 71 | link: "/discover/recommend" 72 | }, 73 | { 74 | title: "排行榜", 75 | link: "/discover/ranking" 76 | }, 77 | { 78 | title: "歌单", 79 | link: "/discover/songs" 80 | }, 81 | { 82 | title: "主播电台", 83 | link: "/discover/djradio" 84 | }, 85 | { 86 | title: "歌手", 87 | link: "/discover/artist" 88 | }, 89 | { 90 | title: "新碟上架", 91 | link: "/discover/album" 92 | }, 93 | ] 94 | 95 | // 热门主播 96 | export const hotRadios = [ 97 | { 98 | picUrl: "http://p1.music.126.net/H3QxWdf0eUiwmhJvA4vrMQ==/1407374893913311.jpg", 99 | name: "陈立", 100 | position: "心理学家、美食家陈立教授", 101 | url: "/user/home?id=278438485" 102 | }, 103 | { 104 | picUrl: "http://p1.music.126.net/y5-sM7tjnxnu_V9LWKgZlw==/7942872001461517.jpg", 105 | name: "DJ艳秋", 106 | position: "著名音乐节目主持人", 107 | url: "/user/home?id=91239965", 108 | }, 109 | { 110 | picUrl: "http://p1.music.126.net/6cc6lgOfQTo6ovNnTHPyJg==/3427177769086282.jpg", 111 | name: "国家大剧院古典音乐频道", 112 | position: "国家大剧院古典音乐官方", 113 | url: "/user/home?id=324314596", 114 | }, 115 | { 116 | picUrl: "http://p1.music.126.net/xa1Uxrrn4J0pm_PJwkGYvw==/3130309604335651.jpg", 117 | name: "谢谢收听", 118 | position: "南京电台主持人王馨", 119 | url: "/user/home?id=1611157", 120 | }, 121 | { 122 | picUrl: "http://p1.music.126.net/slpd09Tf5Ju82Mv-h8MP4w==/3440371884651965.jpg", 123 | name: "DJ晓苏", 124 | position: "独立DJ,CRI环球旅游广播特邀DJ", 125 | url: "/user/home?id=2313954" 126 | } 127 | ] 128 | 129 | // 歌手类别 130 | export const artistCategories = [ 131 | { 132 | title: "推荐", 133 | area: -1, 134 | artists: [ 135 | { 136 | name: "推荐歌手", 137 | type: 1, 138 | url: "/discover/artist", 139 | id: 0 140 | }, 141 | { 142 | name: "入驻歌手", 143 | type: 2, 144 | url: "/discover/artist?cat=5001", 145 | dataPath: "/artist/list?cat=5001" 146 | } 147 | ] 148 | }, 149 | { 150 | title: "华语", 151 | area: 7, 152 | artists: [ 153 | { 154 | name: "华语男歌手", 155 | url: "/discover/artist?id=1001", 156 | type: 1 157 | }, 158 | { 159 | name: "华语女歌手", 160 | url: "/discover/artist?id=1002", 161 | type: 2 162 | }, 163 | { 164 | name: "华语组合/乐队", 165 | url: "/discover/artist?id=1003", 166 | type: 3 167 | } 168 | ] 169 | }, 170 | { 171 | title: "欧美", 172 | area: 96, 173 | artists: [ 174 | { 175 | name: "欧美男歌手", 176 | url: "/discover/artist?id=2001", 177 | type: 1 178 | }, 179 | { 180 | name: "欧美女歌手", 181 | url: "/discover/artist?id=2002", 182 | type: 2 183 | }, 184 | { 185 | name: "欧美组合乐队", 186 | url: "/discover/artist?id=2003", 187 | type: 3 188 | }, 189 | ] 190 | }, 191 | { 192 | title: "日本", 193 | area: 8, 194 | artists: [ 195 | { 196 | name: "日本男歌手", 197 | url: "/discover/artist?id=6001", 198 | type: 1 199 | }, 200 | { 201 | name: "日本女歌手", 202 | url: "/discover/artist?id=6002", 203 | type: 2 204 | }, 205 | { 206 | name: "日本组合/乐队", 207 | url: "/discover/artist?id=6003", 208 | type: 3 209 | }, 210 | ] 211 | }, 212 | { 213 | title: "韩国", 214 | area: 16, 215 | artists: [ 216 | { 217 | name: "韩国男歌手", 218 | url: "/discover/artist?id=7001", 219 | type: 1 220 | }, 221 | { 222 | name: "韩国女歌手", 223 | url: "/discover/artist?id=7002", 224 | type: 2 225 | }, 226 | { 227 | name: "韩国组合/乐队", 228 | url: "/discover/artist?id=7003", 229 | type: 3 230 | }, 231 | ] 232 | }, 233 | { 234 | title: "其他", 235 | area: 0, 236 | artists: [ 237 | { 238 | name: "其他男歌手", 239 | url: "/discover/artist?id=4001", 240 | type: 1 241 | }, 242 | { 243 | name: "其他女歌手", 244 | url: "/discover/artist?id=4002", 245 | type: 2 246 | }, 247 | { 248 | name: "其他组合乐队", 249 | url: "/discover/artist?id=4003", 250 | type: 3 251 | } 252 | ] 253 | }, 254 | ] 255 | -------------------------------------------------------------------------------- /src/services/player.js: -------------------------------------------------------------------------------- 1 | import request from './axios'; 2 | 3 | export function getSongDetail(ids) { 4 | return request({ 5 | url: "/song/detail", 6 | params: { 7 | ids 8 | } 9 | }) 10 | } 11 | 12 | export function getLyric(id) { 13 | return request({ 14 | url: "/lyric", 15 | params: { 16 | id 17 | } 18 | }) 19 | } 20 | 21 | export function getSimiPlaylist(id) { 22 | return request({ 23 | url: "/simi/playlist", 24 | params: { 25 | id 26 | } 27 | }) 28 | } 29 | 30 | export function getSimiSong(id) { 31 | return request({ 32 | url: "/simi/song", 33 | params: { 34 | id 35 | } 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /src/services/ranking.js: -------------------------------------------------------------------------------- 1 | import request from "./axios"; 2 | 3 | export function getTopList() { 4 | return request({ 5 | url: "/toplist" 6 | }) 7 | } 8 | 9 | // 获取榜单详情 10 | export function getRankingList(id) { 11 | return request({ 12 | url: "/playlist/detail", 13 | params: { 14 | id 15 | } 16 | }) 17 | } -------------------------------------------------------------------------------- /src/services/recommend.js: -------------------------------------------------------------------------------- 1 | import request from "./axios"; 2 | 3 | export function getTopBanner() { 4 | return request({ 5 | url: "/banner" 6 | }) 7 | } 8 | 9 | export function getHotRecommend() { 10 | return request({ 11 | url: "/personalized" 12 | }) 13 | } 14 | 15 | export function getNewAlbum(limit, offset) { 16 | return request({ 17 | url: "/album/new", 18 | params: { 19 | limit, 20 | offset 21 | } 22 | }) 23 | } 24 | 25 | export function getTopList(id) { 26 | return request({ 27 | url: "/playlist/detail", 28 | params: { 29 | id 30 | } 31 | }) 32 | } 33 | 34 | 35 | export function getArtistList(limit, cat) { 36 | return request({ 37 | url: "/artist/list", 38 | params: { 39 | cat, 40 | limit 41 | } 42 | }) 43 | } -------------------------------------------------------------------------------- /src/services/songs.js: -------------------------------------------------------------------------------- 1 | import request from "./axios"; 2 | 3 | export function getSongCategory() { 4 | return request({ 5 | url: "/playlist/catlist" 6 | }) 7 | } 8 | 9 | export function getSongCategoryList(cat="全部", offset=0, limit = 35) { 10 | return request({ 11 | url: "/top/playlist", 12 | params: { 13 | cat, 14 | limit, 15 | offset 16 | } 17 | }) 18 | } -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore, compose, applyMiddleware } from "redux"; 2 | import thunk from "redux-thunk"; 3 | import reducer from './reducer'; 4 | 5 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; 6 | 7 | const store = createStore(reducer, composeEnhancers( 8 | applyMiddleware(thunk) 9 | )); 10 | 11 | export default store; 12 | -------------------------------------------------------------------------------- /src/store/reducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux-immutable"; 2 | 3 | import { reducer as recommendReducer } from "../pages/discover/c-pages/recommend/store"; 4 | import { reducer as rankingReducer } from "../pages/discover/c-pages/ranking/store"; 5 | import { reducer as songsReducer } from "../pages/discover/c-pages/songs/store"; 6 | import { reducer as djradioReducer } from "../pages/discover/c-pages/djradio/store"; 7 | import { reducer as albumReducer } from "../pages/discover/c-pages/album/store"; 8 | import { reducer as artistReducer } from "../pages/discover/c-pages/artist/store"; 9 | import { reducer as playerReducer } from "../pages/player/store"; 10 | 11 | export default combineReducers({ 12 | recommend: recommendReducer, 13 | ranking: rankingReducer, 14 | songs: songsReducer, 15 | djradio: djradioReducer, 16 | album: albumReducer, 17 | artist: artistReducer, 18 | player: playerReducer 19 | }) 20 | -------------------------------------------------------------------------------- /src/utils/format-utils.js: -------------------------------------------------------------------------------- 1 | export function getSizeImage(imgUrl, size) { 2 | return `${imgUrl}?param=${size}x${size}`; 3 | } 4 | 5 | export function getCount(count) { 6 | if (count < 0) return; 7 | if (count < 10000) { 8 | return count; 9 | } else if (Math.floor(count / 10000) < 10000) { 10 | return Math.floor(count / 1000) / 10 + "万"; 11 | } else { 12 | return Math.floor(count / 10000000) / 10 + "亿"; 13 | } 14 | } 15 | 16 | export function getPlayUrl(id) { 17 | return `https://music.163.com/song/media/outer/url?id=${id}.mp3` 18 | } 19 | 20 | export function formatDate(time, fmt) { 21 | let date = new Date(time); 22 | 23 | if (/(y+)/.test(fmt)) { 24 | fmt = fmt.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length)); 25 | } 26 | let o = { 27 | 'M+': date.getMonth() + 1, 28 | 'd+': date.getDate(), 29 | 'h+': date.getHours(), 30 | 'm+': date.getMinutes(), 31 | 's+': date.getSeconds() 32 | }; 33 | for (let k in o) { 34 | if (new RegExp(`(${k})`).test(fmt)) { 35 | let str = o[k] + ''; 36 | fmt = fmt.replace(RegExp.$1, (RegExp.$1.length === 1) ? str : padLeftZero(str)); 37 | } 38 | } 39 | return fmt; 40 | }; 41 | 42 | function padLeftZero(str) { 43 | return ('00' + str).substr(str.length); 44 | }; 45 | 46 | export function formatMonthDay(time) { 47 | return formatDate(time, "MM月dd日"); 48 | } 49 | 50 | export function formatMinuteSecond(time) { 51 | return formatDate(time, "mm:ss"); 52 | } 53 | -------------------------------------------------------------------------------- /src/utils/handle-data.js: -------------------------------------------------------------------------------- 1 | export function handleSongsCategory(data) { 2 | // 1.获取所有的类别 3 | const category = data.categories; 4 | 5 | // 2.创建类别数据结构 6 | const categoryData = Object.entries(category).map(([key, value]) => { 7 | return { 8 | name: value, 9 | subs: [] 10 | } 11 | }) 12 | 13 | // 3.将subs添加到对应的类别中 14 | for (let item of data.sub) { 15 | categoryData[item.category].subs.push(item); 16 | } 17 | 18 | return categoryData; 19 | } 20 | 21 | // 获取歌手字母类别 22 | export function generateSingerAlpha() { 23 | var alphabets = ["-1"]; 24 | var start = 'A'.charCodeAt(0); 25 | var last = 'Z'.charCodeAt(0); 26 | for (var i = start; i <= last; ++i) { 27 | alphabets.push(String.fromCharCode(i)); 28 | } 29 | 30 | alphabets.push("0"); 31 | 32 | return alphabets; 33 | } 34 | 35 | export const singerAlphas = generateSingerAlpha(); 36 | -------------------------------------------------------------------------------- /src/utils/lrc-parse.js: -------------------------------------------------------------------------------- 1 | /** 2 | [00:00.000] 作曲 : 许嵩 3 | [00:01.000] 作词 : 许嵩 4 | [00:22.240]天空好想下雨 5 | [00:24.380]我好想住你隔壁 6 | [00:26.810]傻站在你家楼下 7 | [00:29.500]抬起头数乌云 8 | [00:31.160]如果场景里出现一架钢琴 9 | [00:33.640]我会唱歌给你听 10 | [00:35.900]哪怕好多盆水往下淋 11 | [00:41.060]夏天快要过去} 12 | */ 13 | 14 | // [00:31.160]如果场景里出现一架钢琴 15 | const parseExp = /\[(\d{2}):(\d{2})\.(\d{2,3})\]/; 16 | 17 | export function parseLyric(lyricString) { 18 | const lineStrings = lyricString.split("\n"); 19 | 20 | const lyrics = []; 21 | for (let line of lineStrings) { 22 | if (line) { 23 | const result = parseExp.exec(line); 24 | if (!result) continue; 25 | const time1 = result[1] * 60 * 1000; 26 | const time2 = result[2] * 1000; 27 | const time3 = result[3].length === 3? result[3]*1: result[3]*10; 28 | const time = time1 + time2 + time3; 29 | const content = line.replace(parseExp, "").trim(); 30 | const lineObj = {time, content}; 31 | lyrics.push(lineObj); 32 | } 33 | } 34 | return lyrics; 35 | } -------------------------------------------------------------------------------- /src/utils/ui-helper.js: -------------------------------------------------------------------------------- 1 | export function scrollTo(element, to, duration) { 2 | if (duration <= 0) return; 3 | var difference = to - element.scrollTop; 4 | var perTick = difference / duration * 10; 5 | 6 | setTimeout(function() { 7 | element.scrollTop = element.scrollTop + perTick; 8 | if (element.scrollTop === to) return; 9 | scrollTo(element, to, duration - 10); 10 | }, 10); 11 | } --------------------------------------------------------------------------------