├── .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 |
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 |
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 |
26 | You need to enable JavaScript to run this app.
27 |
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 |
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 |
61 |
62 |
63 |
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 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
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 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
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