├── .eslintignore ├── icons ├── icomoon.zip ├── play-list.svg ├── pause.svg ├── play.svg ├── back.svg ├── search.svg ├── fe-music.svg ├── et-more.svg ├── music.svg ├── right.svg ├── user.svg ├── shuffle-play.svg ├── list-play.svg ├── next.svg ├── single-play.svg ├── previous.svg └── delete.svg ├── public ├── favicon.ico ├── manifest.json └── index.html ├── src ├── assets │ ├── imgs │ │ ├── logo.png │ │ ├── music.png │ │ └── play_bg.jpg │ └── stylus │ │ ├── fonts │ │ ├── icomusic.eot │ │ ├── icomusic.ttf │ │ ├── icomusic.woff │ │ └── icomusic.svg │ │ ├── reset.styl │ │ └── font.styl ├── components │ ├── scroll │ │ ├── scroll.styl │ │ └── Scroll.js │ ├── loading │ │ ├── loading.gif │ │ ├── loading.styl │ │ └── Loading.js │ ├── note │ │ ├── musicalnote.styl │ │ └── MusicalNote.js │ └── header │ │ ├── header.styl │ │ └── Header.js ├── index.css ├── redux │ ├── actionTypes.js │ ├── store.js │ ├── actions.js │ ├── storageMiddleware.js │ └── reducers.js ├── views │ ├── Root.js │ ├── play │ │ ├── progress.styl │ │ ├── MusicPlayer.js │ │ ├── miniplayer.styl │ │ ├── MiniPlayer.js │ │ ├── playerlist.styl │ │ ├── Progress.js │ │ ├── player.styl │ │ └── PlayerList.js │ ├── ranking │ │ ├── ranking.styl │ │ ├── rankinginfo.styl │ │ ├── Ranking.js │ │ └── RankingInfo.js │ ├── setting │ │ ├── menu.styl │ │ ├── Menu.js │ │ ├── skin.styl │ │ └── Skin.js │ ├── app.styl │ ├── singer │ │ ├── singerlist.styl │ │ ├── singer.styl │ │ ├── Singer.js │ │ └── SingerList.js │ ├── recommend │ │ ├── recommend.styl │ │ └── Recommend.js │ ├── album │ │ ├── album.styl │ │ └── Album.js │ ├── App.js │ └── search │ │ ├── search.styl │ │ └── Search.js ├── containers │ ├── Skin.js │ ├── Search.js │ ├── Ranking.js │ ├── Album.js │ ├── Singer.js │ ├── PlayerList.js │ └── Player.js ├── util │ ├── event.js │ ├── storage.js │ └── skin.js ├── index.js ├── api │ ├── song.js │ ├── jsonp.js │ ├── ranking.js │ ├── singer.js │ ├── search.js │ ├── config.js │ └── recommend.js ├── models │ ├── song.js │ ├── singer.js │ ├── ranking.js │ └── album.js ├── api.test.js ├── router │ └── index.js └── registerServiceWorker.js ├── .gitignore ├── package.json ├── README.md ├── config ├── paths.js ├── env.js └── webpackDevServer.config.js ├── scripts ├── start.js └── build.js └── music_api.html /.eslintignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /build 3 | /config 4 | /scripts 5 | 6 | *.css 7 | *.html -------------------------------------------------------------------------------- /icons/icomoon.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxx/mango-music/HEAD/icons/icomoon.zip -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxx/mango-music/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/assets/imgs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxx/mango-music/HEAD/src/assets/imgs/logo.png -------------------------------------------------------------------------------- /src/assets/imgs/music.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxx/mango-music/HEAD/src/assets/imgs/music.png -------------------------------------------------------------------------------- /src/assets/imgs/play_bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxx/mango-music/HEAD/src/assets/imgs/play_bg.jpg -------------------------------------------------------------------------------- /src/components/scroll/scroll.styl: -------------------------------------------------------------------------------- 1 | .scroll-view 2 | width: 100% 3 | height: 100% 4 | overflow: hidden -------------------------------------------------------------------------------- /src/assets/stylus/fonts/icomusic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxx/mango-music/HEAD/src/assets/stylus/fonts/icomusic.eot -------------------------------------------------------------------------------- /src/assets/stylus/fonts/icomusic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxx/mango-music/HEAD/src/assets/stylus/fonts/icomusic.ttf -------------------------------------------------------------------------------- /src/components/loading/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxx/mango-music/HEAD/src/components/loading/loading.gif -------------------------------------------------------------------------------- /src/assets/stylus/fonts/icomusic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxx/mango-music/HEAD/src/assets/stylus/fonts/icomusic.woff -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | 4 | # testing 5 | /coverage 6 | 7 | # production 8 | /build 9 | 10 | # misc 11 | .DS_Store 12 | .env.local 13 | .env.development.local 14 | .env.test.local 15 | .env.production.local 16 | 17 | npm-debug.log* 18 | yarn-debug.log* 19 | yarn-error.log* 20 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | html, body, #root { 2 | width: 100%; 3 | height: 100%; 4 | } 5 | body { 6 | margin: 0; 7 | padding: 0; 8 | font-family: Arial, Helvetica, sans-serif; 9 | font-size: 16px; 10 | -webkit-tap-highlight-color: transparent; 11 | } 12 | ::-webkit-scrollbar { 13 | width: 0; 14 | height: 0; 15 | } 16 | -------------------------------------------------------------------------------- /src/components/note/musicalnote.styl: -------------------------------------------------------------------------------- 1 | .music-ico 2 | position: fixed 3 | z-index: 1000 4 | margin-top: -7px 5 | margin-left: -7px 6 | color: #FFD700 7 | font-size: 14px 8 | display: none 9 | transition: transform 1s cubic-bezier(.59, -0.1, .83, .67) 10 | transform: translate3d(0, 0, 0) 11 | div 12 | transition: transform 1s 13 | -------------------------------------------------------------------------------- /src/redux/actionTypes.js: -------------------------------------------------------------------------------- 1 | // 设置皮肤 2 | export const SET_SKIN = "SET_SKIN"; 3 | 4 | // 显示或隐藏播放页面 5 | export const SHOW_PLAYER = "SHOW_PLAYER"; 6 | 7 | // 修改当前歌曲 8 | export const CHANGE_SONG = "CHANGE_SONG"; 9 | 10 | // 从歌曲列表中移除歌曲 11 | export const REMOVE_SONG_FROM_LIST = "REMOVE_SONG"; 12 | 13 | // 设置歌曲列表 14 | export const SET_SONGS = "SET_SONGS"; 15 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Mango Music", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /src/components/loading/loading.styl: -------------------------------------------------------------------------------- 1 | .loading-container 2 | position: absolute 3 | top: 0 4 | left: 0 5 | width: 100% 6 | height: 100% 7 | z-index: 999 8 | display: flex 9 | justify-content: center 10 | align-items: center 11 | .loading-wrapper 12 | display: inline-block 13 | font-size: 12px 14 | text-align: center 15 | .loading-title 16 | margin-top: 5px -------------------------------------------------------------------------------- /src/views/Root.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { Provider } from "react-redux" 3 | import store from "../redux/store" 4 | import App from "./App" 5 | 6 | import "../util/skin" 7 | 8 | class Root extends React.Component { 9 | render() { 10 | return ( 11 | 12 | 13 | 14 | ); 15 | } 16 | } 17 | 18 | export default Root 19 | -------------------------------------------------------------------------------- /src/components/header/header.styl: -------------------------------------------------------------------------------- 1 | .music-header 2 | position: fixed 3 | width: 100% 4 | height: 50px 5 | color: #FFFFFF 6 | text-align: center 7 | font-size: 18px 8 | .header-back 9 | position: absolute 10 | top: 14px 11 | left: 10px 12 | font-size: 22px 13 | .header-title 14 | margin: 0 40px 15 | line-height: 50px 16 | overflow: hidden 17 | text-overflow: ellipsis 18 | white-space: nowrap -------------------------------------------------------------------------------- /src/containers/Skin.js: -------------------------------------------------------------------------------- 1 | import { connect } from "react-redux" 2 | import { setSkin } from "../redux/actions" 3 | import Skin from "../views/setting/Skin" 4 | 5 | const mapStateToProps = (state) => ({ 6 | currentSkin: state.skin 7 | }); 8 | 9 | const mapDispatchToProps = (dispatch) => ({ 10 | setSkin: (skin) => { 11 | dispatch(setSkin(skin)); 12 | } 13 | }); 14 | 15 | export default connect(mapStateToProps, mapDispatchToProps)(Skin) 16 | -------------------------------------------------------------------------------- /src/util/event.js: -------------------------------------------------------------------------------- 1 | function getTransitionEndName(dom) { 2 | let cssTransition = ["transition", "webkitTransition"]; 3 | let transitionEnd = { 4 | "transition": "transitionend", 5 | "webkitTransition": "webkitTransitionEnd" 6 | }; 7 | for (let i = 0; i < cssTransition.length; i++) { 8 | if (dom.style[cssTransition[i]] !== undefined) { 9 | return transitionEnd[cssTransition[i]]; 10 | } 11 | } 12 | return undefined; 13 | } 14 | 15 | export { getTransitionEndName } 16 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import ReactDOM from "react-dom" 3 | import Root from "./views/Root" 4 | import registerServiceWorker from "./registerServiceWorker" 5 | import "./index.css" 6 | 7 | ReactDOM.render(, document.getElementById("root")); 8 | registerServiceWorker(); 9 | 10 | if (module.hot) { 11 | module.hot.accept("./views/Root", () => { 12 | const NewApp = require("./views/Root").default; 13 | ReactDOM.render(, document.getElementById("root")); 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /src/containers/Search.js: -------------------------------------------------------------------------------- 1 | import { connect } from "react-redux" 2 | import { showPlayer, changeSong, setSongs } from "../redux/actions" 3 | import Search from "../views/search/Search" 4 | 5 | const mapDispatchToProps = (dispatch) => ({ 6 | showMusicPlayer: (show) => { 7 | dispatch(showPlayer(show)); 8 | }, 9 | changeCurrentSong: (song) => { 10 | dispatch(changeSong(song)); 11 | }, 12 | setSongs: (songs) => { 13 | dispatch(setSongs(songs)); 14 | } 15 | }); 16 | 17 | export default connect(null, mapDispatchToProps)(Search) 18 | -------------------------------------------------------------------------------- /src/redux/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from "redux" 2 | import logger from "redux-logger" 3 | import reducer from "./reducers" 4 | import storageMiddleware from "./storageMiddleware" 5 | 6 | let useMiddleware = applyMiddleware(storageMiddleware) // 使用中间件 7 | 8 | if (process.env.NODE_ENV === "development") { 9 | // 开发环境使用redux-logger 10 | useMiddleware = applyMiddleware(storageMiddleware, logger); 11 | } 12 | 13 | // 创建store 14 | const store = createStore( 15 | reducer, 16 | useMiddleware 17 | ); 18 | export default store 19 | -------------------------------------------------------------------------------- /src/api/song.js: -------------------------------------------------------------------------------- 1 | import jsonp from "./jsonp" 2 | import { URL, PARAM } from "./config" 3 | 4 | export function getSongVKey(songMid) { 5 | const data = Object.assign({}, PARAM, { 6 | g_tk: 1278911659, 7 | hostUin: 0, 8 | platform: "yqq", 9 | needNewCode: 0, 10 | cid: 205361747, 11 | uin: 0, 12 | songmid: songMid, 13 | filename: `C400${songMid}.m4a`, 14 | guid: 3655047200 15 | }); 16 | const option = { 17 | param: "callback", 18 | prefix: "callback" 19 | }; 20 | return jsonp(URL.songVkey, data, option); 21 | } 22 | -------------------------------------------------------------------------------- /src/containers/Ranking.js: -------------------------------------------------------------------------------- 1 | import { connect } from "react-redux" 2 | import { showPlayer, changeSong, setSongs } from "../redux/actions" 3 | import RankingInfo from "../views/ranking/RankingInfo" 4 | 5 | const mapDispatchToProps = (dispatch) => ({ 6 | showMusicPlayer: (show) => { 7 | dispatch(showPlayer(show)); 8 | }, 9 | changeCurrentSong: (song) => { 10 | dispatch(changeSong(song)); 11 | }, 12 | setSongs: (songs) => { 13 | dispatch(setSongs(songs)); 14 | } 15 | }); 16 | 17 | export default connect(null, mapDispatchToProps)(RankingInfo) 18 | -------------------------------------------------------------------------------- /src/containers/Album.js: -------------------------------------------------------------------------------- 1 | import { connect } from "react-redux" 2 | import { showPlayer, changeSong, setSongs } from "../redux/actions" 3 | import Album from "../views/album/Album" 4 | 5 | // 映射dispatch到props上 6 | const mapDispatchToProps = (dispatch) => ({ 7 | showMusicPlayer: (status) => { 8 | dispatch(showPlayer(status)); 9 | }, 10 | changeCurrentSong: (song) => { 11 | dispatch(changeSong(song)); 12 | }, 13 | setSongs: (songs) => { 14 | dispatch(setSongs(songs)); 15 | } 16 | }); 17 | 18 | export default connect(null, mapDispatchToProps)(Album) 19 | -------------------------------------------------------------------------------- /src/containers/Singer.js: -------------------------------------------------------------------------------- 1 | import { connect } from "react-redux" 2 | import { showPlayer, changeSong, setSongs } from "../redux/actions" 3 | import Singer from "../views/singer/Singer" 4 | 5 | // 映射dispatch到props上 6 | const mapDispatchToProps = (dispatch) => ({ 7 | showMusicPlayer: (status) => { 8 | dispatch(showPlayer(status)); 9 | }, 10 | changeCurrentSong: (song) => { 11 | dispatch(changeSong(song)); 12 | }, 13 | setSongs: (songs) => { 14 | dispatch(setSongs(songs)); 15 | } 16 | }); 17 | 18 | export default connect(null, mapDispatchToProps)(Singer) 19 | -------------------------------------------------------------------------------- /icons/play-list.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/header/Header.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import style from "./header.styl?module" 3 | 4 | class MusicHeader extends React.Component { 5 | handleClick() { 6 | window.history.back(); 7 | } 8 | render() { 9 | return ( 10 |
11 | 12 | 13 | 14 |
15 | {this.props.title} 16 |
17 |
18 | ); 19 | } 20 | } 21 | 22 | export default MusicHeader 23 | -------------------------------------------------------------------------------- /src/containers/PlayerList.js: -------------------------------------------------------------------------------- 1 | import { connect } from "react-redux" 2 | import { changeSong, removeSong } from "../redux/actions" 3 | import PlayerList from "../views/play/PlayerList" 4 | 5 | // 映射Redux全局的state到组件的props上 6 | const mapStateToProps = (state) => ({ 7 | currentSong: state.song, 8 | playSongs: state.songs 9 | }); 10 | 11 | // 映射dispatch到props上 12 | const mapDispatchToProps = (dispatch) => ({ 13 | changeCurrentSong: (song) => { 14 | dispatch(changeSong(song)); 15 | }, 16 | removeSong: (id) => { 17 | dispatch(removeSong(id)); 18 | } 19 | }); 20 | 21 | // 将ui组件包装成容器组件 22 | export default connect(mapStateToProps, mapDispatchToProps)(PlayerList) 23 | -------------------------------------------------------------------------------- /src/views/play/progress.styl: -------------------------------------------------------------------------------- 1 | $zIndex = 1 2 | .progress-bar 3 | position: relative 4 | width: 100% 5 | height: 3px 6 | background-color: rgba(0, 0, 0, 0.3); 7 | .progress-load, .progress, .progress-button 8 | position: absolute 9 | top: 0 10 | left: 0 11 | height: 100% 12 | .progress-load 13 | z-index: $zIndex 14 | .progress 15 | z-index: $zIndex + 1 16 | background-color: #FFD700 17 | .progress-button 18 | width: 14px 19 | height: 14px 20 | border: 2px solid #FFFFFF 21 | border-radius: 50% 22 | box-sizing: border-box 23 | background-color: #FFD700 24 | z-index: $zIndex + 2 25 | top: -6px 26 | margin-left: -5px 27 | -------------------------------------------------------------------------------- /src/api/jsonp.js: -------------------------------------------------------------------------------- 1 | import originJsonp from "jsonp" 2 | 3 | let jsonp = (url, data, option) => { 4 | return new Promise((resolve, reject) => { 5 | originJsonp(buildUrl(url, data), option, (err, data) => { 6 | if (!err) { 7 | resolve(data); 8 | } else { 9 | reject(err); 10 | } 11 | }); 12 | }); 13 | }; 14 | 15 | function buildUrl(url, data) { 16 | let params = []; 17 | for (var k in data) { 18 | params.push(`${k}=${data[k]}`); 19 | } 20 | let param = params.join("&"); 21 | if (url.indexOf("?") === -1) { 22 | url += "?" + param; 23 | } else { 24 | url += "&" + param; 25 | } 26 | return url; 27 | } 28 | 29 | export default jsonp 30 | -------------------------------------------------------------------------------- /src/components/loading/Loading.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import loadingImg from "./loading.gif" 3 | import style from "./loading.styl?module" 4 | 5 | class Loading extends React.Component { 6 | render() { 7 | let displayStyle = this.props.show === true ? 8 | { display: "" } : { display: "none" }; 9 | return ( 10 |
11 |
12 | loading 13 |
{this.props.title}
14 |
15 |
16 | ); 17 | } 18 | } 19 | 20 | export default Loading 21 | -------------------------------------------------------------------------------- /src/containers/Player.js: -------------------------------------------------------------------------------- 1 | import { connect } from "react-redux" 2 | import { showPlayer, changeSong } from "../redux/actions" 3 | import Player from "../views/play/Player" 4 | 5 | // 映射Redux全局的state到组件的props上 6 | const mapStateToProps = (state) => ({ 7 | showStatus: state.showStatus, 8 | currentSong: state.song, 9 | playSongs: state.songs 10 | }); 11 | 12 | // 映射dispatch到props上 13 | const mapDispatchToProps = (dispatch) => ({ 14 | showMusicPlayer: (status) => { 15 | dispatch(showPlayer(status)); 16 | }, 17 | changeCurrentSong: (song) => { 18 | dispatch(changeSong(song)); 19 | } 20 | }); 21 | 22 | // 将ui组件包装成容器组件 23 | export default connect(mapStateToProps, mapDispatchToProps)(Player) 24 | -------------------------------------------------------------------------------- /icons/pause.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/redux/actions.js: -------------------------------------------------------------------------------- 1 | import * as ActionTypes from "./actionTypes" 2 | /** 3 | * Action是把数据从应用传到store的有效载荷。它是store数据的唯一来源 4 | */ 5 | 6 | // Action创建函数,用来创建action对象。使用action创建函数更容易被移植和测试 7 | 8 | export function setSkin(skin) { 9 | return { type: ActionTypes.SET_SKIN, skin }; 10 | } 11 | 12 | export function showPlayer(showStatus) { 13 | return { type: ActionTypes.SHOW_PLAYER, showStatus }; 14 | } 15 | 16 | export function changeSong(song) { 17 | return { type: ActionTypes.CHANGE_SONG, song }; 18 | } 19 | 20 | export function removeSong(id) { 21 | return { type: ActionTypes.REMOVE_SONG_FROM_LIST, id }; 22 | } 23 | 24 | export function setSongs(songs) { 25 | return { type: ActionTypes.SET_SONGS, songs }; 26 | } 27 | -------------------------------------------------------------------------------- /src/api/ranking.js: -------------------------------------------------------------------------------- 1 | import jsonp from "./jsonp" 2 | import { URL, PARAM, OPTION } from "./config" 3 | 4 | export function getRankingList() { 5 | const data = Object.assign({}, PARAM, { 6 | g_tk: 5381, 7 | uin: 0, 8 | platform: "h5", 9 | needNewCode: 1, 10 | _: new Date().getTime() 11 | }); 12 | return jsonp(URL.rankingList, data, OPTION); 13 | } 14 | 15 | export function getRankingInfo(topId) { 16 | const data = Object.assign({}, PARAM, { 17 | g_tk: 5381, 18 | uin: 0, 19 | platform: "h5", 20 | needNewCode: 1, 21 | tpl: 3, 22 | page: "detail", 23 | type: "top", 24 | topid: topId, 25 | _: new Date().getTime() 26 | }); 27 | return jsonp(URL.rankingInfo, data, OPTION); 28 | } 29 | -------------------------------------------------------------------------------- /icons/play.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/util/storage.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 本地持久化对象 3 | */ 4 | let localStorage = { 5 | setSkin(key) { 6 | window.localStorage.setItem("skin", key); 7 | }, 8 | getSkin() { 9 | let skin = window.localStorage.getItem("skin"); 10 | return !skin ? "coolBlack" : skin; 11 | }, 12 | setCurrentSong(song) { 13 | window.localStorage.setItem("song", JSON.stringify(song)); 14 | }, 15 | getCurrentSong() { 16 | let song = window.localStorage.getItem("song"); 17 | return song ? JSON.parse(song) : {}; 18 | }, 19 | setSongs(songs) { 20 | window.localStorage.setItem("songs", JSON.stringify(songs)); 21 | }, 22 | getSongs() { 23 | let songs = window.localStorage.getItem("songs"); 24 | return songs ? JSON.parse(songs) : []; 25 | } 26 | } 27 | 28 | export default localStorage 29 | -------------------------------------------------------------------------------- /src/models/song.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 歌曲类模型 3 | */ 4 | export class Song { 5 | constructor(id, mId, name, img, duration, url, singer) { 6 | this.id = id; 7 | this.mId = mId; 8 | this.name = name; 9 | this.img = img; 10 | this.duration = duration; 11 | this.url = url; 12 | this.singer = singer; 13 | } 14 | } 15 | 16 | /** 17 | * 创建歌曲对象函数 18 | */ 19 | export function createSong(data) { 20 | return new Song( 21 | data.songid, 22 | data.songmid, 23 | data.songname, 24 | `http://y.gtimg.cn/music/photo_new/T002R300x300M000${data.albummid}.jpg?max_age=2592000`, 25 | data.interval, 26 | "", 27 | filterSinger(data.singer) 28 | ); 29 | } 30 | 31 | function filterSinger(singers) { 32 | let singerArray = singers.map(singer => { 33 | return singer.name; 34 | }); 35 | return singerArray.join("/"); 36 | } 37 | -------------------------------------------------------------------------------- /src/models/singer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 歌手类模型 3 | */ 4 | export class Singer { 5 | constructor(id, mId, name, img) { 6 | this.id = id; 7 | this.mId = mId; 8 | this.name = name; 9 | this.img = img; 10 | } 11 | } 12 | 13 | /** 14 | * 通过搜索创建歌手对象函数 15 | */ 16 | export function createSingerBySearch(data) { 17 | return new Singer( 18 | data.singerID, 19 | data.singerMID, 20 | data.singerName, 21 | // `http://y.gtimg.cn/music/photo_new/T001R68x68M000${data.singerMID}.jpg?max_age=2592000` 22 | data.singerPic 23 | ); 24 | } 25 | 26 | /** 27 | * 通过歌手详情创建歌手对象函数 28 | */ 29 | export function createSingerByDetail(data) { 30 | return new Singer( 31 | data.singer_id, 32 | data.singer_mid, 33 | data.singer_name, 34 | `http://y.gtimg.cn/music/photo_new/T001R300x300M000${data.singer_mid}.jpg?max_age=2592000` 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /src/api/singer.js: -------------------------------------------------------------------------------- 1 | import jsonp from "./jsonp" 2 | import { URL, PARAM, OPTION } from "./config" 3 | 4 | export function getSingerList(pageNum, key) { 5 | const data = Object.assign({}, PARAM, { 6 | g_tk: 5381, 7 | loginUin: 0, 8 | hostUin: 0, 9 | platform: "yqq", 10 | needNewCode: 0, 11 | channel: "singer", 12 | page: "list", 13 | key, 14 | pagenum: pageNum, 15 | pagesize: 100 16 | }); 17 | return jsonp(URL.singerList, data, OPTION); 18 | } 19 | 20 | export function getSingerInfo(mId) { 21 | const data = Object.assign({}, PARAM, { 22 | g_tk: 5381, 23 | loginUin: 0, 24 | hostUin: 0, 25 | platform: "yqq", 26 | needNewCode: 0, 27 | singermid: mId, 28 | order: "listen", 29 | begin: 0, 30 | num: 100, 31 | songstatus: 1 32 | }); 33 | return jsonp(URL.singerInfo, data, OPTION); 34 | } 35 | -------------------------------------------------------------------------------- /src/models/ranking.js: -------------------------------------------------------------------------------- 1 | import * as SongModel from "./song" 2 | /** 3 | * 排行榜类模型 4 | */ 5 | export class Ranking { 6 | constructor(id, title, img, songs) { 7 | this.id = id; 8 | this.title = title; 9 | this.img = img; 10 | this.songs = songs; 11 | } 12 | } 13 | 14 | /** 15 | * 通过排行榜列表创建排行榜对象函数 16 | */ 17 | export function createRankingByList(data) { 18 | const songList = []; 19 | data.songList.forEach(item => { 20 | songList.push(new SongModel.Song(0, "", item.songname, "", 0, "", item.singername)); 21 | }); 22 | return new Ranking( 23 | data.id, 24 | data.topTitle, 25 | data.picUrl, 26 | songList 27 | ); 28 | } 29 | 30 | /** 31 | * 通过排行榜详情创建排行榜对象函数 32 | */ 33 | export function createRankingByDetail(data) { 34 | return new Ranking( 35 | data.topID, 36 | data.ListName, 37 | data.pic_v12, 38 | [] 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /src/redux/storageMiddleware.js: -------------------------------------------------------------------------------- 1 | import * as ActionTypes from "./actionTypes" 2 | import localStorage from "../util/storage" 3 | 4 | /** 5 | * 本地存储中间件 6 | */ 7 | const storage = store => next => action => { 8 | let result = next(action); 9 | switch (action.type) { 10 | case ActionTypes.CHANGE_SONG: 11 | // 设置当前歌曲 12 | localStorage.setCurrentSong(action.song); 13 | break; 14 | case ActionTypes.SET_SONGS: 15 | // 设置播放歌曲列表 16 | localStorage.setSongs(store.getState().songs); 17 | break; 18 | case ActionTypes.REMOVE_SONG_FROM_LIST: 19 | // 移除歌曲 20 | let newSongs = store.getState().songs.filter(song => song.id !== action.id); 21 | localStorage.setSongs(newSongs); 22 | break; 23 | case ActionTypes.SET_SKIN: 24 | // 设置皮肤 25 | localStorage.setSkin(action.skin); 26 | break; 27 | default: 28 | } 29 | return result; 30 | } 31 | 32 | export default storage 33 | -------------------------------------------------------------------------------- /src/views/ranking/ranking.styl: -------------------------------------------------------------------------------- 1 | :global(.music-ranking) 2 | width: 100% 3 | height: 100% 4 | .ranking-list 5 | padding-bottom: 15px 6 | .ranking-wrapper 7 | display: flex 8 | margin: 15px 9 | /*background-color: #333333*/ 10 | .left 11 | flex: 0 0 100px 12 | width: w = 100px 13 | height: h = @width 14 | img 15 | width: w 16 | height: h 17 | .right 18 | flex: 1 19 | line-height: 22px 20 | font-size: 14px 21 | padding: 0 10px 22 | overflow: hidden 23 | :global(.ranking-title) 24 | font-size: 16px 25 | /*color: #FFFFFF*/ 26 | margin: 4px 0 6px 0 27 | .top-song, :global(.ranking-title) 28 | overflow: hidden 29 | text-overflow: ellipsis 30 | white-space: nowrap 31 | .index 32 | margin-right: 10px 33 | /*.singer 34 | color: rgba(221,221,221,0.7)*/ -------------------------------------------------------------------------------- /src/views/setting/menu.styl: -------------------------------------------------------------------------------- 1 | .bottom-container 2 | position: fixed 3 | top: 0 4 | left: 0 5 | width: 100% 6 | height: @width 7 | z-index: 9999 8 | background-color: rgba(0, 0, 0, .7) 9 | display: none 10 | &:global(.fade-enter) 11 | opacity: 0 12 | &:global(.fade-enter-active) 13 | transition: opacity .3s 14 | transform: translate3d(0, 0, 0) 15 | opacity: 1 16 | &:global(.fade-exit) 17 | transition: opacity .3s 18 | transform: translate3d(0, 0, 0) 19 | opacity: 1 20 | &:global(.fade-exit-active) 21 | opacity: 0 22 | .bottom-wrapper 23 | position: absolute 24 | bottom: 0 25 | width: 100% 26 | text-align: center 27 | font-size: 15px 28 | color: #333333 29 | background-color: #EEEEEE 30 | div 31 | height: 40px 32 | line-height: 40px 33 | background-color: #FFFFFF 34 | .item-close 35 | margin-top: 10px -------------------------------------------------------------------------------- /icons/back.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /icons/search.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/views/play/MusicPlayer.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import Player from "../../containers/Player" 3 | import PlayerList from "../../containers/PlayerList" 4 | 5 | 6 | class MusicPlayer extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | 10 | this.state = { 11 | currentSongIndex: 0, 12 | show: false, // 控制播放列表显示和隐藏 13 | } 14 | } 15 | changeCurrentIndex = (index) => { 16 | this.setState({ 17 | currentSongIndex: index 18 | }); 19 | } 20 | showList = (status) => { 21 | this.setState({ 22 | show: status 23 | }); 24 | } 25 | render() { 26 | return ( 27 |
28 | 31 | 35 |
36 | ); 37 | } 38 | } 39 | 40 | export default MusicPlayer 41 | -------------------------------------------------------------------------------- /icons/fe-music.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /icons/et-more.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/stylus/reset.styl: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, body, div, span, applet, object, iframe, 7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 8 | a, abbr, acronym, address, big, cite, code, 9 | del, dfn, em, img, ins, kbd, q, s, samp, 10 | small, strike, strong, sub, sup, tt, var, 11 | b, u, i, center, 12 | dl, dt, dd, ol, ul, li, 13 | fieldset, form, label, legend, 14 | table, caption, tbody, tfoot, thead, tr, th, td, 15 | article, aside, canvas, details, embed, 16 | figure, figcaption, footer, header, hgroup, 17 | menu, nav, output, ruby, section, summary, 18 | time, mark, audio, video 19 | margin: 0 20 | padding: 0 21 | border: 0 22 | vertical-align: baseline 23 | 24 | /* HTML5 display-role reset for older browsers */ 25 | article, aside, details, figcaption, figure, 26 | footer, header, hgroup, menu, nav, section 27 | display: block 28 | 29 | body 30 | line-height: 1 31 | 32 | ol, ul 33 | list-style: none 34 | 35 | blockquote, q 36 | quotes: none 37 | 38 | blockquote:before, blockquote:after, 39 | q:before, q:after 40 | content: '' 41 | content: none 42 | 43 | table 44 | border-collapse: collapse 45 | border-spacing: 0 46 | -------------------------------------------------------------------------------- /icons/music.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /icons/right.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/api/search.js: -------------------------------------------------------------------------------- 1 | import jsonp from "./jsonp" 2 | import { URL, PARAM, OPTION } from "./config" 3 | 4 | export function getHotKey() { 5 | const data = Object.assign({}, PARAM, { 6 | g_tk: 5381, 7 | uin: 0, 8 | platform: "h5", 9 | needNewCode: 1, 10 | notice: 0, 11 | _: new Date().getTime() 12 | }); 13 | 14 | return jsonp(URL.hotkey, data, OPTION); 15 | } 16 | 17 | /*export function search(w) { 18 | const data = Object.assign({}, PARAM, { 19 | g_tk: 5381, 20 | uin: 0, 21 | platform: "h5", 22 | needNewCode: 1, 23 | notice: 0, 24 | zhidaqu: 1, 25 | catZhida: 1, 26 | t: 0, 27 | flag: 1, 28 | ie: "utf-8", 29 | sem: 1, 30 | aggr: 0, 31 | perpage: 20, 32 | n: 20, 33 | p: 1, 34 | w, 35 | remoteplace: "txt.mqq.all", 36 | _: new Date().getTime() 37 | }); 38 | 39 | return jsonp(URL.search, data, OPTION); 40 | }*/ 41 | 42 | export function search(w) { 43 | const data = Object.assign({}, PARAM, { 44 | g_tk: 5381, 45 | platform: "h5", 46 | needNewCode: 0, 47 | catZhida: 1, 48 | cr: 1, 49 | t: 0, 50 | flag_qc: 0, 51 | aggr: 0, 52 | n: 20, 53 | p: 1, 54 | w, 55 | remoteplace: "txt.yqq.song", 56 | _: new Date().getTime() 57 | }); 58 | 59 | return jsonp(URL.search, data, OPTION); 60 | } 61 | -------------------------------------------------------------------------------- /src/views/app.styl: -------------------------------------------------------------------------------- 1 | .app 2 | width: 100% 3 | height: 100% 4 | /*color: #DDDDDD 5 | background-color: #212121*/ 6 | .app-header 7 | height: 50px 8 | line-height: 50px 9 | text-align: center 10 | position: relative 11 | .app-more 12 | position: absolute 13 | top: 15px 14 | left: 15px 15 | font-size: 20px 16 | .app-logo 17 | width: 30px 18 | height: 25px 19 | margin-top: -5px 20 | vertical-align: middle 21 | .app-title 22 | display: inline-block 23 | height: 55px 24 | margin-left: 10px 25 | font-size: 18px 26 | font-weight: 300 27 | .music-tab 28 | display: flex 29 | line-height: 30px 30 | text-align: center 31 | padding-top: 2px 32 | /*color: #DDDDDD*/ 33 | .tab-item 34 | flex: 1 35 | .music-view 36 | position: fixed 37 | top: 84px 38 | left: 0 39 | bottom: 52px 40 | width: 100% 41 | 42 | :global(.nav-link), :global(.link) 43 | display: block 44 | color: inherit 45 | text-decoration: none 46 | /*&.active 47 | color: #FFD700 48 | border-bottom: 2px solid #FFD700*/ 49 | 50 | /*子路由动画*/ 51 | :global(.translate-enter) 52 | transform: translate3d(100%, 0, 0) 53 | &:global(.translate-enter-active) 54 | transition: transform .3s 55 | transform: translate3d(0, 0, 0) 56 | -------------------------------------------------------------------------------- /src/api/config.js: -------------------------------------------------------------------------------- 1 | const URL = { 2 | /* 推荐轮播 */ 3 | carousel: "https://c.y.qq.com/musichall/fcgi-bin/fcg_yqqhomepagerecommend.fcg", 4 | /* 最新专辑 */ 5 | // newalbum: "https://c.y.qq.com/v8/fcg-bin/album_library", 6 | newalbum: "https://u.y.qq.com/cgi-bin/musicu.fcg", 7 | /* 专辑信息 */ 8 | albumInfo: "https://c.y.qq.com/v8/fcg-bin/fcg_v8_album_info_cp.fcg", 9 | /* 排行榜 */ 10 | rankingList: "https://c.y.qq.com/v8/fcg-bin/fcg_myqq_toplist.fcg", 11 | /* 排行榜详情 */ 12 | rankingInfo: "https://c.y.qq.com/v8/fcg-bin/fcg_v8_toplist_cp.fcg", 13 | /* 搜索 */ 14 | // search: "https://c.y.qq.com/soso/fcgi-bin/search_for_qq_cp", 15 | search: "https://c.y.qq.com/soso/fcgi-bin/client_search_cp", 16 | /* 热搜 */ 17 | hotkey: "https://c.y.qq.com/splcloud/fcgi-bin/gethotkey.fcg", 18 | /* 歌手列表 */ 19 | singerList: "https://c.y.qq.com/v8/fcg-bin/v8.fcg", 20 | /* 歌手详情 */ 21 | singerInfo: "https://c.y.qq.com/v8/fcg-bin/fcg_v8_singer_track_cp.fcg", 22 | /* 歌曲vkey */ 23 | songVkey: "https://c.y.qq.com/base/fcgi-bin/fcg_music_express_mobile3.fcg" 24 | }; 25 | 26 | const PARAM = { 27 | format: "jsonp", 28 | inCharset: "utf-8", 29 | outCharset: "utf-8", 30 | notice: 0 31 | }; 32 | 33 | const OPTION = { 34 | param: "jsonpCallback", 35 | prefix: "callback" 36 | }; 37 | 38 | const CODE_SUCCESS = 0; 39 | 40 | export { URL, PARAM, OPTION, CODE_SUCCESS } 41 | -------------------------------------------------------------------------------- /src/views/singer/singerlist.styl: -------------------------------------------------------------------------------- 1 | :global(.music-singers) 2 | width: 100% 3 | height: 100% 4 | .nav 5 | padding: 10px 20px 6 | font-size: 14px 7 | /*color: rgba(221,221,221,0.7)*/ 8 | .tag, .index 9 | display: flex 10 | align-items: center 11 | height: 30px 12 | overflow: hidden 13 | span 14 | flex: 0 0 auto 15 | padding: 3px 8px 16 | color: inherit 17 | text-align: center 18 | border: 1px solid transparent 19 | box-sizing: border-box 20 | cursor: default 21 | &:global(.choose) 22 | border: 1px solid #ffd700 23 | border-radius: 10px 24 | /*color: #ffd700*/ 25 | .singer-list 26 | position: absolute 27 | top: 80px 28 | left: 0 29 | right: 0 30 | bottom: 0 31 | padding: 0 20px 32 | .singer-wraper 33 | margin: 10px 0 34 | &:first-child 35 | margin-top: 0 36 | &:last-child 37 | padding-bottom: 10px 38 | .singer-img, .singer-name 39 | display: inline-block 40 | .singer-img 41 | width: 50px 42 | height: @width 43 | img 44 | border-radius: 50% 45 | .singer-name 46 | height: 50px 47 | max-width: 220px 48 | line-height: 50px 49 | margin-left: 10px 50 | vertical-align: top 51 | overflow: hidden 52 | text-overflow: ellipsis 53 | white-space: nowrap 54 | /*color: #FFFFFF*/ -------------------------------------------------------------------------------- /icons/user.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/models/album.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 专辑类模型 3 | */ 4 | export class Album { 5 | constructor(id, mId, name, img, singer, publicTime) { 6 | this.id = id; 7 | this.mId = mId; 8 | this.name = name; 9 | this.img = img; 10 | this.singer = singer; 11 | this.publicTime = publicTime; 12 | } 13 | } 14 | 15 | /** 16 | * 通过专辑列表数据创建专辑对象函数 17 | */ 18 | export function createAlbumByList(data) { 19 | return new Album( 20 | data.album_id, 21 | data.album_mid, 22 | data.album_name, 23 | `http://y.gtimg.cn/music/photo_new/T002R300x300M000${data.album_mid}.jpg?max_age=2592000`, 24 | filterSinger(data.singers), 25 | data.public_time 26 | ); 27 | } 28 | 29 | /** 30 | * 通过专辑详情数据创建专辑对象函数 31 | */ 32 | export function createAlbumByDetail(data) { 33 | return new Album( 34 | data.id, 35 | data.mid, 36 | data.name, 37 | `http://y.gtimg.cn/music/photo_new/T002R300x300M000${data.mid}.jpg?max_age=2592000`, 38 | data.singername, 39 | data.aDate 40 | ); 41 | } 42 | 43 | /** 44 | * 通过搜索创建专辑对象函数 45 | */ 46 | export function createAlbumBySearch(data) { 47 | return new Album( 48 | data.albumID, 49 | data.albumMID, 50 | data.albumName, 51 | // `http://y.gtimg.cn/music/photo_new/T002R68x68M000${data.albumMID}.jpg?max_age=2592000`, 52 | data.albumPic, 53 | data.singerName, 54 | "" 55 | ); 56 | } 57 | 58 | function filterSinger(singers) { 59 | let singerArray = singers.map(singer => { 60 | return singer.singer_name; 61 | }); 62 | return singerArray.join("/"); 63 | } 64 | -------------------------------------------------------------------------------- /icons/shuffle-play.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /icons/list-play.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/views/recommend/recommend.styl: -------------------------------------------------------------------------------- 1 | :global(.music-recommend) 2 | width: 100% 3 | height: 100% 4 | :global(.slider-container) 5 | height: 160px 6 | position: relative 7 | :global(.slider-nav) 8 | display: block 9 | width: 100% 10 | height: 100% 11 | :global(.swiper-pagination-bullet-active) 12 | background-color: #DDDDDD 13 | .album-container 14 | .title 15 | height: 50px 16 | line-height: 50px 17 | text-align: center 18 | font-size: 15px 19 | font-weight: 400 20 | /*color: #FFD700*/ 21 | .album-list 22 | font-size: 15px 23 | padding-bottom: 20px 24 | .album-wrapper 25 | margin: 20px 26 | /*color: rgba(221, 221, 221, 0.7)*/ 27 | .left 28 | float: left 29 | width: 60px 30 | height: 60px 31 | vertical-align: top 32 | .right 33 | margin-left: 80px 34 | line-height: 30px 35 | position: relative 36 | :global(.album-name), .singer-name 37 | max-width: 70% 38 | overflow: hidden 39 | text-overflow: ellipsis 40 | white-space: nowrap 41 | :global(.album-name) 42 | /*color: #FFFFFF*/ 43 | font-weight: 500 44 | .public-time 45 | position: absolute 46 | right: 0 47 | top: 50% 48 | margin-top: -15px 49 | font-size: 14px 50 | .album-wrapper:first-child 51 | margin-top: 0 52 | .album-wrapper:last-child 53 | margin-bottom: 0 54 | -------------------------------------------------------------------------------- /src/views/setting/Menu.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { CSSTransition } from "react-transition-group" 3 | import Skin from "../../containers/Skin" 4 | 5 | import style from "./menu.styl?module" 6 | 7 | class Menu extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | 11 | this.bottomRef = React.createRef(); 12 | 13 | this.state = { 14 | skinShow: false 15 | }; 16 | } 17 | showSetting = (status) => { 18 | this.close(); 19 | // menu关闭后打开设置 20 | setTimeout(() => { 21 | this.setState({ 22 | skinShow: status 23 | }); 24 | }, 300); 25 | } 26 | close = () => { 27 | this.props.closeMenu(); 28 | } 29 | render() { 30 | return ( 31 |
32 | { 34 | this.bottomRef.current.style.display = "block"; 35 | }} 36 | onExited={() => { 37 | this.bottomRef.current.style.display = "none"; 38 | }}> 39 |
40 |
41 |
{ this.showSetting(true); }}> 42 | 皮肤中心 43 |
44 |
45 | 关闭 46 |
47 |
48 |
49 |
50 | { this.showSetting(false); }} /> 51 |
52 | ); 53 | } 54 | } 55 | 56 | export default Menu 57 | -------------------------------------------------------------------------------- /src/api.test.js: -------------------------------------------------------------------------------- 1 | import { getCarousel, getNewAlbum, getAlbumInfo } from "./api/recommend" 2 | import { getRankingList, getRankingInfo } from "./api/ranking" 3 | import { getSongVKey} from "./api/song" 4 | import { getHotKey, search } from "./api/search" 5 | import { getSingerList, getSingerInfo } from "./api/singer" 6 | 7 | getCarousel().then((res) => { 8 | console.log("获取轮播:"); 9 | if (res) { 10 | console.log(res); 11 | } 12 | }); 13 | 14 | getNewAlbum().then((res) => { 15 | console.log("获取最新专辑:"); 16 | if (res) { 17 | console.log(res); 18 | } 19 | }); 20 | 21 | getAlbumInfo("0007kqbv3ZbOtl").then((res) => { 22 | console.log("获取专辑详情:"); 23 | if (res) { 24 | console.log(res); 25 | } 26 | }); 27 | 28 | getRankingList().then((res) => { 29 | console.log("获取排行榜:"); 30 | if (res) { 31 | console.log(res); 32 | } 33 | }); 34 | 35 | getRankingInfo(4).then((res) => { 36 | console.log("获取排行榜详情:"); 37 | if (res) { 38 | console.log(res); 39 | } 40 | }); 41 | 42 | getSongVKey("000OFXjz0Nljbh").then((res) => { 43 | console.log("获取歌曲vkey:"); 44 | if (res) { 45 | console.log(res); 46 | } 47 | }); 48 | 49 | getHotKey().then((res) => { 50 | console.log("获取热搜:"); 51 | if (res) { 52 | console.log(res); 53 | } 54 | }); 55 | 56 | search("欧阳朵").then((res) => { 57 | console.log("搜索:"); 58 | if (res) { 59 | console.log(res); 60 | } 61 | }); 62 | 63 | getSingerList(1, "all_all_all").then((res) => { 64 | console.log("获取歌手列表:"); 65 | if (res) { 66 | console.log(res); 67 | } 68 | }); 69 | 70 | getSingerInfo("001iI8LW0ZRpXn").then((res) => { 71 | console.log("获取歌手详情:"); 72 | if (res) { 73 | console.log(res); 74 | } 75 | }); 76 | -------------------------------------------------------------------------------- /src/api/recommend.js: -------------------------------------------------------------------------------- 1 | import jsonp from "./jsonp" 2 | import { URL, PARAM, OPTION } from "./config" 3 | 4 | export function getCarousel() { 5 | const data = Object.assign({}, PARAM, { 6 | g_tk: 701075963, 7 | uin: 0, 8 | platform: "h5", 9 | needNewCode: 1, 10 | _: new Date().getTime() 11 | }); 12 | return jsonp(URL.carousel, data, OPTION); 13 | } 14 | 15 | /* 16 | //旧接口 17 | export function getNewAlbum() { 18 | const data = Object.assign({}, PARAM, { 19 | g_tk: 1278911659, 20 | hostUin: 0, 21 | platform: "yqq", 22 | needNewCode: 0, 23 | cmd: "firstpage", 24 | page: 0, 25 | pagesize: 50, 26 | sort: 1, 27 | language: -1, 28 | genre: 0, 29 | year: 1, 30 | pay: 0, 31 | type: -1, 32 | company: -1 33 | }); 34 | return jsonp(URL.newalbum, data, OPTION); 35 | }*/ 36 | 37 | export function getNewAlbum() { 38 | const data = Object.assign({}, PARAM, { 39 | g_tk: 1278911659, 40 | hostUin: 0, 41 | platform: "yqq", 42 | needNewCode: 0, 43 | data: `{"albumlib": 44 | {"method":"get_album_by_tags","param": 45 | {"area":1,"company":-1,"genre":-1,"type":-1,"year":-1,"sort":2,"get_tags":1,"sin":0,"num":50,"click_albumid":0}, 46 | "module":"music.web_album_library"}}` 47 | }); 48 | const option = { 49 | param: "callback", 50 | prefix: "callback" 51 | }; 52 | return jsonp(URL.newalbum, data, option); 53 | } 54 | 55 | export function getAlbumInfo(albumMid) { 56 | const data = Object.assign({}, PARAM, { 57 | albummid: albumMid, 58 | g_tk: 1278911659, 59 | hostUin: 0, 60 | platform: "yqq", 61 | needNewCode: 0 62 | }); 63 | return jsonp(URL.albumInfo, data, OPTION); 64 | } 65 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | Mango Music 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/assets/stylus/font.styl: -------------------------------------------------------------------------------- 1 | @font-face 2 | font-family: 'icomusic' 3 | src: url('fonts/icomusic.eot?you7t1') 4 | src: url('fonts/icomusic.eot?you7t1#iefix') format('embedded-opentype'), 5 | url('fonts/icomusic.ttf?you7t1') format('truetype'), 6 | url('fonts/icomusic.woff?you7t1') format('woff'), 7 | url('fonts/icomusic.svg?you7t1#icomusic') format('svg') 8 | font-weight: normal 9 | font-style: normal 10 | 11 | 12 | [class^="icon-"], [class*=" icon-"] 13 | /* use !important to prevent issues with browser extensions that change fonts */ 14 | font-family: 'icomusic' !important 15 | speak: none 16 | font-style: normal 17 | font-weight: normal 18 | font-variant: normal 19 | text-transform: none 20 | line-height: 1 21 | 22 | /* Better Font Rendering =========== */ 23 | -webkit-font-smoothing: antialiased 24 | -moz-osx-font-smoothing: grayscale 25 | 26 | 27 | .icon-back:before 28 | content: "\e900" 29 | 30 | .icon-delete:before 31 | content: "\e901" 32 | 33 | .icon-et-more:before 34 | content: "\e902" 35 | 36 | .icon-fe-music:before 37 | content: "\e903" 38 | 39 | .icon-list-play:before 40 | content: "\e904" 41 | 42 | .icon-music:before 43 | content: "\e905" 44 | 45 | .icon-next:before 46 | content: "\e906" 47 | 48 | .icon-pause:before 49 | content: "\e907" 50 | 51 | .icon-play:before 52 | content: "\e908" 53 | 54 | .icon-play-list:before 55 | content: "\e909" 56 | 57 | .icon-previous:before 58 | content: "\e90a" 59 | 60 | .icon-right:before 61 | content: "\e90b" 62 | 63 | .icon-search:before 64 | content: "\e90c" 65 | 66 | .icon-shuffle-play:before 67 | content: "\e90d" 68 | 69 | .icon-single-play:before 70 | content: "\e90e" 71 | 72 | .icon-user:before 73 | content: "\e90f" 74 | -------------------------------------------------------------------------------- /src/components/scroll/Scroll.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import PropTypes from "prop-types" 3 | import BScroll from "better-scroll" 4 | import "./scroll.styl" 5 | 6 | class Scroll extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | 10 | this.scrollViewRef = React.createRef(); 11 | } 12 | componentDidUpdate() { 13 | // 组件更新后,如果实例化了better-scroll并且需要刷新就调用refresh()函数 14 | if (this.bScroll && this.props.refresh === true) { 15 | this.bScroll.refresh(); 16 | } 17 | } 18 | componentDidMount() { 19 | if (!this.bScroll) { 20 | this.bScroll = new BScroll(this.scrollViewRef.current, { 21 | scrollX: this.props.direction === "horizontal", 22 | scrollY: this.props.direction === "vertical", 23 | // 实时派发scroll事件 24 | probeType: 3, 25 | click: this.props.click 26 | }); 27 | 28 | if (this.props.onScroll) { 29 | this.bScroll.on("scroll", (scroll) => { 30 | this.props.onScroll(scroll); 31 | }); 32 | } 33 | 34 | } 35 | } 36 | componentWillUnmount() { 37 | this.bScroll.off("scroll"); 38 | this.bScroll = null; 39 | } 40 | refresh() { 41 | if (this.bScroll) { 42 | this.bScroll.refresh(); 43 | } 44 | } 45 | render() { 46 | return ( 47 |
48 | {/* 获取子组件 */} 49 | {this.props.children} 50 |
51 | ); 52 | } 53 | } 54 | 55 | Scroll.defaultProps = { 56 | direction: "vertical", 57 | click: true, 58 | refresh: true, 59 | onScroll: null 60 | }; 61 | 62 | Scroll.propTypes = { 63 | direction: PropTypes.oneOf(['vertical', 'horizontal']), 64 | // 是否启用点击 65 | click: PropTypes.bool, 66 | // 是否刷新 67 | refresh: PropTypes.bool, 68 | onScroll: PropTypes.func 69 | }; 70 | 71 | export default Scroll 72 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import React, { lazy, Suspense } from "react" 2 | 3 | /// React 16.6 or higher 4 | // 使用Suspense做Code-Splitting 5 | const withSuspense = (Component) => { 6 | return (props) => ( 7 | 8 | 9 | 10 | ); 11 | } 12 | 13 | const Recommend = withSuspense(lazy(() => import("../views/recommend/Recommend"))); 14 | const Rankings = withSuspense(lazy(() => import("../views/ranking/Ranking"))); 15 | const SingerList = withSuspense(lazy(() => import("../views/singer/SingerList"))); 16 | const Search = withSuspense(lazy(() => import("../containers/Search"))); 17 | 18 | const Album = withSuspense(lazy(() => import("../containers/Album"))); 19 | const Ranking = withSuspense(lazy(() => import("../containers/Ranking"))); 20 | const Singer = withSuspense(lazy(() => import("../containers/Singer"))); 21 | 22 | const router = [ 23 | { 24 | path: "/recommend", 25 | component: Recommend, 26 | routes: [ 27 | { 28 | path: "/recommend/:id", 29 | component: Album 30 | } 31 | ] 32 | }, 33 | { 34 | path: "/ranking", 35 | component: Rankings, 36 | routes: [ 37 | { 38 | path: "/ranking/:id", 39 | component: Ranking 40 | } 41 | ] 42 | }, 43 | { 44 | path: "/singer", 45 | component: SingerList, 46 | routes: [ 47 | { 48 | path: "/singer/:id", 49 | component: Singer 50 | } 51 | ] 52 | }, 53 | { 54 | path: "/search", 55 | component: Search, 56 | routes: [ 57 | { 58 | path: "/search/album/:id", 59 | component: Album 60 | }, 61 | { 62 | path: "/search/singer/:id", 63 | component: Singer 64 | } 65 | ] 66 | }, 67 | { 68 | component: () => ( 69 |
70 | 请求的页面不存在 71 |
72 | ) 73 | } 74 | ]; 75 | 76 | export default router 77 | -------------------------------------------------------------------------------- /icons/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /icons/single-play.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /icons/previous.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/redux/reducers.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux" 2 | import * as ActionTypes from "./actionTypes" 3 | import localStorage from "../util/storage" 4 | 5 | /** 6 | * reducer就是一个纯函数,接收旧的state和action,返回新的state 7 | */ 8 | 9 | // 需要存储的初始状态数据 10 | const initialState = { 11 | skin: localStorage.getSkin(), // 皮肤 12 | showStatus: false, // 显示状态 13 | song: localStorage.getCurrentSong(), // 当前歌曲 14 | songs: localStorage.getSongs() // 歌曲列表 15 | }; 16 | 17 | // 拆分Reducer 18 | 19 | // 设置皮肤 20 | function skin(skin = initialState.skin, action) { 21 | switch (action.type) { 22 | case ActionTypes.SET_SKIN: 23 | return action.skin; 24 | default: 25 | return skin; 26 | } 27 | } 28 | 29 | // 显示或隐藏播放状态 30 | function showStatus(showStatus = initialState.showStatus, action) { 31 | switch (action.type) { 32 | case ActionTypes.SHOW_PLAYER: 33 | return action.showStatus; 34 | default: 35 | return showStatus; 36 | } 37 | } 38 | 39 | // 修改当前歌曲 40 | function song(song = initialState.song, action) { 41 | switch (action.type) { 42 | case ActionTypes.CHANGE_SONG: 43 | return action.song; 44 | default: 45 | return song; 46 | } 47 | } 48 | 49 | // 添加或移除歌曲 50 | function songs(songs = initialState.songs, action) { 51 | switch (action.type) { 52 | case ActionTypes.SET_SONGS: 53 | if (action.songs.length > 1) { 54 | return action.songs; 55 | } else { 56 | let newSongs = [...songs]; 57 | let addSong = action.songs[0]; 58 | let index = newSongs.findIndex(song => song.id === addSong.id); 59 | if (index === -1) { 60 | newSongs.push(addSong); 61 | } 62 | return newSongs; 63 | } 64 | case ActionTypes.REMOVE_SONG_FROM_LIST: 65 | let newSongs = songs.filter(song => song.id !== action.id); 66 | return newSongs; 67 | default: 68 | return songs; 69 | } 70 | } 71 | 72 | 73 | // 合并Reducer 74 | const reducer = combineReducers({ 75 | skin, 76 | showStatus, 77 | song, 78 | songs 79 | }); 80 | 81 | export default reducer 82 | -------------------------------------------------------------------------------- /src/views/play/miniplayer.styl: -------------------------------------------------------------------------------- 1 | .mini-player 2 | position: fixed 3 | left: 0 4 | bottom: 0 5 | z-index: 1000 6 | width: 100% 7 | height: 52px 8 | /*background-color: #212121 9 | color: #FFFFFF*/ 10 | &.mini-player-translate-enter 11 | transform: translate3d(0, 100%, 0) 12 | &.mini-player-translate-enter-active 13 | transition: transform .3s 14 | transform: translate3d(0, 0, 0) 15 | &.mini-player-translate-exit 16 | transform: translate3d(0, 0, 0) 17 | &.mini-player-translate-exit-active 18 | transition: transform .3s 19 | transform: translate3d(0, 100%, 0) 20 | .player-img 21 | position: absolute 22 | width: 46px 23 | height: 46px 24 | left: 10px 25 | top: -6px 26 | /*border: 2px solid rgba(221, 221, 221, 0.3)*/ 27 | border-radius: 50% 28 | &.rotate 29 | animation: rotate 15s linear infinite 30 | img 31 | width: 100% 32 | height: @width 33 | border-radius: 50% 34 | .player-center 35 | height: 52px 36 | padding: 6px 15px 0 66px 37 | box-sizing: border-box 38 | span 39 | display: block 40 | .song, .singer 41 | max-width: 220px 42 | overflow: hidden 43 | text-overflow: ellipsis 44 | white-space: nowrap 45 | .song 46 | padding-top: 5px 47 | font-size: 14px 48 | .singer 49 | margin-top: 2px 50 | font-size: 12px 51 | /*color: rgba(221,221,221,0.7)*/ 52 | .progress-wrapper 53 | .progress-bar 54 | height: 2px 55 | .player-right 56 | position: absolute 57 | top: 15px 58 | right: 15px 59 | padding: 1px 60 | /*color: #FFD700*/ 61 | font-size: 26px 62 | .ml-16 63 | margin-left: 16px 64 | .filter 65 | position: absolute 66 | top: 0 67 | left: 0 68 | width: 100% 69 | height: @width 70 | overflow: hidden 71 | z-index: -1 72 | &:after 73 | content: "" 74 | position: absolute 75 | top: 0 76 | left: 0 77 | right: 0 78 | bottom: 0 79 | z-index: -1 80 | margin: -30px 81 | filter: blur(30px) 82 | opacity: .6 83 | -------------------------------------------------------------------------------- /src/views/setting/skin.styl: -------------------------------------------------------------------------------- 1 | .music-skin 2 | position: fixed 3 | top: 0 4 | left: 0 5 | width: 100% 6 | height: @width 7 | z-index: 9999 8 | color: #FFFFFF 9 | background: url("../../assets/imgs/play_bg.jpg") no-repeat center 10 | background-size: cover 11 | display: none 12 | &:after 13 | content: "" 14 | position: absolute 15 | top: 0 16 | left: 0 17 | right: 0 18 | bottom: 0 19 | z-index: -1 20 | background-color: rgba(0, 0, 0, .3) 21 | &:global(.pop-enter) 22 | transform: translate3d(0, 100%, 0) 23 | &:global(.pop-enter-active) 24 | transition: transform .3s 25 | transform: translate3d(0, 0, 0) 26 | &:global(.pop-exit) 27 | transform: translate3d(0, 0, 0) 28 | &:global(.pop-exit-active) 29 | transition: transform .3s 30 | transform: translate3d(0, 100%, 0) 31 | .header 32 | height: 50px 33 | line-height: 50px 34 | text-align: center 35 | font-size: 18px 36 | position: relative 37 | .cancel 38 | position: absolute 39 | right: 15px 40 | font-size: 16px 41 | .skin-title 42 | height: 15px 43 | line-height: 15px 44 | padding-left: 15px 45 | font-size: 15px 46 | position: relative 47 | &:before 48 | content: "" 49 | position: absolute 50 | left: 0 51 | width: 3px 52 | height: 100% 53 | background-color: #FAEBD7 54 | .skin-container 55 | display: flex 56 | margin-top: 20px 57 | flex-wrap: wrap 58 | font-size: 14px 59 | .skin-wrapper 60 | flex: 0 0 32% 61 | width: 32% 62 | text-align: center 63 | padding-left: 4% 64 | margin-bottom: 10px 65 | box-sizing: border-box 66 | .skin-color 67 | padding-bottom: 110% 68 | margin-bottom: 5px 69 | box-shadow: 0 0 3px #FFFFFF 70 | position: relative 71 | :global(.icon-right) 72 | position: absolute 73 | right: 5px 74 | bottom: 5px 75 | font-size: 20px -------------------------------------------------------------------------------- /src/views/setting/Skin.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { CSSTransition } from "react-transition-group" 3 | import { skin, setSkinStyle } from "../../util/skin" 4 | 5 | import style from "./skin.styl?module" 6 | 7 | class Skin extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | 11 | this.skinRef = React.createRef(); 12 | 13 | this.skins = [ 14 | { key: "mangoYellow", name: "芒果黄", color: "#FFD700" }, 15 | { key: "coolBlack", name: "炫酷黑", color: "#212121" }, 16 | { key: "kuGouBlue", name: "酷狗蓝", color: "#2CA2F9" }, 17 | { key: "netBaseRed", name: "网易红", color: "#D43C33" }, 18 | { key: "qqGreen", name: "QQ绿", color: "#31C27C" } 19 | ] 20 | } 21 | setCurrentSkin = (key) => { 22 | // 设置皮肤 23 | setSkinStyle(skin[key]); 24 | this.props.setSkin(key); 25 | // 关闭当前页面 26 | this.props.close(); 27 | } 28 | render() { 29 | return ( 30 | { 32 | this.skinRef.current.style.display = "block"; 33 | }} 34 | onExited={() => { 35 | this.skinRef.current.style.display = "none"; 36 | }}> 37 |
38 |
39 | 皮肤中心 40 | { this.props.close(); }}>取消 41 |
42 |
推荐皮肤
43 |
44 | { 45 | this.skins.map(skin => ( 46 |
{ this.setCurrentSkin(skin.key); }} key={skin.key}> 47 |
48 | 49 |
50 |
{skin.name}
51 |
52 | )) 53 | } 54 |
55 |
56 |
57 | ); 58 | } 59 | } 60 | 61 | export default Skin 62 | -------------------------------------------------------------------------------- /src/views/singer/singer.styl: -------------------------------------------------------------------------------- 1 | :global(.music-singer) 2 | position: fixed 3 | top: 0 4 | left: 0 5 | right: 0 6 | bottom: 0 7 | z-index: 100 8 | /*background-color: #212121*/ 9 | div[class*="music-header"] 10 | z-index: 10 11 | .singer-img 12 | position: relative 13 | z-index: -1 14 | width: 100% 15 | padding-top: 70% 16 | background-repeat: no-repeat 17 | background-size: cover 18 | transform-origin: top center 19 | transition: transform 20 | &.fixed 21 | position: fixed 22 | top: 0 23 | height: 50px 24 | z-index: 1 25 | background-position: 0 0 26 | padding: 0 27 | display: none 28 | .play-wrapper 29 | position: absolute 30 | bottom: 20px 31 | width: 100% 32 | text-align: center 33 | color: #FFD700 34 | .play-button 35 | display: inline-block 36 | width: 120px 37 | padding: 8px 0 38 | border-radius: 50px 39 | border: 1px solid #FFD700 40 | i 41 | display: inline-block 42 | vertical-align: middle 43 | margin-top: -2px 44 | margin-right: 5px 45 | font-size: 16px 46 | font-weight: 600 47 | span 48 | font-size: 14px 49 | .filter 50 | position: absolute 51 | top: 0 52 | left: 0 53 | width: 100% 54 | height: 100% 55 | background-color: rgba(0, 0, 0, 0.3) 56 | .singer-container 57 | position: absolute 58 | bottom: 52px 59 | width: 100% 60 | .singer-scroll 61 | width: 100% 62 | height: @width 63 | :global(.scroll-view) 64 | overflow: visible 65 | .singer-wrapper 66 | padding: 25px 25px 0 25px 67 | /*background-color: #212121*/ 68 | .song-count 69 | font-size: 14px 70 | .song-list 71 | margin-top: 20px 72 | .song 73 | padding-bottom: 25px 74 | :global(.song-name) 75 | height: 16px 76 | /*color: #FFF*/ 77 | :global(.song-singer) 78 | font-size: 14px 79 | padding-top: 8px 80 | /*color: rgba(221, 221, 221, 0.7)*/ 81 | :global(.song-name), :global(.song-singer) 82 | overflow: hidden 83 | text-overflow: ellipsis 84 | white-space: nowrap 85 | -------------------------------------------------------------------------------- /src/components/note/MusicalNote.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { getTransitionEndName } from "../../util/event" 3 | 4 | import "./musicalnote.styl" 5 | 6 | /** 7 | * 使用PureComponent,不需要触发生组件update,以免ref回调函数多次调用 8 | */ 9 | class MusicalNote extends React.PureComponent { 10 | constructor(props) { 11 | super(props); 12 | 13 | this.musicIcos = []; 14 | } 15 | componentDidMount() { 16 | this.initMusicIco(); 17 | } 18 | /** 19 | * 初始化音符图标 20 | */ 21 | initMusicIco() { 22 | this.musicIcos.forEach((item) => { 23 | // 初始化状态 24 | item.run = false; 25 | let transitionEndName = getTransitionEndName(item); 26 | item.addEventListener(transitionEndName, function () { 27 | this.style.display = "none"; 28 | this.style.webkitTransform = "translate3d(0, 0, 0)"; 29 | this.style.transform = "translate3d(0, 0, 0)"; 30 | this.run = false; 31 | 32 | let icon = this.querySelector("div"); 33 | icon.style.webkitTransform = "translate3d(0, 0, 0)"; 34 | icon.style.transform = "translate3d(0, 0, 0)"; 35 | }, false); 36 | }); 37 | } 38 | /** 39 | * 开始音符下落动画 40 | */ 41 | startAnimation({ x, y }) { 42 | if (this.musicIcos.length > 0) { 43 | for (let i = 0; i < this.musicIcos.length; i++) { 44 | let item = this.musicIcos[i]; 45 | // 选择一个未在动画中的元素开始动画 46 | if (item.run === false) { 47 | item.style.left = x + "px"; 48 | item.style.top = y + "px"; 49 | item.style.display = "inline-block"; 50 | setTimeout(() => { 51 | item.run = true; 52 | item.style.webkitTransform = "translate3d(0, 1000px, 0)"; 53 | item.style.transform = "translate3d(0, 1000px, 0)"; 54 | 55 | let icon = item.querySelector("div"); 56 | icon.style.webkitTransform = "translate3d(-30px, 0, 0)"; 57 | icon.style.transform = "translate3d(-30px, 0, 0)"; 58 | }, 10); 59 | break; 60 | } 61 | } 62 | } 63 | } 64 | render() { 65 | return ( 66 |
67 | { 68 | [1, 2, 3].map((item) => ( 69 |
{ this.musicIcos.push(el); } } 71 | key={item}> 72 |
73 |
74 | )) 75 | } 76 |
77 | ); 78 | } 79 | } 80 | 81 | export default MusicalNote 82 | -------------------------------------------------------------------------------- /src/views/album/album.styl: -------------------------------------------------------------------------------- 1 | :global(.music-album) 2 | position: fixed 3 | top: 0 4 | left: 0 5 | right: 0 6 | bottom: 0 7 | z-index: 100 8 | /*background-color: #212121*/ 9 | div[class*="music-header"] 10 | z-index: 10 11 | .album-img 12 | position: relative 13 | z-index: -1 14 | width: 100% 15 | padding-top: 70% 16 | background-repeat: no-repeat 17 | background-size: cover 18 | transform-origin: top center 19 | transition: transform 20 | &.fixed 21 | position: fixed 22 | top: 0 23 | height: 50px 24 | z-index: 1 25 | background-position: 0 0 26 | padding: 0 27 | display: none 28 | .play-wrapper 29 | position: absolute 30 | bottom: 20px 31 | width: 100% 32 | text-align: center 33 | color: #FFD700 34 | .play-button 35 | display: inline-block 36 | width: 120px 37 | padding: 8px 0 38 | border-radius: 50px 39 | border: 1px solid #FFD700 40 | i 41 | display: inline-block 42 | vertical-align: middle 43 | margin-top: -2px 44 | margin-right: 5px 45 | font-size: 16px 46 | font-weight: 600 47 | span 48 | font-size: 14px 49 | .filter 50 | position: absolute 51 | top: 0 52 | left: 0 53 | width: 100% 54 | height: 100% 55 | background-color: rgba(0, 0, 0, 0.3) 56 | .album-container 57 | position: absolute 58 | bottom: 52px 59 | width: 100% 60 | .album-scroll 61 | width: 100% 62 | height: @width 63 | :global(.scroll-view) 64 | overflow: visible 65 | .album-wrapper 66 | padding: 25px 25px 0 25px 67 | /*background-color: #212121*/ 68 | .song-count 69 | font-size: 14px 70 | .song-list 71 | margin-top: 20px 72 | .song 73 | padding-bottom: 25px 74 | :global(.song-name) 75 | height: 16px 76 | /*color: #FFF*/ 77 | :global(.song-singer) 78 | font-size: 14px 79 | padding-top: 8px 80 | /*color: rgba(221, 221, 221, 0.7)*/ 81 | :global(.song-name), :global(.song-singer) 82 | overflow: hidden 83 | text-overflow: ellipsis 84 | white-space: nowrap 85 | .album-info 86 | padding-bottom: 25px 87 | .album-title 88 | font-size: 18px 89 | font-weight: 100 90 | text-align: center 91 | /*color: #FFFFFF*/ 92 | .album-desc 93 | font-size: 14px 94 | margin-top: 15px 95 | line-height: 22px 96 | -------------------------------------------------------------------------------- /src/views/App.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { BrowserRouter as Router, Switch, Redirect, NavLink } from "react-router-dom" 3 | import { renderRoutes } from "react-router-config" 4 | 5 | import router from "../router" 6 | import MusicPlayer from "./play/MusicPlayer" 7 | import MusicMenu from "./setting/Menu" 8 | 9 | import logo from "../assets/imgs/logo.png" 10 | import "../assets/stylus/reset.styl" 11 | import "../assets/stylus/font.styl" 12 | import style from "./app.styl?module" 13 | 14 | class App extends React.Component { 15 | constructor(props) { 16 | super(props); 17 | 18 | this.state = { 19 | menuShow: false 20 | }; 21 | } 22 | render() { 23 | return ( 24 | 25 |
26 |
27 | { this.setState({ menuShow: true }); }}> 28 | logo 29 |

Mango Music

30 |
31 |
32 |
33 | 34 | 推荐 35 | 36 |
37 |
38 | 39 | 排行榜 40 | 41 |
42 |
43 | 44 | 歌手 45 | 46 |
47 |
48 | 49 | 搜索 50 | 51 |
52 |
53 |
54 | {/* 55 | Switch组件用来选择最近的一个路由,否则没有指定path的路由也会显示 56 | Redirect重定向到列表页 57 | */} 58 | 59 | 60 | {/* 渲染 Route */} 61 | { renderRoutes(router) } 62 | 63 |
64 | 65 | { this.setState({ menuShow: false }); }} /> 67 |
68 |
69 | ); 70 | } 71 | } 72 | 73 | export default App 74 | -------------------------------------------------------------------------------- /src/views/play/MiniPlayer.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { CSSTransition } from "react-transition-group" 3 | import Progress from "./Progress" 4 | 5 | import "./miniplayer.styl" 6 | 7 | class MiniPlayer extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | 11 | this.miniPlayerRef = React.createRef(); 12 | } 13 | handlePlayOrPause = (e) => { 14 | e.stopPropagation(); 15 | if (this.props.song.url) { 16 | // 调用父组件的播放或暂停方法 17 | this.props.playOrPause(); 18 | } 19 | } 20 | handleNext = (e) => { 21 | e.stopPropagation(); 22 | 23 | if (this.props.song.url) { 24 | // 调用父组件播放下一首方法 25 | this.props.next(); 26 | } 27 | } 28 | handleShow = () => { 29 | if (this.props.song.url) { 30 | this.props.showMiniPlayer(); 31 | } 32 | } 33 | render() { 34 | let song = this.props.song; 35 | 36 | if (!song.img) { 37 | song.img = require("../../assets/imgs/music.png"); 38 | } 39 | 40 | let imgStyle = {}; 41 | if (song.playStatus === true) { 42 | imgStyle.WebkitAnimationPlayState = "running"; 43 | imgStyle.animationPlayState = "running"; 44 | } else { 45 | imgStyle.WebkitAnimationPlayState = "paused"; 46 | imgStyle.animationPlayState = "paused"; 47 | } 48 | 49 | let playButtonClass = song.playStatus === true ? "icon-pause" : "icon-play"; 50 | return ( 51 | { 53 | this.miniPlayerRef.current.style.display = "block"; 54 | }} 55 | onExited={() => { 56 | this.miniPlayerRef.current.style.display = "none"; 57 | }}> 58 |
59 |
60 | {song.name} 61 |
62 |
63 |
64 | 65 |
66 | 67 | {song.name} 68 | 69 | 70 | {song.singer} 71 | 72 |
73 |
74 | 75 | 76 |
77 |
78 |
79 |
80 | ); 81 | } 82 | } 83 | 84 | export default MiniPlayer 85 | -------------------------------------------------------------------------------- /icons/delete.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mango-music", 3 | "version": "1.1.0", 4 | "description": "A music webapp built with react", 5 | "author": "mcx", 6 | "private": true, 7 | "scripts": { 8 | "start": "npm run dev", 9 | "dev": "node scripts/start.js", 10 | "build": "node scripts/build.js" 11 | }, 12 | "dependencies": { 13 | "better-scroll": "^1.6.0", 14 | "jsonp": "^0.2.1", 15 | "prop-types": "^15.6.0", 16 | "react": "^16.7.0", 17 | "react-dom": "^16.7.0", 18 | "react-lazyload": "^2.3.0", 19 | "react-redux": "^5.0.6", 20 | "react-router-config": "^1.0.0-beta.4", 21 | "react-router-dom": "^4.3.1", 22 | "react-transition-group": "^2.2.1", 23 | "redux": "^3.7.2", 24 | "swiper": "^3.4.2" 25 | }, 26 | "devDependencies": { 27 | "@babel/core": "7.1.6", 28 | "@svgr/webpack": "2.4.1", 29 | "babel-core": "7.0.0-bridge.0", 30 | "babel-eslint": "9.0.0", 31 | "babel-loader": "8.0.4", 32 | "babel-plugin-named-asset-import": "^0.3.0", 33 | "babel-preset-react-app": "^7.0.0", 34 | "bfj": "6.1.1", 35 | "case-sensitive-paths-webpack-plugin": "2.1.2", 36 | "chalk": "2.4.1", 37 | "css-loader": "1.0.0", 38 | "dotenv": "6.0.0", 39 | "dotenv-expand": "4.2.0", 40 | "eslint": "5.6.0", 41 | "eslint-config-react-app": "^3.0.6", 42 | "eslint-loader": "2.1.1", 43 | "eslint-plugin-flowtype": "2.50.1", 44 | "eslint-plugin-import": "2.14.0", 45 | "eslint-plugin-jsx-a11y": "6.1.2", 46 | "eslint-plugin-react": "7.11.1", 47 | "file-loader": "2.0.0", 48 | "fork-ts-checker-webpack-plugin-alt": "0.4.14", 49 | "fs-extra": "7.0.0", 50 | "html-webpack-plugin": "4.0.0-alpha.2", 51 | "identity-obj-proxy": "3.0.0", 52 | "mini-css-extract-plugin": "0.4.3", 53 | "optimize-css-assets-webpack-plugin": "5.0.1", 54 | "pnp-webpack-plugin": "1.1.0", 55 | "postcss-flexbugs-fixes": "4.1.0", 56 | "postcss-loader": "3.0.0", 57 | "postcss-preset-env": "6.3.1", 58 | "postcss-safe-parser": "4.0.1", 59 | "poststylus": "^1.0.0", 60 | "react-app-polyfill": "^0.2.0", 61 | "react-dev-utils": "^7.0.0", 62 | "redux-logger": "^3.0.6", 63 | "resolve": "1.8.1", 64 | "sass-loader": "7.1.0", 65 | "style-loader": "0.23.0", 66 | "stylus": "^0.54.5", 67 | "stylus-loader": "^3.0.1", 68 | "terser-webpack-plugin": "1.1.0", 69 | "url-loader": "1.1.1", 70 | "webpack": "4.19.1", 71 | "webpack-dev-server": "^3.1.14", 72 | "webpack-manifest-plugin": "2.0.4", 73 | "workbox-webpack-plugin": "3.6.3" 74 | }, 75 | "browserslist": [ 76 | ">0.2%", 77 | "not dead", 78 | "not ie <= 11", 79 | "not op_mini all" 80 | ], 81 | "babel": { 82 | "presets": [ 83 | "react-app" 84 | ] 85 | }, 86 | "eslintConfig": { 87 | "extends": "react-app" 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | mango music 3 |

4 |

5 | react 6 | react-router 7 | redux 8 | react-redux 9 | react-transition-group 10 | react-lazyload 11 |

12 | 13 | ## Mango Music 14 | 15 | > This repository is no longer maintained. Some interfaces may be invalid. 16 | 17 | This is a music webapp. Build with [Create React App](https://github.com/facebookincubator/create-react-app). 18 | 19 | Online preview address: https://dxx.github.io/mango-music. 20 | 21 | ## Technology Stack 22 | 23 | React + React-Router + Redux + ES6 + Webpack. 24 | 25 | 26 | ## Available Scripts 27 | 28 | ### `npm install` 29 | 30 | First, run the `npm install` to install dependence. 31 | 32 | ### `npm start` 33 | 34 | Runs the app in the development mode.
35 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 36 | 37 | ### `npm run build` 38 | 39 | Builds the app for production to the `build` folder.
40 | It correctly bundles React in production mode and optimizes the build for the best performance. 41 | 42 | ## Screenshot 43 | 44 |

45 | recommend 46 | ranking 47 | 48 | singer 49 | search 50 | 51 | album info 52 | ranking info 53 | 54 | search result 55 | singer info 56 | 57 | play 58 | play list 59 |

-------------------------------------------------------------------------------- /config/paths.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const url = require('url'); 6 | 7 | // Make sure any symlinks in the project folder are resolved: 8 | // https://github.com/facebook/create-react-app/issues/637 9 | const appDirectory = fs.realpathSync(process.cwd()); 10 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath); 11 | 12 | const envPublicUrl = process.env.PUBLIC_URL; 13 | 14 | function ensureSlash(inputPath, needsSlash) { 15 | const hasSlash = inputPath.endsWith('/'); 16 | if (hasSlash && !needsSlash) { 17 | return inputPath.substr(0, inputPath.length - 1); 18 | } else if (!hasSlash && needsSlash) { 19 | return `${inputPath}/`; 20 | } else { 21 | return inputPath; 22 | } 23 | } 24 | 25 | const getPublicUrl = appPackageJson => 26 | envPublicUrl || require(appPackageJson).homepage; 27 | 28 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer 29 | // "public path" at which the app is served. 30 | // Webpack needs to know it to put the right