├── .gitignore ├── README.md ├── config ├── env.js ├── jest │ ├── cssTransform.js │ └── fileTransform.js ├── paths.js ├── polyfills.js ├── webpack.config.dev.js ├── webpack.config.dll.js ├── webpack.config.prod.js └── webpackDevServer.config.js ├── ecosystem.json ├── npm-shrinkwrap.json ├── package.json ├── public ├── favicon.ico ├── images │ ├── notFound.jpg │ └── personalized.png ├── index.html └── manifest.json ├── scripts ├── build.js ├── start.js └── test.js ├── server ├── .gitignore ├── api │ └── index.js ├── config │ ├── development.js │ ├── index.js │ └── production.js ├── graphql │ ├── album │ │ ├── model.js │ │ └── query.js │ ├── artist │ │ ├── model.js │ │ └── query.js │ ├── banner │ │ ├── model.js │ │ └── query.js │ ├── lyrics │ │ ├── model.js │ │ └── query.js │ ├── music │ │ ├── model.js │ │ └── query.js │ ├── mv │ │ └── model.js │ ├── personalized │ │ ├── model.js │ │ └── query.js │ ├── playList │ │ ├── model.js │ │ └── query.js │ ├── radio │ │ ├── model.js │ │ └── query.js │ ├── schema.js │ ├── search │ │ ├── model.js │ │ └── query.js │ ├── song │ │ └── model.js │ └── user │ │ └── model.js ├── index.js ├── npm-shrinkwrap.json ├── package.json ├── server.js └── utils │ └── axios.js └── src ├── App.jsx ├── App.test.js ├── actions ├── actionTypes.js ├── home.js ├── index.js ├── music.js ├── radio.js └── search.js ├── api └── index.js ├── assets ├── font │ ├── iconfont.css │ ├── iconfont.eot │ ├── iconfont.svg │ ├── iconfont.ttf │ └── iconfont.woff └── media │ └── fixAutoPlay.mp3 ├── components ├── albumList │ ├── AlbumList.jsx │ └── albumList.styl ├── alert │ ├── Alert.jsx │ └── alert.styl ├── artistList │ ├── ArtistList.jsx │ └── artistList.styl ├── commentList │ ├── CommentList.jsx │ └── commentList.styl ├── commonHeader │ ├── CommonHeader.jsx │ └── commonHeader.styl ├── fullPlay │ ├── FullPlay.jsx │ └── fullPlay.styl ├── historyList │ ├── HistoryList.jsx │ └── historyList.styl ├── loading │ ├── Loading.jsx │ └── loading.styl ├── miniPlay │ ├── MiniPlay.jsx │ └── miniPlay.styl ├── musicList │ ├── MusicList.jsx │ └── musicList.styl ├── mvList │ ├── MvList.jsx │ └── mvList.styl ├── playList │ ├── PlayList.jsx │ └── playList.styl ├── popup │ ├── Popup.jsx │ └── popup.styl ├── programList │ ├── ProgramList.jsx │ └── programList.styl ├── progressBar │ ├── ProgressBar.jsx │ └── progressBar.styl ├── radioList │ ├── RadioList.jsx │ └── radioList.styl ├── radiothumbnailList │ ├── RadiothumbnailList.jsx │ └── radiothumbnailList.styl ├── scroll │ ├── Scroll.jsx │ └── scroll.styl ├── slidebar │ ├── Slidebar.jsx │ └── slidebar.styl ├── songList │ ├── SongList.jsx │ └── songList.styl ├── swipe │ ├── Swipe.jsx │ └── swipe.styl ├── tabMenu │ ├── TabMenu.jsx │ └── tabMenu.styl └── thumbnailList │ ├── ThumbnailList.jsx │ └── thumbnailList.styl ├── containers ├── Found.jsx ├── Home.jsx ├── Mime.jsx ├── Play.jsx ├── PlaylistDetail.jsx ├── Radio.jsx ├── RadioDetail.jsx ├── Search.jsx ├── found.styl ├── home.styl ├── mime.styl ├── playlistDetail.styl ├── radioDetail.styl └── search.styl ├── index.js ├── logo.svg ├── reducers ├── homeData.js ├── index.js ├── music.js ├── radio.js ├── search.js └── slidebar.js ├── registerServiceWorker.js ├── router └── index.js ├── styles ├── icon.styl ├── index.styl └── reset.styl └── utils ├── axios.js ├── index.js ├── music.js ├── reactUtil.js └── watcher.js /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | /public/base.dll.js 12 | /public/base.manifest.json 13 | /config/.uglify-cache 14 | 15 | # misc 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-music 2 | 本来没打算写网易云音乐的,好像都已经被大家写烂了,不过没办法,暂时想不到其他的可写,加上网易云音乐又有API,还可以基于restful API做一层graphql的处理再提供给前端调用,然后才决定用react写了这个仿app版网易云音乐 3 | 4 | ### 技术栈 5 | - react 6 | - react-router 7 | - redux 8 | - react-redux 9 | - react-motion 10 | - better-scroll 11 | - ES6/7 12 | - stylus 13 | - koa 14 | - graphql 15 | 16 | ### 实现的功能 17 | - 发现页 18 | - 我的 19 | - 电台页 20 | - 侧边栏 21 | - 歌单内页 22 | - 电台内页 23 | - 搜索页及结果页 24 | - 上一首 25 | - 下一首 26 | - 播放模式切换 27 | - 歌曲删除 28 | - 歌词 29 | - 左右滑切歌 30 | 31 | ### 运行 32 | ``` 33 | git clone git@github.com:Binaryify/NeteaseCloudMusicApi.git 34 | ``` 35 | 这是网易云API,因为最新的banner数据已经改了,可以`git reset --hard d155a1fc0177e525cb650d239b8a98a8549a85e1`回退到这次提交 36 | ``` 37 | cross-env PORT=8080 npm start 38 | ``` 39 | 首先启动api服务器,需要用`8080`端口启动 40 | ``` 41 | git clone git@github.com:Kim09AI/react-music.git 42 | 43 | # dev模式 44 | # 先启动graphql服务器 45 | $ cd server && npm run dev 46 | # 再回到根目录 47 | $ npm start 48 | 49 | # production模式 50 | # 首次build前先执行(因为使用了dll) 51 | $ npm run build:dll 52 | $ npm run build 53 | # 本地以production模式启动服务器 54 | cd server && npm start 55 | ``` 56 | 57 | ### 预览 58 | [线上地址][1] 59 | [github地址][2] 60 | 移动端预览 61 | ![此处输入图片的描述][3] 62 | 63 | ![此处输入图片的描述][4] 64 | 65 | ![此处输入图片的描述][5] 66 | 67 | ![此处输入图片的描述][6] 68 | 69 | 70 | 71 | # react使用的一些总结 72 | 主要还是在react-redux的使用了,数据应该保存在state还是全局的store,主要还是看数据需不需要共享,或者是需不需要缓存,不然存在store反而会使问题变得更麻烦 73 | 74 | # 最后 75 | 感谢`Binaryify`提供的[NeteaseCloudMusicApi][7] 76 | 欢迎`star`或`fork`,有问题或有发现bug页欢迎提`issues`,写的不好的地方也请大佬指点 77 | 78 | 79 | [1]: https://react-music.foreversnsd.cn 80 | [2]: https://github.com/Kim09AI/react-music 81 | [3]: http://47.106.94.19:3001/images/%E8%81%94%E5%9B%BE%E4%BA%8C%E7%BB%B4%E7%A0%81.png 82 | [4]: http://47.106.94.19:3001/images/01.gif 83 | [5]: http://47.106.94.19:3001/images/02.gif 84 | [6]: http://47.106.94.19:3001/images/03.gif 85 | [7]: https://github.com/Binaryify/NeteaseCloudMusicApi -------------------------------------------------------------------------------- /config/env.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const paths = require('./paths'); 6 | 7 | // Make sure that including paths.js after env.js will read .env variables. 8 | delete require.cache[require.resolve('./paths')]; 9 | 10 | const NODE_ENV = process.env.NODE_ENV; 11 | if (!NODE_ENV) { 12 | throw new Error( 13 | 'The NODE_ENV environment variable is required but was not specified.' 14 | ); 15 | } 16 | 17 | // https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use 18 | var dotenvFiles = [ 19 | `${paths.dotenv}.${NODE_ENV}.local`, 20 | `${paths.dotenv}.${NODE_ENV}`, 21 | // Don't include `.env.local` for `test` environment 22 | // since normally you expect tests to produce the same 23 | // results for everyone 24 | NODE_ENV !== 'test' && `${paths.dotenv}.local`, 25 | paths.dotenv, 26 | ].filter(Boolean); 27 | 28 | // Load environment variables from .env* files. Suppress warnings using silent 29 | // if this file is missing. dotenv will never modify any environment variables 30 | // that have already been set. Variable expansion is supported in .env files. 31 | // https://github.com/motdotla/dotenv 32 | // https://github.com/motdotla/dotenv-expand 33 | dotenvFiles.forEach(dotenvFile => { 34 | if (fs.existsSync(dotenvFile)) { 35 | require('dotenv-expand')( 36 | require('dotenv').config({ 37 | path: dotenvFile, 38 | }) 39 | ); 40 | } 41 | }); 42 | 43 | // We support resolving modules according to `NODE_PATH`. 44 | // This lets you use absolute paths in imports inside large monorepos: 45 | // https://github.com/facebookincubator/create-react-app/issues/253. 46 | // It works similar to `NODE_PATH` in Node itself: 47 | // https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders 48 | // Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored. 49 | // Otherwise, we risk importing Node.js core modules into an app instead of Webpack shims. 50 | // https://github.com/facebookincubator/create-react-app/issues/1023#issuecomment-265344421 51 | // We also resolve them to make sure all tools using them work consistently. 52 | const appDirectory = fs.realpathSync(process.cwd()); 53 | process.env.NODE_PATH = (process.env.NODE_PATH || '') 54 | .split(path.delimiter) 55 | .filter(folder => folder && !path.isAbsolute(folder)) 56 | .map(folder => path.resolve(appDirectory, folder)) 57 | .join(path.delimiter); 58 | 59 | // Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be 60 | // injected into the application via DefinePlugin in Webpack configuration. 61 | const REACT_APP = /^REACT_APP_/i; 62 | 63 | function getClientEnvironment(publicUrl) { 64 | const raw = Object.keys(process.env) 65 | .filter(key => REACT_APP.test(key)) 66 | .reduce( 67 | (env, key) => { 68 | env[key] = process.env[key]; 69 | return env; 70 | }, 71 | { 72 | // Useful for determining whether we’re running in production mode. 73 | // Most importantly, it switches React into the correct mode. 74 | NODE_ENV: process.env.NODE_ENV || 'development', 75 | // Useful for resolving the correct path to static assets in `public`. 76 | // For example, . 77 | // This should only be used as an escape hatch. Normally you would put 78 | // images into the `src` and `import` them in code to get their paths. 79 | PUBLIC_URL: publicUrl, 80 | } 81 | ); 82 | // Stringify all values so we can feed into Webpack DefinePlugin 83 | const stringified = { 84 | 'process.env': Object.keys(raw).reduce((env, key) => { 85 | env[key] = JSON.stringify(raw[key]); 86 | return env; 87 | }, {}), 88 | }; 89 | 90 | return { raw, stringified }; 91 | } 92 | 93 | module.exports = getClientEnvironment; 94 | -------------------------------------------------------------------------------- /config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This is a custom Jest transformer turning style imports into empty objects. 4 | // http://facebook.github.io/jest/docs/en/webpack.html 5 | 6 | module.exports = { 7 | process() { 8 | return 'module.exports = {};'; 9 | }, 10 | getCacheKey() { 11 | // The output is always the same. 12 | return 'cssTransform'; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | // This is a custom Jest transformer turning file imports into filenames. 6 | // http://facebook.github.io/jest/docs/en/webpack.html 7 | 8 | module.exports = { 9 | process(src, filename) { 10 | return `module.exports = ${JSON.stringify(path.basename(filename))};`; 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /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/facebookincubator/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(path, needsSlash) { 15 | const hasSlash = path.endsWith('/'); 16 | if (hasSlash && !needsSlash) { 17 | return path.substr(path, path.length - 1); 18 | } else if (!hasSlash && needsSlash) { 19 | return `${path}/`; 20 | } else { 21 | return path; 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 30 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "React 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 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'production'; 5 | process.env.NODE_ENV = 'production'; 6 | 7 | // Makes the script crash on unhandled rejections instead of silently 8 | // ignoring them. In the future, promise rejections that are not handled will 9 | // terminate the Node.js process with a non-zero exit code. 10 | process.on('unhandledRejection', err => { 11 | throw err; 12 | }); 13 | 14 | // Ensure environment variables are read. 15 | require('../config/env'); 16 | 17 | const path = require('path'); 18 | const chalk = require('chalk'); 19 | const fs = require('fs-extra'); 20 | const webpack = require('webpack'); 21 | const config = require('../config/webpack.config.prod'); 22 | const paths = require('../config/paths'); 23 | const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); 24 | const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages'); 25 | const printHostingInstructions = require('react-dev-utils/printHostingInstructions'); 26 | const FileSizeReporter = require('react-dev-utils/FileSizeReporter'); 27 | const printBuildError = require('react-dev-utils/printBuildError'); 28 | 29 | const measureFileSizesBeforeBuild = 30 | FileSizeReporter.measureFileSizesBeforeBuild; 31 | const printFileSizesAfterBuild = FileSizeReporter.printFileSizesAfterBuild; 32 | const useYarn = fs.existsSync(paths.yarnLockFile); 33 | 34 | // These sizes are pretty large. We'll warn for bundles exceeding them. 35 | const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024; 36 | const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024; 37 | 38 | const start = Date.now(); 39 | 40 | // Warn and crash if required files are missing 41 | if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { 42 | process.exit(1); 43 | } 44 | 45 | // First, read the current file sizes in build directory. 46 | // This lets us display how much they changed later. 47 | measureFileSizesBeforeBuild(paths.appBuild) 48 | .then(previousFileSizes => { 49 | // Remove all content but keep the directory so that 50 | // if you're in it, you don't end up in Trash 51 | fs.emptyDirSync(paths.appBuild); 52 | // Merge with the public folder 53 | copyPublicFolder(); 54 | // Start the webpack build 55 | return build(previousFileSizes); 56 | }) 57 | .then( 58 | ({ stats, previousFileSizes, warnings }) => { 59 | if (warnings.length) { 60 | console.log(chalk.yellow('Compiled with warnings.\n')); 61 | console.log(warnings.join('\n\n')); 62 | console.log( 63 | '\nSearch for the ' + 64 | chalk.underline(chalk.yellow('keywords')) + 65 | ' to learn more about each warning.' 66 | ); 67 | console.log( 68 | 'To ignore, add ' + 69 | chalk.cyan('// eslint-disable-next-line') + 70 | ' to the line before.\n' 71 | ); 72 | } else { 73 | console.log(chalk.green('Compiled successfully.\n')); 74 | } 75 | 76 | console.log('File sizes after gzip:\n'); 77 | printFileSizesAfterBuild( 78 | stats, 79 | previousFileSizes, 80 | paths.appBuild, 81 | WARN_AFTER_BUNDLE_GZIP_SIZE, 82 | WARN_AFTER_CHUNK_GZIP_SIZE 83 | ); 84 | console.log(); 85 | 86 | const appPackage = require(paths.appPackageJson); 87 | const publicUrl = paths.publicUrl; 88 | const publicPath = config.output.publicPath; 89 | const buildFolder = path.relative(process.cwd(), paths.appBuild); 90 | printHostingInstructions( 91 | appPackage, 92 | publicUrl, 93 | publicPath, 94 | buildFolder, 95 | useYarn 96 | ); 97 | 98 | console.log('use', ((Date.now() - start) / 1000).toFixed(2), 's'); 99 | }, 100 | err => { 101 | console.log(chalk.red('Failed to compile.\n')); 102 | printBuildError(err); 103 | process.exit(1); 104 | } 105 | ); 106 | 107 | // Create the production build and print the deployment instructions. 108 | function build(previousFileSizes) { 109 | console.log('Creating an optimized production build...'); 110 | 111 | let compiler = webpack(config); 112 | return new Promise((resolve, reject) => { 113 | compiler.run((err, stats) => { 114 | if (err) { 115 | return reject(err); 116 | } 117 | const messages = formatWebpackMessages(stats.toJson({}, true)); 118 | if (messages.errors.length) { 119 | // Only keep the first error. Others are often indicative 120 | // of the same problem, but confuse the reader with noise. 121 | if (messages.errors.length > 1) { 122 | messages.errors.length = 1; 123 | } 124 | return reject(new Error(messages.errors.join('\n\n'))); 125 | } 126 | if ( 127 | process.env.CI && 128 | (typeof process.env.CI !== 'string' || 129 | process.env.CI.toLowerCase() !== 'false') && 130 | messages.warnings.length 131 | ) { 132 | console.log( 133 | chalk.yellow( 134 | '\nTreating warnings as errors because process.env.CI = true.\n' + 135 | 'Most CI servers set it automatically.\n' 136 | ) 137 | ); 138 | return reject(new Error(messages.warnings.join('\n\n'))); 139 | } 140 | return resolve({ 141 | stats, 142 | previousFileSizes, 143 | warnings: messages.warnings, 144 | }); 145 | }); 146 | }); 147 | } 148 | 149 | function copyPublicFolder() { 150 | fs.copySync(paths.appPublic, paths.appBuild, { 151 | dereference: true, 152 | filter: file => file !== paths.appHtml, 153 | }); 154 | } 155 | -------------------------------------------------------------------------------- /scripts/start.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'development'; 5 | process.env.NODE_ENV = 'development'; 6 | 7 | // Makes the script crash on unhandled rejections instead of silently 8 | // ignoring them. In the future, promise rejections that are not handled will 9 | // terminate the Node.js process with a non-zero exit code. 10 | process.on('unhandledRejection', err => { 11 | throw err; 12 | }); 13 | 14 | // Ensure environment variables are read. 15 | require('../config/env'); 16 | 17 | const fs = require('fs'); 18 | const chalk = require('chalk'); 19 | const webpack = require('webpack'); 20 | const WebpackDevServer = require('webpack-dev-server'); 21 | const clearConsole = require('react-dev-utils/clearConsole'); 22 | const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); 23 | const { 24 | choosePort, 25 | createCompiler, 26 | prepareProxy, 27 | prepareUrls, 28 | } = require('react-dev-utils/WebpackDevServerUtils'); 29 | const openBrowser = require('react-dev-utils/openBrowser'); 30 | const paths = require('../config/paths'); 31 | const config = require('../config/webpack.config.dev'); 32 | const createDevServerConfig = require('../config/webpackDevServer.config'); 33 | 34 | const useYarn = fs.existsSync(paths.yarnLockFile); 35 | const isInteractive = process.stdout.isTTY; 36 | 37 | // Warn and crash if required files are missing 38 | if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { 39 | process.exit(1); 40 | } 41 | 42 | // Tools like Cloud9 rely on this. 43 | const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000; 44 | const HOST = process.env.HOST || '0.0.0.0'; 45 | 46 | if (process.env.HOST) { 47 | console.log( 48 | chalk.cyan( 49 | `Attempting to bind to HOST environment variable: ${chalk.yellow( 50 | chalk.bold(process.env.HOST) 51 | )}` 52 | ) 53 | ); 54 | console.log( 55 | `If this was unintentional, check that you haven't mistakenly set it in your shell.` 56 | ); 57 | console.log(`Learn more here: ${chalk.yellow('http://bit.ly/2mwWSwH')}`); 58 | console.log(); 59 | } 60 | 61 | // We attempt to use the default port but if it is busy, we offer the user to 62 | // run on a different port. `choosePort()` Promise resolves to the next free port. 63 | choosePort(HOST, DEFAULT_PORT) 64 | .then(port => { 65 | if (port == null) { 66 | // We have not found a port. 67 | return; 68 | } 69 | const protocol = process.env.HTTPS === 'true' ? 'https' : 'http'; 70 | const appName = require(paths.appPackageJson).name; 71 | const urls = prepareUrls(protocol, HOST, port); 72 | // Create a webpack compiler that is configured with custom messages. 73 | const compiler = createCompiler(webpack, config, appName, urls, useYarn); 74 | // Load proxy config 75 | const proxySetting = require(paths.appPackageJson).proxy; 76 | const proxyConfig = prepareProxy(proxySetting, paths.appPublic); 77 | // Serve webpack assets generated by the compiler over a web sever. 78 | const serverConfig = createDevServerConfig( 79 | proxyConfig, 80 | urls.lanUrlForConfig 81 | ); 82 | const devServer = new WebpackDevServer(compiler, serverConfig); 83 | // Launch WebpackDevServer. 84 | devServer.listen(port, HOST, err => { 85 | if (err) { 86 | return console.log(err); 87 | } 88 | if (isInteractive) { 89 | clearConsole(); 90 | } 91 | console.log(chalk.cyan('Starting the development server...\n')); 92 | openBrowser(urls.localUrlForBrowser); 93 | }); 94 | 95 | ['SIGINT', 'SIGTERM'].forEach(function(sig) { 96 | process.on(sig, function() { 97 | devServer.close(); 98 | process.exit(); 99 | }); 100 | }); 101 | }) 102 | .catch(err => { 103 | if (err && err.message) { 104 | console.log(err.message); 105 | } 106 | process.exit(1); 107 | }); 108 | -------------------------------------------------------------------------------- /scripts/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'test'; 5 | process.env.NODE_ENV = 'test'; 6 | process.env.PUBLIC_URL = ''; 7 | 8 | // Makes the script crash on unhandled rejections instead of silently 9 | // ignoring them. In the future, promise rejections that are not handled will 10 | // terminate the Node.js process with a non-zero exit code. 11 | process.on('unhandledRejection', err => { 12 | throw err; 13 | }); 14 | 15 | // Ensure environment variables are read. 16 | require('../config/env'); 17 | 18 | const jest = require('jest'); 19 | let argv = process.argv.slice(2); 20 | 21 | // Watch unless on CI or in coverage mode 22 | if (!process.env.CI && argv.indexOf('--coverage') < 0) { 23 | argv.push('--watch'); 24 | } 25 | 26 | 27 | jest.run(argv); 28 | -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /server/api/index.js: -------------------------------------------------------------------------------- 1 | import axios from '../utils/axios' 2 | 3 | class Service { 4 | getBanner() { 5 | return axios.get('/banner') 6 | } 7 | 8 | getPersonalized() { 9 | return axios.get('/personalized') 10 | } 11 | 12 | search({ keywords, limit = 30, offset = 0, type = 1 }) { 13 | return axios.get('/search', { 14 | params: { 15 | keywords, 16 | limit, 17 | offset, 18 | type 19 | } 20 | }) 21 | } 22 | 23 | searchSuggest({ keywords, limit = 30, offset = 0, type = 1 }) { 24 | return axios.get('/search/suggest', { 25 | params: { 26 | keywords, 27 | limit, 28 | offset, 29 | type 30 | } 31 | }) 32 | } 33 | 34 | getRadioRecommend() { 35 | return axios.get('/dj/recommend') 36 | } 37 | 38 | getRadioRecommendType(type = 1) { 39 | return axios.get('/dj/recommend/type', { 40 | params: { 41 | type 42 | } 43 | }) 44 | } 45 | 46 | getPlayListDetail(id) { 47 | return axios.get('/playlist/detail', { 48 | params: { 49 | id 50 | } 51 | }) 52 | } 53 | 54 | getAlbumDetail(id) { 55 | return axios.get('/album', { 56 | params: { 57 | id 58 | } 59 | }) 60 | } 61 | 62 | getArtistDetail(id) { 63 | return axios.get('/artists', { 64 | params: { 65 | id 66 | } 67 | }) 68 | } 69 | 70 | getRadioDetail(rid) { 71 | return axios.get('/dj/detail', { 72 | params: { 73 | rid 74 | } 75 | }) 76 | } 77 | 78 | getRadioProgram(rid, limit = 60) { 79 | return axios.get('/dj/program', { 80 | params: { 81 | rid, 82 | limit 83 | } 84 | }) 85 | } 86 | 87 | getMusicUrl(id) { 88 | return axios.get('/music/url', { 89 | params: { 90 | id 91 | } 92 | }) 93 | } 94 | 95 | getLyric(id) { 96 | return axios.get('/lyric', { 97 | params: { 98 | id 99 | } 100 | }) 101 | } 102 | } 103 | 104 | export default new Service() -------------------------------------------------------------------------------- /server/config/development.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | port: 4000, 3 | host: 'localhost' 4 | } -------------------------------------------------------------------------------- /server/config/index.js: -------------------------------------------------------------------------------- 1 | const config = process.env.NODE_ENV || 'development' 2 | 3 | module.exports = require(`./${config}`) -------------------------------------------------------------------------------- /server/config/production.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | port: 4000, 3 | host: 'localhost' 4 | } -------------------------------------------------------------------------------- /server/graphql/album/model.js: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLObjectType, 3 | GraphQLList, 4 | GraphQLString, 5 | GraphQLInt, 6 | GraphQLFloat, 7 | GraphQLID, 8 | GraphQLBoolean 9 | } from 'graphql' 10 | import { Artist } from '../artist/model' 11 | 12 | export const Album = new GraphQLObjectType({ 13 | name: 'Album', 14 | fields: { 15 | id: { 16 | type: GraphQLID 17 | }, 18 | name: { 19 | type: GraphQLString 20 | }, 21 | artist: { 22 | type: Artist 23 | }, 24 | publishTime: { 25 | type: GraphQLFloat 26 | }, 27 | size: { 28 | type: GraphQLInt 29 | }, 30 | copyrightId: { 31 | type: GraphQLID 32 | }, 33 | status: { 34 | type: GraphQLInt 35 | }, 36 | picId: { 37 | type: GraphQLID 38 | }, 39 | picUrl: { 40 | type: GraphQLString 41 | }, 42 | blurPicUrl: { 43 | type: GraphQLString 44 | }, 45 | description: { 46 | type: GraphQLString 47 | }, 48 | subType: { 49 | type: GraphQLString 50 | }, 51 | type: { 52 | type: GraphQLString 53 | }, 54 | alias: { 55 | type: new GraphQLList(GraphQLString) 56 | }, 57 | info: { 58 | type: new GraphQLObjectType({ 59 | name: 'Info', 60 | fields: { 61 | liked: { 62 | type: GraphQLBoolean 63 | }, 64 | resourceType: { 65 | type: GraphQLInt 66 | }, 67 | commentCount: { 68 | type: GraphQLInt 69 | }, 70 | likedCount: { 71 | type: GraphQLInt 72 | }, 73 | shareCount: { 74 | type: GraphQLInt 75 | } 76 | } 77 | }) 78 | } 79 | } 80 | }) 81 | 82 | export const Albums = new GraphQLList(Album) 83 | -------------------------------------------------------------------------------- /server/graphql/album/query.js: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLObjectType, 3 | GraphQLID 4 | } from 'graphql' 5 | import { Album } from './model' 6 | import { Songs } from '../song/model' 7 | import api from '../../api' 8 | 9 | const AlbumDetail = new GraphQLObjectType({ 10 | name: 'AlbumDetail', 11 | fields: { 12 | album: { 13 | type: Album 14 | }, 15 | songs: { 16 | type: Songs 17 | } 18 | } 19 | }) 20 | 21 | const albumDetail = { 22 | type: AlbumDetail, 23 | args: { 24 | id: { 25 | type: GraphQLID 26 | } 27 | }, 28 | async resolve(root, args) { 29 | try { 30 | let res = await api.getAlbumDetail(args.id) 31 | return { 32 | songs: res.songs, 33 | album: res.album 34 | } 35 | } catch (e) { 36 | console.log(e) 37 | } 38 | } 39 | } 40 | 41 | export default albumDetail -------------------------------------------------------------------------------- /server/graphql/artist/model.js: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLObjectType, 3 | GraphQLList, 4 | GraphQLString, 5 | GraphQLInt, 6 | GraphQLID, 7 | GraphQLBoolean 8 | } from 'graphql' 9 | 10 | export const Artist = new GraphQLObjectType({ 11 | name: 'Artist', 12 | fields: { 13 | id: { 14 | type: GraphQLID 15 | }, 16 | name: { 17 | type: GraphQLString 18 | }, 19 | picUrl: { 20 | type: GraphQLString 21 | }, 22 | briefDesc: { 23 | type: GraphQLString 24 | }, 25 | albumSize: { 26 | type: GraphQLInt 27 | }, 28 | musicSize: { 29 | type: GraphQLInt 30 | }, 31 | mvSize: { 32 | type: GraphQLInt 33 | }, 34 | picId: { 35 | type: GraphQLInt 36 | }, 37 | img1v1Url: { 38 | type: GraphQLString 39 | }, 40 | img1v1: { 41 | type: GraphQLInt 42 | }, 43 | alia: { 44 | type: new GraphQLList(GraphQLString) 45 | }, 46 | alias: { 47 | type: new GraphQLList(GraphQLString) 48 | }, 49 | tns: { 50 | type: new GraphQLList(GraphQLString) 51 | }, 52 | trans: { 53 | type: GraphQLString 54 | }, 55 | transNames: { 56 | type: new GraphQLList(GraphQLString) 57 | }, 58 | followed: { 59 | type: GraphQLBoolean 60 | } 61 | } 62 | }) 63 | 64 | export const Artists = new GraphQLList(Artist) -------------------------------------------------------------------------------- /server/graphql/artist/query.js: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLObjectType, 3 | GraphQLID 4 | } from 'graphql' 5 | import { Artist } from './model' 6 | import { Songs } from '../song/model' 7 | import api from '../../api' 8 | 9 | const ArtistDetail = new GraphQLObjectType({ 10 | name: 'ArtistDetail', 11 | fields: { 12 | artist: { 13 | type: Artist 14 | }, 15 | hotSongs: { 16 | type: Songs 17 | } 18 | } 19 | }) 20 | 21 | const artistDetail = { 22 | type: ArtistDetail, 23 | args: { 24 | id: { 25 | type: GraphQLID 26 | } 27 | }, 28 | async resolve(root, args) { 29 | try { 30 | let res = await api.getArtistDetail(args.id) 31 | return { 32 | artist: res.artist, 33 | hotSongs: res.hotSongs 34 | } 35 | } catch (e) { 36 | console.log(e) 37 | } 38 | } 39 | } 40 | 41 | export default artistDetail -------------------------------------------------------------------------------- /server/graphql/banner/model.js: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLObjectType, 3 | GraphQLList, 4 | GraphQLString, 5 | GraphQLInt 6 | } from 'graphql' 7 | 8 | export const Banners = new GraphQLList(new GraphQLObjectType({ 9 | name: 'Banners', 10 | fields: { 11 | "pic": { 12 | type: GraphQLString 13 | }, 14 | "targetId": { 15 | type: GraphQLInt 16 | }, 17 | "titleColor": { 18 | type: GraphQLString 19 | }, 20 | "typeTitle": { 21 | type: GraphQLString 22 | }, 23 | "encodeId": { 24 | type: GraphQLString 25 | }, 26 | "targetType": { 27 | type: GraphQLInt 28 | } 29 | } 30 | })) -------------------------------------------------------------------------------- /server/graphql/banner/query.js: -------------------------------------------------------------------------------- 1 | import { Banners } from './model' 2 | import api from '../../api' 3 | 4 | const banners = { 5 | type: Banners, 6 | async resolve() { 7 | try { 8 | let res = await api.getBanner() 9 | return res.banners 10 | } catch (e) { 11 | console.log(e) 12 | } 13 | } 14 | } 15 | 16 | export default banners -------------------------------------------------------------------------------- /server/graphql/lyrics/model.js: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLInt, 3 | GraphQLString, 4 | GraphQLObjectType 5 | } from 'graphql' 6 | 7 | const Lyric = new GraphQLObjectType({ 8 | name: 'Lyric', 9 | fields: { 10 | lyric: { 11 | type: GraphQLString 12 | }, 13 | version: { 14 | type: GraphQLInt 15 | } 16 | } 17 | }) 18 | 19 | export const Lyrics = new GraphQLObjectType({ 20 | name: 'Lyrics', 21 | fields: { 22 | klyric: { 23 | type: Lyric 24 | }, 25 | lrc: { 26 | type: Lyric 27 | }, 28 | tlyric: { 29 | type: Lyric 30 | } 31 | } 32 | }) -------------------------------------------------------------------------------- /server/graphql/lyrics/query.js: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLID 3 | } from 'graphql' 4 | import api from '../../api' 5 | import { Lyrics } from './model' 6 | 7 | const lyrics = { 8 | type: Lyrics, 9 | args: { 10 | id: { 11 | type: GraphQLID 12 | } 13 | }, 14 | async resolve(root, args) { 15 | try { 16 | let res = await api.getLyric(args.id) 17 | 18 | return { 19 | klyric: res.klyric, 20 | lrc: res.lrc, 21 | tlyric: res.tlyric 22 | } 23 | } catch (e) { 24 | console.log(e) 25 | } 26 | } 27 | } 28 | 29 | export default lyrics -------------------------------------------------------------------------------- /server/graphql/music/model.js: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLInt, 3 | GraphQLString, 4 | GraphQLObjectType, 5 | GraphQLID, 6 | GraphQLFloat 7 | } from 'graphql' 8 | 9 | export const Music = new GraphQLObjectType({ 10 | name: 'Music', 11 | fields: { 12 | "url": { 13 | type: GraphQLString 14 | }, 15 | "type": { 16 | type: GraphQLString 17 | }, 18 | "size": { 19 | type: GraphQLInt 20 | }, 21 | "id": { 22 | type: GraphQLID 23 | }, 24 | "payed": { 25 | type: GraphQLInt 26 | }, 27 | "fee": { 28 | type: GraphQLInt 29 | }, 30 | "br": { 31 | type: GraphQLInt 32 | }, 33 | "fid": { 34 | type: GraphQLID 35 | }, 36 | "vd": { 37 | type: GraphQLFloat 38 | }, 39 | "name": { 40 | type: GraphQLString 41 | }, 42 | "extension": { 43 | type: GraphQLString 44 | }, 45 | "sr": { 46 | type: GraphQLInt 47 | }, 48 | "dfsId": { 49 | type: GraphQLID 50 | }, 51 | "bitrate": { 52 | type: GraphQLInt 53 | }, 54 | "playTime": { 55 | type: GraphQLInt 56 | }, 57 | "volumeDelta": { 58 | type: GraphQLFloat 59 | } 60 | } 61 | }) 62 | -------------------------------------------------------------------------------- /server/graphql/music/query.js: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLID 3 | } from 'graphql' 4 | import api from '../../api' 5 | import { Music } from './model' 6 | 7 | const music = { 8 | type: Music, 9 | args: { 10 | id: { 11 | type: GraphQLID 12 | } 13 | }, 14 | async resolve(root, args) { 15 | try { 16 | let res = await api.getMusicUrl(args.id) 17 | return res.data[0] 18 | } catch (e) { 19 | console.log(e) 20 | } 21 | } 22 | } 23 | 24 | export default music -------------------------------------------------------------------------------- /server/graphql/mv/model.js: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLObjectType, 3 | GraphQLList, 4 | GraphQLString, 5 | GraphQLInt, 6 | GraphQLID, 7 | GraphQLBoolean, 8 | GraphQLFloat 9 | } from 'graphql' 10 | import { Artist } from '../artist/model' 11 | 12 | export const Mv = new GraphQLObjectType({ 13 | name: 'Mv', 14 | fields: { 15 | id: { 16 | type: GraphQLID 17 | }, 18 | cover: { 19 | type: GraphQLString 20 | }, 21 | name: { 22 | type: GraphQLString 23 | }, 24 | playCount: { 25 | type: GraphQLInt 26 | }, 27 | briefDesc: { 28 | type: GraphQLString 29 | }, 30 | desc: { 31 | type: GraphQLString 32 | }, 33 | artistName: { 34 | type: GraphQLString 35 | }, 36 | artistId: { 37 | type: GraphQLID 38 | }, 39 | duration: { 40 | type: GraphQLFloat 41 | }, 42 | mark: { 43 | type: GraphQLInt 44 | }, 45 | subed: { 46 | type: GraphQLBoolean 47 | }, 48 | artists: { 49 | type: new GraphQLList(Artist) 50 | } 51 | } 52 | }) 53 | 54 | export const Mvs = new GraphQLList(Mv) -------------------------------------------------------------------------------- /server/graphql/personalized/model.js: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLObjectType, 3 | GraphQLList, 4 | GraphQLString, 5 | GraphQLInt, 6 | GraphQLBoolean, 7 | GraphQLFloat, 8 | GraphQLID 9 | } from 'graphql' 10 | 11 | export const Personalized = new GraphQLList(new GraphQLObjectType({ 12 | name: 'Personalized', 13 | fields: { 14 | "id": { 15 | type: GraphQLID 16 | }, 17 | "type": { 18 | type: GraphQLInt 19 | }, 20 | "name": { 21 | type: GraphQLString 22 | }, 23 | "copywriter": { 24 | type: GraphQLString 25 | }, 26 | "picUrl": { 27 | type: GraphQLString 28 | }, 29 | "canDislike": { 30 | type: GraphQLBoolean 31 | }, 32 | "playCount": { 33 | type: GraphQLFloat 34 | }, 35 | "trackCount": { 36 | type: GraphQLInt 37 | }, 38 | "highQuality": { 39 | type: GraphQLBoolean 40 | }, 41 | "alg": { 42 | type: GraphQLString 43 | } 44 | } 45 | })) -------------------------------------------------------------------------------- /server/graphql/personalized/query.js: -------------------------------------------------------------------------------- 1 | import { Personalized } from './model' 2 | import api from '../../api' 3 | 4 | const personalized = { 5 | type: Personalized, 6 | async resolve() { 7 | try { 8 | let res = await api.getPersonalized() 9 | return res.result 10 | } catch (e) { 11 | console.log(e) 12 | } 13 | } 14 | } 15 | 16 | export default personalized -------------------------------------------------------------------------------- /server/graphql/playList/model.js: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLInt, 3 | GraphQLObjectType, 4 | GraphQLString, 5 | GraphQLID, 6 | GraphQLFloat, 7 | GraphQLBoolean, 8 | GraphQLList 9 | } from 'graphql' 10 | import { User } from '../user/model' 11 | import { Artists } from '../artist/model' 12 | import { Album } from '../album/model' 13 | import { Music } from '../music/model' 14 | 15 | const Track = new GraphQLObjectType({ 16 | name: 'Track', 17 | fields: { 18 | "name": { 19 | type: GraphQLString 20 | }, 21 | "id": { 22 | type: GraphQLID 23 | }, 24 | "position": { 25 | type: GraphQLInt 26 | }, 27 | "status": { 28 | type: GraphQLInt 29 | }, 30 | "fee": { 31 | type: GraphQLInt 32 | }, 33 | "artists": { 34 | type: Artists 35 | }, 36 | "album": { 37 | type: Album 38 | }, 39 | "starred": { 40 | type: GraphQLBoolean 41 | }, 42 | "popularity": { 43 | type: GraphQLInt 44 | }, 45 | "score": { 46 | type: GraphQLInt 47 | }, 48 | "starredNum": { 49 | type: GraphQLInt 50 | }, 51 | "duration": { 52 | type: GraphQLInt 53 | }, 54 | "playedNum": { 55 | type: GraphQLInt 56 | }, 57 | "dayPlays": { 58 | type: GraphQLInt 59 | }, 60 | "hearTime": { 61 | type: GraphQLInt 62 | }, 63 | "ftype": { 64 | type: GraphQLInt 65 | }, 66 | "copyright": { 67 | type: GraphQLInt 68 | }, 69 | "rtype": { 70 | type: GraphQLInt 71 | }, 72 | "mvid": { 73 | type: GraphQLID 74 | }, 75 | "bMusic": { 76 | type: Music 77 | }, 78 | "hMusic": { 79 | type: Music 80 | }, 81 | "mMusic": { 82 | type: Music 83 | }, 84 | "lMusic": { 85 | type: Music 86 | } 87 | } 88 | }) 89 | 90 | export const PlayList = new GraphQLObjectType({ 91 | name: 'PlayList', 92 | fields: { 93 | "subscribed": { 94 | type: GraphQLBoolean 95 | }, 96 | "creator": { 97 | type: User 98 | }, 99 | "bookCount": { 100 | type: GraphQLInt 101 | }, 102 | "alg": { 103 | type: GraphQLString 104 | }, 105 | "artists": { 106 | type: Artists 107 | }, 108 | "tracks": { 109 | type: new GraphQLList(Track) 110 | }, 111 | "status": { 112 | type: GraphQLInt 113 | }, 114 | "playCount": { 115 | type: GraphQLInt 116 | }, 117 | "trackCount": { 118 | type: GraphQLInt 119 | }, 120 | "trackUpdateTime": { 121 | type: GraphQLFloat 122 | }, 123 | "totalDuration": { 124 | type: GraphQLInt 125 | }, 126 | "highQuality": { 127 | type: GraphQLBoolean 128 | }, 129 | "anonimous": { 130 | type: GraphQLBoolean 131 | }, 132 | "coverImgId": { 133 | type: GraphQLID 134 | }, 135 | "createTime": { 136 | type: GraphQLFloat 137 | }, 138 | "updateTime": { 139 | type: GraphQLFloat 140 | }, 141 | "specialType": { 142 | type: GraphQLInt 143 | }, 144 | "userId": { 145 | type: GraphQLID 146 | }, 147 | "commentThreadId": { 148 | type: GraphQLString 149 | }, 150 | "coverImgUrl": { 151 | type: GraphQLString 152 | }, 153 | "privacy": { 154 | type: GraphQLInt 155 | }, 156 | "newImported": { 157 | type: GraphQLBoolean 158 | }, 159 | "description": { 160 | type: GraphQLString 161 | }, 162 | "ordered": { 163 | type: GraphQLBoolean 164 | }, 165 | "adType": { 166 | type: GraphQLInt 167 | }, 168 | "trackNumberUpdateTime": { 169 | type: GraphQLFloat 170 | }, 171 | "subscribedCount": { 172 | type: GraphQLInt 173 | }, 174 | "cloudTrackCount": { 175 | type: GraphQLInt 176 | }, 177 | "name": { 178 | type: GraphQLString 179 | }, 180 | "id": { 181 | type: GraphQLID 182 | }, 183 | "shareCount": { 184 | type: GraphQLInt 185 | }, 186 | "coverImgId_str": { 187 | type: GraphQLString 188 | }, 189 | "commentCount": { 190 | type: GraphQLInt 191 | } 192 | } 193 | }) 194 | 195 | export const PlayLists = new GraphQLList(PlayList) -------------------------------------------------------------------------------- /server/graphql/playList/query.js: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLID 3 | } from 'graphql' 4 | import { PlayList } from './model' 5 | import api from '../../api' 6 | 7 | const playList = { 8 | type: PlayList, 9 | args: { 10 | id: { 11 | type: GraphQLID 12 | } 13 | }, 14 | async resolve(root, args) { 15 | try { 16 | let res = await api.getPlayListDetail(args.id) 17 | return res.result 18 | } catch (e) { 19 | console.log(e) 20 | } 21 | } 22 | } 23 | 24 | export default playList -------------------------------------------------------------------------------- /server/graphql/radio/model.js: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLObjectType, 3 | GraphQLList, 4 | GraphQLString, 5 | GraphQLInt, 6 | GraphQLFloat, 7 | GraphQLID, 8 | GraphQLBoolean 9 | } from 'graphql' 10 | import { User } from '../user/model' 11 | import { Song } from '../song/model' 12 | 13 | const Comment = new GraphQLObjectType({ 14 | name: 'Comment', 15 | fields: { 16 | commentId: { 17 | type: GraphQLID 18 | }, 19 | programId: { 20 | type: GraphQLID 21 | }, 22 | content: { 23 | type: GraphQLString 24 | }, 25 | programName: { 26 | type: GraphQLString 27 | }, 28 | userProfile: { 29 | type: User 30 | } 31 | } 32 | }) 33 | 34 | export const Radio = new GraphQLObjectType({ 35 | name: 'Radio', 36 | fields: { 37 | id: { 38 | type: GraphQLID 39 | }, 40 | dj: { 41 | type: User 42 | }, 43 | name: { 44 | type: GraphQLString 45 | }, 46 | picUrl: { 47 | type: GraphQLString 48 | }, 49 | desc: { 50 | type: GraphQLString 51 | }, 52 | commentDatas: { 53 | type: new GraphQLList(Comment) 54 | }, 55 | subCount: { 56 | type: GraphQLInt 57 | }, 58 | programCount: { 59 | type: GraphQLInt 60 | }, 61 | createTime: { 62 | type: GraphQLFloat 63 | }, 64 | categoryId: { 65 | type: GraphQLID 66 | }, 67 | category: { 68 | type: GraphQLString 69 | }, 70 | radioFeeType: { 71 | type: GraphQLInt 72 | }, 73 | feeScope: { 74 | type: GraphQLInt 75 | }, 76 | buyed: { 77 | type: GraphQLBoolean 78 | }, 79 | purchaseCount: { 80 | type: GraphQLInt 81 | }, 82 | price: { 83 | type: GraphQLFloat 84 | }, 85 | originalPrice: { 86 | type: GraphQLFloat 87 | }, 88 | lastProgramCreateTime: { 89 | type: GraphQLFloat 90 | }, 91 | lastProgramName: { 92 | type: GraphQLString 93 | }, 94 | lastProgramId: { 95 | type: GraphQLID 96 | }, 97 | picId: { 98 | type: GraphQLID 99 | }, 100 | shareCount: { 101 | type: GraphQLInt 102 | }, 103 | likedCount: { 104 | type: GraphQLInt 105 | }, 106 | alg: { 107 | type: GraphQLString 108 | }, 109 | commentCount: { 110 | type: GraphQLInt 111 | }, 112 | rcmdtext: { 113 | type: GraphQLString 114 | } 115 | } 116 | }) 117 | 118 | export const Radios = new GraphQLList(Radio) 119 | 120 | const RadioProgram = new GraphQLObjectType({ 121 | name: 'RadioPrograms', 122 | fields: { 123 | "mainSong": { 124 | type: Song 125 | }, 126 | "dj": { 127 | type: User 128 | }, 129 | "blurCoverUrl": { 130 | type: GraphQLString 131 | }, 132 | "radio": { 133 | type: Radio 134 | }, 135 | "duration": { 136 | type: GraphQLInt 137 | }, 138 | "buyed": { 139 | type: GraphQLBoolean 140 | }, 141 | "canReward": { 142 | type: GraphQLBoolean 143 | }, 144 | "isPublish": { 145 | type: GraphQLBoolean 146 | }, 147 | "serialNum": { 148 | type: GraphQLInt 149 | }, 150 | "createTime": { 151 | type: GraphQLFloat 152 | }, 153 | "listenerCount": { 154 | type: GraphQLInt 155 | }, 156 | "subscribedCount": { 157 | type: GraphQLInt 158 | }, 159 | "reward": { 160 | type: GraphQLBoolean 161 | }, 162 | "feeScope": { 163 | type: GraphQLInt 164 | }, 165 | "pubStatus": { 166 | type: GraphQLInt 167 | }, 168 | "bdAuditStatus": { 169 | type: GraphQLInt 170 | }, 171 | "coverUrl": { 172 | type: GraphQLString 173 | }, 174 | "mainTrackId": { 175 | type: GraphQLID 176 | }, 177 | "programFeeType": { 178 | type: GraphQLInt 179 | }, 180 | "description": { 181 | type: GraphQLString 182 | }, 183 | "trackCount": { 184 | type: GraphQLInt 185 | }, 186 | "name": { 187 | type: GraphQLString 188 | }, 189 | "id": { 190 | type: GraphQLID 191 | }, 192 | "shareCount": { 193 | type: GraphQLInt 194 | }, 195 | "subscribed": { 196 | type: GraphQLBoolean 197 | }, 198 | "likedCount": { 199 | type: GraphQLInt 200 | }, 201 | "commentCount": { 202 | type: GraphQLInt 203 | } 204 | } 205 | }) 206 | 207 | export const RadioPrograms = new GraphQLList(RadioProgram) -------------------------------------------------------------------------------- /server/graphql/radio/query.js: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLInt, 3 | GraphQLID 4 | } from 'graphql' 5 | import { Radio, Radios, RadioPrograms } from './model' 6 | import api from '../../api' 7 | 8 | const radioRecommends = { 9 | type: Radios, 10 | async resolve() { 11 | try { 12 | let res = await api.getRadioRecommend() 13 | return res.djRadios 14 | } catch (e) { 15 | console.log(e) 16 | } 17 | } 18 | } 19 | 20 | const radioRecommendType = { 21 | type: Radios, 22 | args: { 23 | type: { 24 | type: GraphQLInt 25 | } 26 | }, 27 | async resolve(root, args) { 28 | try { 29 | let res = await api.getRadioRecommendType(args.type) 30 | return res.djRadios 31 | } catch (e) { 32 | console.log(e) 33 | } 34 | } 35 | } 36 | 37 | const radioDetail = { 38 | type: Radio, 39 | args: { 40 | rid: { 41 | type: GraphQLID 42 | } 43 | }, 44 | async resolve(root, args) { 45 | try { 46 | let res = await api.getRadioDetail(args.rid) 47 | return res.djRadio 48 | } catch (e) { 49 | console.log(e) 50 | } 51 | } 52 | } 53 | 54 | const radioPrograms = { 55 | type: RadioPrograms, 56 | args: { 57 | rid: { 58 | type: GraphQLID 59 | } 60 | }, 61 | async resolve(root, args) { 62 | try { 63 | let res = await api.getRadioProgram(args.rid) 64 | return res.programs 65 | } catch (e) { 66 | console.log(e) 67 | } 68 | } 69 | } 70 | 71 | export default { 72 | radioRecommends, 73 | radioRecommendType, 74 | radioDetail, 75 | radioPrograms 76 | } -------------------------------------------------------------------------------- /server/graphql/schema.js: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLObjectType, 3 | GraphQLSchema 4 | } from 'graphql' 5 | 6 | import banners from './banner/query' 7 | import personalized from './personalized/query' 8 | import search from './search/query' 9 | import radio from './radio/query' 10 | import playList from './playList/query' 11 | import albumDetail from './album/query' 12 | import artistDetail from './artist/query' 13 | import music from './music/query' 14 | import lyrics from './lyrics/query' 15 | 16 | const schema = new GraphQLSchema({ 17 | query: new GraphQLObjectType({ 18 | name: 'Query', 19 | fields: { 20 | banners, 21 | personalized, 22 | ...search, 23 | ...radio, 24 | playList, 25 | albumDetail, 26 | artistDetail, 27 | music, 28 | lyrics 29 | } 30 | }) 31 | }) 32 | 33 | export default schema -------------------------------------------------------------------------------- /server/graphql/search/model.js: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLObjectType, 3 | GraphQLInt, 4 | GraphQLUnionType 5 | } from 'graphql' 6 | import { Radios } from '../radio/model' 7 | import { PlayLists } from '../playList/model' 8 | import { Albums } from '../album/model' 9 | import { Artists } from '../artist/model' 10 | import { Mvs } from '../mv/model' 11 | import { Songs } from '../song/model' 12 | 13 | const SongResult = new GraphQLObjectType({ 14 | name: 'SongResult', 15 | fields: { 16 | songCount: { 17 | type: GraphQLInt 18 | }, 19 | songs: { 20 | type: Songs 21 | } 22 | } 23 | }) 24 | 25 | const AlbumResult = new GraphQLObjectType({ 26 | name: 'AlbumResult', 27 | fields: { 28 | albumCount: { 29 | type: GraphQLInt 30 | }, 31 | albums: { 32 | type: Albums 33 | } 34 | } 35 | }) 36 | 37 | const ArtistResult = new GraphQLObjectType({ 38 | name: 'ArtistResult', 39 | fields: { 40 | artistCount: { 41 | type: GraphQLInt 42 | }, 43 | artists: { 44 | type: Artists 45 | } 46 | } 47 | }) 48 | 49 | const MvResult = new GraphQLObjectType({ 50 | name: 'MvResult', 51 | fields: { 52 | mvCount: { 53 | type: GraphQLInt 54 | }, 55 | mvs: { 56 | type: Mvs 57 | } 58 | } 59 | }) 60 | 61 | const RadioResult = new GraphQLObjectType({ 62 | name: 'RadioResult', 63 | fields: { 64 | djRadiosCount: { 65 | type: GraphQLInt 66 | }, 67 | djRadios: { 68 | type: Radios 69 | } 70 | } 71 | }) 72 | 73 | const PlayListResult = new GraphQLObjectType({ 74 | name: 'PlayListResult', 75 | fields: { 76 | playlistCount: { 77 | type: GraphQLInt 78 | }, 79 | playlists: { 80 | type: PlayLists 81 | } 82 | } 83 | }) 84 | 85 | const types = { 86 | songCount: SongResult, 87 | mvCount: MvResult, 88 | albumCount: AlbumResult, 89 | artistCount: ArtistResult, 90 | playlistCount: PlayListResult, 91 | djRadiosCount: RadioResult 92 | } 93 | 94 | export const SearchType = new GraphQLUnionType({ 95 | name: 'SearchType', 96 | types: [SongResult, MvResult, AlbumResult, ArtistResult, PlayListResult, RadioResult], 97 | resolveType(value) { 98 | let ret 99 | 100 | Object.keys(types).some(key => { 101 | if (value.hasOwnProperty(key)) { 102 | ret = types[key] 103 | return true 104 | } 105 | }) 106 | 107 | return ret 108 | } 109 | }) 110 | -------------------------------------------------------------------------------- /server/graphql/search/query.js: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLString, 3 | GraphQLInt, 4 | GraphQLObjectType 5 | } from 'graphql' 6 | import { SearchType } from './model' 7 | import { Albums } from '../album/model' 8 | import { Artists } from '../artist/model' 9 | import { Mvs } from '../mv/model' 10 | import { Songs } from '../song/model' 11 | import api from '../../api' 12 | 13 | const searchArgs = { 14 | keywords: { 15 | type: GraphQLString 16 | }, 17 | limit: { 18 | type: GraphQLInt 19 | }, 20 | offset: { 21 | type: GraphQLInt 22 | }, 23 | type: { 24 | type: GraphQLInt 25 | } 26 | } 27 | 28 | const searchResult = { 29 | type: SearchType, 30 | args: searchArgs, 31 | async resolve(root, args) { 32 | try { 33 | let res = await api.search(args) 34 | return res.result 35 | } catch (e) { 36 | console.log(e) 37 | } 38 | } 39 | } 40 | 41 | const searchSuggest = { 42 | type: new GraphQLObjectType({ 43 | name: 'SearchSuggest', 44 | fields: { 45 | songs: { 46 | type: Songs 47 | }, 48 | albums: { 49 | type: Albums 50 | }, 51 | artists: { 52 | type: Artists 53 | }, 54 | mvs: { 55 | type: Mvs 56 | } 57 | } 58 | }), 59 | args: searchArgs, 60 | async resolve(root, args) { 61 | try { 62 | let res = await api.searchSuggest(args) 63 | return res.result 64 | } catch (e) { 65 | console.log(e) 66 | } 67 | } 68 | } 69 | 70 | export default { 71 | searchResult, 72 | searchSuggest 73 | } -------------------------------------------------------------------------------- /server/graphql/song/model.js: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLObjectType, 3 | GraphQLList, 4 | GraphQLString, 5 | GraphQLInt, 6 | GraphQLFloat, 7 | GraphQLID, 8 | GraphQLBoolean 9 | } from 'graphql' 10 | import { Artist } from '../artist/model' 11 | import { Album } from '../album/model' 12 | import { Music } from '../music/model' 13 | 14 | export const Song = new GraphQLObjectType({ 15 | name: 'Song', 16 | fields: { 17 | "id": { 18 | type: GraphQLID 19 | }, 20 | "name": { 21 | type: GraphQLString 22 | }, 23 | "artists": { 24 | type: new GraphQLList(Artist) 25 | }, 26 | "album": { 27 | type: Album 28 | }, 29 | "duration": { 30 | type: GraphQLFloat 31 | }, 32 | "copyrightId": { 33 | type: GraphQLID 34 | }, 35 | "status": { 36 | type: GraphQLInt 37 | }, 38 | "starred": { 39 | type: GraphQLBoolean 40 | }, 41 | "rtype": { 42 | type: GraphQLInt 43 | }, 44 | "ftype": { 45 | type: GraphQLInt 46 | }, 47 | "mvid": { 48 | type: GraphQLID 49 | }, 50 | "fee": { 51 | type: GraphQLInt 52 | }, 53 | "ar": { 54 | type: new GraphQLList(Artist) 55 | }, 56 | "al": { 57 | type: Artist 58 | }, 59 | "pst": { 60 | type: GraphQLInt 61 | }, 62 | "v": { 63 | type: GraphQLInt 64 | }, 65 | "pop": { 66 | type: GraphQLInt 67 | }, 68 | "mst": { 69 | type: GraphQLInt 70 | }, 71 | "cp": { 72 | type: GraphQLInt 73 | }, 74 | "dt": { 75 | type: GraphQLInt 76 | }, 77 | "h": { 78 | type: Music 79 | }, 80 | "l": { 81 | type: Music 82 | }, 83 | "m": { 84 | type: Music 85 | }, 86 | "mv": { 87 | type: GraphQLID 88 | }, 89 | "privilege": { 90 | type: new GraphQLObjectType({ 91 | name: 'Privilege', 92 | fields: { 93 | "id": { 94 | type: GraphQLID 95 | }, 96 | "fee": { 97 | type: GraphQLInt 98 | }, 99 | "payed": { 100 | type: GraphQLInt 101 | }, 102 | "st": { 103 | type: GraphQLInt 104 | }, 105 | "pl": { 106 | type: GraphQLInt 107 | }, 108 | "dl": { 109 | type: GraphQLInt 110 | }, 111 | "sp": { 112 | type: GraphQLInt 113 | }, 114 | "cp": { 115 | type: GraphQLInt 116 | }, 117 | "subp": { 118 | type: GraphQLInt 119 | }, 120 | "cs": { 121 | type: GraphQLBoolean 122 | }, 123 | "maxbr": { 124 | type: GraphQLInt 125 | }, 126 | "fl": { 127 | type: GraphQLInt 128 | }, 129 | "toast": { 130 | type: GraphQLBoolean 131 | }, 132 | "flag": { 133 | type: GraphQLInt 134 | }, 135 | "preSell": { 136 | type: GraphQLBoolean 137 | } 138 | } 139 | }) 140 | } 141 | } 142 | }) 143 | 144 | export const Songs = new GraphQLList(Song) 145 | -------------------------------------------------------------------------------- /server/graphql/user/model.js: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLInt, 3 | GraphQLObjectType, 4 | GraphQLString, 5 | GraphQLID, 6 | GraphQLFloat, 7 | GraphQLBoolean 8 | } from 'graphql' 9 | 10 | export const User = new GraphQLObjectType({ 11 | name: 'User', 12 | fields: { 13 | "nickname": { 14 | type: GraphQLString 15 | }, 16 | "userId": { 17 | type: GraphQLID 18 | }, 19 | "userType": { 20 | type: GraphQLInt 21 | }, 22 | "authStatus": { 23 | type: GraphQLInt 24 | }, 25 | "defaultAvatar": { 26 | type: GraphQLBoolean 27 | }, 28 | "accountStatus": { 29 | type: GraphQLInt 30 | }, 31 | "description": { 32 | type: GraphQLString 33 | }, 34 | "detailDescription": { 35 | type: GraphQLString 36 | }, 37 | "avatarImgId": { 38 | type: GraphQLID 39 | }, 40 | "backgroundImgId": { 41 | type: GraphQLID 42 | }, 43 | "rewardCount": { 44 | type: GraphQLInt 45 | }, 46 | "authority": { 47 | type: GraphQLInt 48 | }, 49 | "mutual": { 50 | type: GraphQLBoolean 51 | }, 52 | "vipType": { 53 | type: GraphQLInt 54 | }, 55 | "province": { 56 | type: GraphQLInt 57 | }, 58 | "followed": { 59 | type: GraphQLBoolean 60 | }, 61 | "avatarUrl": { 62 | type: GraphQLString 63 | }, 64 | "gender": { 65 | type: GraphQLInt 66 | }, 67 | "city": { 68 | type: GraphQLInt 69 | }, 70 | "birthday": { 71 | type: GraphQLFloat 72 | }, 73 | "signature": { 74 | type: GraphQLString 75 | }, 76 | "backgroundUrl": { 77 | type: GraphQLString 78 | }, 79 | "djStatus": { 80 | type: GraphQLInt 81 | } 82 | } 83 | }) -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | require('babel-core/register')({ 2 | "presets": [ 3 | "stage-3", 4 | ["latest-node", { "target": "current" }] 5 | ] 6 | }) 7 | require('babel-polyfill') 8 | require('./server') -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "cross-env NODE_ENV=development nodemon node index.js", 8 | "start": "cross-env NODE_ENV=production node index.js" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "apollo-server-koa": "^1.3.4", 14 | "graphql": "^0.13.2", 15 | "koa": "^2.5.0", 16 | "koa-bodyparser": "^4.2.0", 17 | "koa-router": "^7.4.0", 18 | "koa-static": "^4.0.2" 19 | }, 20 | "devDependencies": { 21 | "nodemon": "^1.17.4", 22 | "babel-core": "^6.26.0", 23 | "babel-preset-latest-node": "^1.0.0", 24 | "babel-preset-stage-3": "^6.24.1" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | import Koa from 'koa' 2 | import KoaStatic from 'koa-static' 3 | import Router from 'koa-router' 4 | import bodyParser from 'koa-bodyparser' 5 | import path from 'path' 6 | import fs from 'fs' 7 | import { graphqlKoa, graphiqlKoa } from 'apollo-server-koa' 8 | import schema from './graphql/schema' 9 | import config from './config' 10 | 11 | const isProd = process.env.NODE_ENV === 'production' 12 | 13 | const app = new Koa() 14 | const router = new Router() 15 | 16 | const port = process.env.PORT || config.port 17 | const host = process.env.HOST || config.host 18 | 19 | app.use(bodyParser()) 20 | app.use(KoaStatic(path.resolve(__dirname, '../build'))) 21 | 22 | router.all('/graphql', graphqlKoa({ schema })) 23 | !isProd && router.get('/graphiql', graphiqlKoa({ endpointURL: '/graphql' })) 24 | 25 | router.get('*', (ctx, next) => { 26 | ctx.body = fs.readFileSync('../build/index.html', 'utf-8') 27 | }) 28 | 29 | app 30 | .use(router.routes()) 31 | .use(router.allowedMethods()) 32 | 33 | app.listen(port, () => console.log(`server running on http://${host}:${port}`)) -------------------------------------------------------------------------------- /server/utils/axios.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | axios.defaults.baseURL = 'http://localhost:8080' 4 | 5 | axios.interceptors.response.use(res => { 6 | return res && res.data 7 | }, err => { 8 | return Promise.reject(err) 9 | }) 10 | 11 | export default axios -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { BrowserRouter as Router, Switch, Redirect } from 'react-router-dom' 3 | import { routes } from './utils/reactUtil' 4 | import routeConfig from './router' 5 | import Slidebar from './components/slidebar/Slidebar' 6 | import Play from './containers/Play' 7 | 8 | class App extends Component { 9 | render() { 10 | return ( 11 |
12 | 13 | 14 | { 15 | routes(routeConfig) 16 | } 17 | 18 | 19 | 20 | 21 | 22 |
23 | ) 24 | } 25 | } 26 | 27 | export default App 28 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from './App' 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div') 7 | ReactDOM.render(, div) 8 | ReactDOM.unmountComponentAtNode(div) 9 | }) 10 | -------------------------------------------------------------------------------- /src/actions/actionTypes.js: -------------------------------------------------------------------------------- 1 | export const GET_HOME_DATA = 'GET_HOME_DATA' 2 | 3 | export const SET_SLIDEBAR_STATE = 'SET_SLIDEBAR_STATE' 4 | 5 | export const GET_SEARCH_SUGGEST = 'GET_SEARCH_SUGGEST' 6 | 7 | export const ADD_SEARCH_HISTORY = 'ADD_SEARCH_HISTORY' 8 | 9 | export const RM_SEARCH_HISTORY = 'RM_SEARCH_HISTORY' 10 | 11 | export const GET_RADIO = 'GET_RADIO' 12 | 13 | export const ADD_MUSIC = 'ADD_MUSIC' 14 | 15 | export const REMOVE_MUSIC = 'REMOVE_MUSIC' 16 | 17 | export const REMOVE_ALL_MUSIC = 'REMOVE_ALL_MUSIC' 18 | 19 | export const PREV_MUSIC = 'PREV_MUSIC' 20 | 21 | export const NEXT_MUSIC = 'NEXT_MUSIC' 22 | 23 | export const SWITCH_MUSIC = 'SWITCH_MUSIC' 24 | 25 | export const TOGGLE_MODE = 'TOGGLE_MODE' -------------------------------------------------------------------------------- /src/actions/home.js: -------------------------------------------------------------------------------- 1 | import * as types from './actionTypes' 2 | import api from 'api' 3 | 4 | export const getHomeData = () => ({ 5 | types: { 6 | successType: types.GET_HOME_DATA 7 | }, 8 | shouldCallAPI: state => Object.keys(state.homeData.banners).length === 0, 9 | callAPI: () => api.getHomeData() 10 | }) -------------------------------------------------------------------------------- /src/actions/index.js: -------------------------------------------------------------------------------- 1 | import * as types from './actionTypes' 2 | 3 | export const setSlidebarState = (state) => ({ 4 | type: types.SET_SLIDEBAR_STATE, 5 | slidebarState: state 6 | }) -------------------------------------------------------------------------------- /src/actions/music.js: -------------------------------------------------------------------------------- 1 | import * as types from './actionTypes' 2 | 3 | export const addMusic = music => ({ 4 | type: types.ADD_MUSIC, 5 | music 6 | }) 7 | 8 | export const removeMusic = music => ({ 9 | type: types.REMOVE_MUSIC, 10 | music 11 | }) 12 | 13 | export const removeAllMusic = () => ({ 14 | type: types.REMOVE_ALL_MUSIC 15 | }) 16 | 17 | export const prevMusic = () => ({ 18 | type: types.PREV_MUSIC 19 | }) 20 | 21 | export const nextMusic = (isAuto = false) => ({ 22 | type: types.NEXT_MUSIC, 23 | isAuto 24 | }) 25 | 26 | export const switchMusic = (id) => ({ 27 | type: types.SWITCH_MUSIC, 28 | id 29 | }) 30 | 31 | export const toggleMode = () => ({ 32 | type: types.TOGGLE_MODE 33 | }) -------------------------------------------------------------------------------- /src/actions/radio.js: -------------------------------------------------------------------------------- 1 | import * as types from './actionTypes' 2 | import api from 'api' 3 | 4 | export const getRadio = () => ({ 5 | types: { 6 | successType: types.GET_RADIO 7 | }, 8 | shouldCallAPI: state => Object.keys(state.radio.radioRecommends).length === 0, 9 | callAPI: () => api.getRadio() 10 | }) 11 | -------------------------------------------------------------------------------- /src/actions/search.js: -------------------------------------------------------------------------------- 1 | import * as types from './actionTypes' 2 | import api from 'api' 3 | 4 | export const getSearchSuggest = ({ keywords, limit, offset, type }) => ({ 5 | types: { 6 | successType: types.GET_SEARCH_SUGGEST 7 | }, 8 | callAPI: () => api.getSearchSuggest({ keywords, limit, offset, type }) 9 | }) 10 | 11 | export const addSearchHistory = (keywords) => ({ 12 | type: types.ADD_SEARCH_HISTORY, 13 | keywords 14 | }) 15 | 16 | export const rmSearchHistory = (keywords) => ({ 17 | type: types.RM_SEARCH_HISTORY, 18 | keywords 19 | }) -------------------------------------------------------------------------------- /src/assets/font/iconfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kim09AI/react-music/8205e2c3f6fc2be14bfabb444585f39e9d8dafc0/src/assets/font/iconfont.eot -------------------------------------------------------------------------------- /src/assets/font/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kim09AI/react-music/8205e2c3f6fc2be14bfabb444585f39e9d8dafc0/src/assets/font/iconfont.ttf -------------------------------------------------------------------------------- /src/assets/font/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kim09AI/react-music/8205e2c3f6fc2be14bfabb444585f39e9d8dafc0/src/assets/font/iconfont.woff -------------------------------------------------------------------------------- /src/assets/media/fixAutoPlay.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kim09AI/react-music/8205e2c3f6fc2be14bfabb444585f39e9d8dafc0/src/assets/media/fixAutoPlay.mp3 -------------------------------------------------------------------------------- /src/components/albumList/AlbumList.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { getDate } from 'utils/index' 4 | import './albumList.styl' 5 | 6 | function AlbumList(props) { 7 | let { list } = props 8 | return ( 9 |
10 | { 11 | list.map((item, index) => ( 12 |
13 |
14 |
15 |
16 | {item.name} 17 | { 18 | item.alias[0] &&  ({item.alias[0]}) 19 | } 20 |
21 |
22 | {item.artist.name} 23 |  {getDate(item.publishTime)} 24 |
25 |
26 |
27 | )) 28 | } 29 |
30 | ) 31 | } 32 | 33 | AlbumList.propTypes = { 34 | list: PropTypes.arrayOf(PropTypes.object).isRequired 35 | } 36 | 37 | export default AlbumList -------------------------------------------------------------------------------- /src/components/albumList/albumList.styl: -------------------------------------------------------------------------------- 1 | .album-list 2 | .item 3 | padding: 5px 4 | border-bottom: 1px solid #dddddd 5 | display: flex 6 | align-items: center 7 | .icon 8 | width: 20% 9 | padding-top: 20% 10 | min-width: 20% 11 | margin-right: 10px 12 | background-repeat: no-repeat 13 | background-size: cover 14 | .info 15 | font-size: 16px 16 | color: #333333 17 | line-height: 1.2 18 | overflow hidden 19 | .name 20 | color: #333333 21 | overflow hidden 22 | white-space nowrap 23 | text-overflow: ellipsis 24 | .alias 25 | color: #707070 26 | .other 27 | font-size: 12px 28 | margin-top: 8px 29 | color: #707070 30 | .artist 31 | color: #507daf -------------------------------------------------------------------------------- /src/components/alert/Alert.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import classNames from 'classnames' 3 | import './alert.styl' 4 | 5 | class Alert extends React.Component { 6 | constructor(props) { 7 | super(props) 8 | this.state = { 9 | show: false, 10 | msg: '' 11 | } 12 | this.transitionEnd = this.transitionEnd.bind(this) 13 | } 14 | 15 | componentDidMount() { 16 | this.alert.addEventListener('webkitTransitionEnd', this.transitionEnd) 17 | } 18 | 19 | componentWillUnmount() { 20 | this.alert.addEventListener('webkitTransitionEnd', this.transitionEnd) 21 | } 22 | 23 | transitionEnd() { 24 | this.alert.style.display = 'none' 25 | } 26 | 27 | show(msg) { 28 | this.setState({ 29 | show: true, 30 | msg 31 | }) 32 | 33 | this.alert && (this.alert.style.display = 'block') 34 | 35 | clearTimeout(this.timer) 36 | this.timer = setTimeout(() => { 37 | this.hide() 38 | }, 2000) 39 | } 40 | 41 | hide() { 42 | this.setState({ 43 | show: false 44 | }) 45 | } 46 | 47 | render() { 48 | return
this.alert = alert}>{this.state.msg}
49 | } 50 | } 51 | 52 | export default Alert -------------------------------------------------------------------------------- /src/components/alert/alert.styl: -------------------------------------------------------------------------------- 1 | .alert-wrapper 2 | position: fixed 3 | top: 65% 4 | left: 50% 5 | z-index: 999 6 | transform: translateX(-50%) 7 | overflow hidden 8 | white-space nowrap 9 | text-overflow: ellipsis 10 | text-align: center 11 | font-size: 14px 12 | color: #ffffff 13 | background-color: #cccccc 14 | padding: 8px 15px 15 | display: none 16 | max-width 300px 17 | border-radius: 30px 18 | &.hide 19 | opacity: 0 20 | transition: all 0.3s -------------------------------------------------------------------------------- /src/components/artistList/ArtistList.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import './artistList.styl' 4 | 5 | function ArtistList(props) { 6 | let { list } = props 7 | return ( 8 |
9 | { 10 | list.map((item, index) => ( 11 |
12 |
13 |
14 | {item.name} 15 | { 16 | item.trans && ({item.trans}) 17 | } 18 |
19 |
20 | )) 21 | } 22 |
23 | ) 24 | } 25 | 26 | ArtistList.propTypes = { 27 | list: PropTypes.arrayOf(PropTypes.object).isRequired 28 | } 29 | 30 | export default ArtistList -------------------------------------------------------------------------------- /src/components/artistList/artistList.styl: -------------------------------------------------------------------------------- 1 | .artist-list 2 | .item 3 | padding: 5px 4 | border-bottom: 1px solid #dddddd 5 | display: flex 6 | align-items: center 7 | .icon 8 | width: 20% 9 | padding-top: 20% 10 | min-width 20% 11 | margin-right: 10px 12 | background-repeat: no-repeat 13 | background-size: cover 14 | .info 15 | font-size: 16px 16 | color: #333333 17 | line-height: 1.2 18 | .trans 19 | color: #707070 -------------------------------------------------------------------------------- /src/components/commentList/CommentList.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import './commentList.styl' 4 | 5 | export default function CommentList(props) { 6 | let { title, list } = props 7 | 8 | if (list.length === 0) { 9 | return null 10 | } 11 | 12 | return ( 13 |
14 |
{title}
15 |
16 | { 17 | list.map(item => ( 18 |
19 |
20 | 21 |
{item.userProfile.nickname}
22 |
23 |
{item.content}
24 |
——{item.programName}
25 |
26 | )) 27 | } 28 |
29 |
30 | ) 31 | } 32 | 33 | CommentList.propTypes = { 34 | list: PropTypes.arrayOf(PropTypes.shape({ 35 | programName: PropTypes.string, 36 | content: PropTypes.string, 37 | commentId: PropTypes.string, 38 | userProfile: PropTypes.shape({ 39 | nickname: PropTypes.string, 40 | avatarUrl: PropTypes.string 41 | }) 42 | })).isRequired, 43 | title: PropTypes.string.isRequired 44 | } -------------------------------------------------------------------------------- /src/components/commentList/commentList.styl: -------------------------------------------------------------------------------- 1 | .comment-list-wrapper 2 | .title 3 | color: #333333 4 | font-size: 15px 5 | position relative 6 | padding-left: 10px 7 | &:before 8 | content: '' 9 | position: absolute 10 | left: 0 11 | top: 0 12 | bottom: 0 13 | margin: auto 14 | border-left: 2px solid #c62f2f 15 | height 15px 16 | .item 17 | padding: 15px 10px 18 | border-bottom: 1px solid #eee 19 | &:last-child 20 | border-bottom: none 21 | .user 22 | display: flex 23 | align-items: center 24 | img 25 | width: 35px 26 | height: 35px 27 | border-radius: 50% 28 | .name 29 | font-size: 14px 30 | color: #646867 31 | margin-left: 15px 32 | .content 33 | margin-left: 50px 34 | line-height: 1.3 35 | font-size: 14px 36 | .program-name 37 | margin-left: 50px 38 | color: #646867 39 | font-size: 12px 40 | margin-top: 10px -------------------------------------------------------------------------------- /src/components/commonHeader/CommonHeader.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { withRouter } from 'react-router-dom' 3 | import { connect } from 'react-redux' 4 | import classNames from 'classnames' 5 | import { setSlidebarState } from '../../actions/index' 6 | import './commonHeader.styl' 7 | 8 | function CommonHeader(props) { 9 | let path = props.match.path 10 | 11 | return ( 12 |
13 | props.setSlidebarState(true)}> 14 |
15 | 16 | 17 | 18 |
19 | props.history.push('/search')}> 20 |
21 | ) 22 | } 23 | 24 | export default withRouter(connect(null, { setSlidebarState })(CommonHeader)) -------------------------------------------------------------------------------- /src/components/commonHeader/commonHeader.styl: -------------------------------------------------------------------------------- 1 | .common-header 2 | display: flex 3 | justify-content: space-between 4 | align-items: center 5 | height: 50px 6 | background-color: #c62f2f 7 | padding: 0 15px 8 | .iconfont 9 | color: #ccafaf 10 | .menu, .wangyi 11 | font-size: 18px 12 | .media, .user, .search 13 | font-size: 20px 14 | .media, .wangyi, .user 15 | margin: 0 15px 16 | &.active 17 | color: #eeeeee -------------------------------------------------------------------------------- /src/components/fullPlay/fullPlay.styl: -------------------------------------------------------------------------------- 1 | .full-play-wrapper 2 | position: fixed 3 | top: 0 4 | left 0 5 | right 0 6 | bottom: 0 7 | z-index: 100 8 | background-color rgb(127, 127, 127) 9 | transition: all 0.6s 10 | transform: translateY(100px) 11 | opacity 0 12 | display: none 13 | &.active 14 | transform: translateY(0) 15 | opacity 1 16 | display: block !important 17 | .bg 18 | background-repeat: no-repeat 19 | background-position: center 20 | background-size auto 100% 21 | // filter: blur(35px) // 移动端太卡去掉 22 | opacity 0.6 23 | position: absolute 24 | top: 0 25 | left 0 26 | right 0 27 | bottom: 0 28 | z-index: -1 29 | .play-header 30 | display: flex 31 | align-items: center 32 | height 50px 33 | position relative 34 | padding: 0 10px 0 15px 35 | color: #ffffff 36 | &:after 37 | content: '' 38 | position: absolute 39 | left 0 40 | right 0 41 | bottom: 0 42 | height 1px 43 | background: linear-gradient(90deg, rgba(220, 220, 220, 0), #ffffff, rgba(220, 220, 220, 0)) 44 | transform: scaleY(0.2) 45 | .content 46 | flex: 1 47 | overflow hidden 48 | margin-left: 15px 49 | .name 50 | overflow hidden 51 | white-space nowrap 52 | text-overflow: ellipsis 53 | .artist-name 54 | font-size: 12px 55 | color: #e2e2e2 56 | margin-top: 5px 57 | line-height: 1.2 58 | .shape 59 | margin-left: 10px 60 | font-size: 20px 61 | .visible 62 | z-index: 5 !important 63 | opacity 1 !important 64 | .music-turn-wrapper 65 | position relative 66 | z-index: 1 67 | opacity 0 68 | transform: translateY(-100%) 69 | padding-top: 20% 70 | height calc(100vh - 148px - 50px - 20px) 71 | min-height: 321px 72 | box-sizing: border-box 73 | .music-turn-bg 74 | width: 70% 75 | height 0 76 | padding-top: 70% 77 | margin: 0 auto 78 | background: url(http://s3.music.126.net/m/s/img/disc.png?d3bdd1080a72129346aa0b4b4964b75f) no-repeat 79 | background-size contain 80 | position relative 81 | .music-turn 82 | position: absolute 83 | width: 61% 84 | height: 61% 85 | top: 0 86 | left 0 87 | right 0 88 | bottom: 0 89 | margin: auto 90 | border-radius: 50% 91 | animation: rotate 14s linear infinite 92 | &.stop 93 | animation-play-state: paused; 94 | .dist 95 | background: url(http://s3.music.126.net/m/s/img/needle.png?702cf6d95f29e2e594f53a3caab50e12) no-repeat 96 | background-size contain 97 | position: absolute 98 | top: 0 99 | left 42% 100 | width: 30% 101 | height 0 102 | padding-top: 39% 103 | z-index 1 104 | transition: all 0.3s 105 | transform: rotate(-25deg) 106 | transform-origin: 15px 0 107 | &.active 108 | transform: rotate(0) 109 | .lyric-wrapper 110 | position relative 111 | z-index: 1 112 | opacity 0 113 | font-size: 14px 114 | color: #cecece 115 | text-align: center 116 | margin-top: 20px 117 | height calc(100vh - 148px - 50px - 20px - 20px) 118 | overflow hidden 119 | &:after 120 | content: '' 121 | position: absolute 122 | top: 0 123 | left 0 124 | right 0 125 | bottom: 0 126 | z-index: 10 127 | .placehoder 128 | height: calc((100vh - 148px - 50px - 20px) / 2) 129 | .lyric 130 | padding: 10px 25px 131 | line-height: 1.4 132 | word-break: break-all 133 | &.active 134 | color: #ffffff 135 | 136 | .control-wrapper 137 | position: absolute 138 | left 0 139 | right 0 140 | bottom: 0 141 | padding-bottom: 30px 142 | .other-control 143 | text-align: center 144 | justify-content: space-around 145 | display: flex 146 | padding: 0 50px 147 | color: #e6e6e6 148 | .iconfont 149 | font-size 19px 150 | .progressbar-wrapper 151 | display: flex 152 | padding: 0 20px 153 | margin-top: 25px 154 | color: #e2e2e2 155 | font-size: 12px 156 | align-items: center 157 | .progress 158 | flex: 1 159 | padding: 0 10px 160 | .play-control 161 | color: #e6e6e6 162 | display: flex 163 | align-items: center 164 | justify-content: space-around 165 | padding: 0 15px 166 | margin-top: 25px 167 | .iconfont 168 | font-size 19px 169 | .play-btn 170 | border: 1px solid #e6e6e6 171 | border-radius: 50% 172 | width: 35px 173 | height: 35px 174 | .iconfont 175 | position relative 176 | top: 8.5px 177 | left 8px 178 | .paused 179 | left 9.5px 180 | 181 | @keyframes rotate 182 | from 183 | transform: rotate(0) 184 | to 185 | transform: rotate(360deg) 186 | -------------------------------------------------------------------------------- /src/components/historyList/HistoryList.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import './historyList.styl' 4 | 5 | function HistoryList(props) { 6 | let { searchHistory, onKeywordsClick, onCloseClick } = props 7 | 8 | return ( 9 |
10 | { 11 | searchHistory.map(keywords => ( 12 |
13 | 14 | onKeywordsClick(keywords)}>{keywords} 15 | onCloseClick(keywords)}> 16 |
17 | )) 18 | } 19 |
20 | ) 21 | } 22 | 23 | HistoryList.propTypes = { 24 | onCloseClick: PropTypes.func.isRequired, 25 | onKeywordsClick: PropTypes.func.isRequired, 26 | searchHistory: PropTypes.arrayOf(PropTypes.string).isRequired 27 | } 28 | 29 | export default HistoryList -------------------------------------------------------------------------------- /src/components/historyList/historyList.styl: -------------------------------------------------------------------------------- 1 | .search-history-wrapper 2 | .item 3 | display: flex 4 | align-items: center 5 | padding: 0 10px 6 | border-bottom: 1px solid #dddddd 7 | .iconfont 8 | color: #c5c4c4 9 | .text 10 | font-size: 14px 11 | color: #333333 12 | flex: 1 13 | margin-left: 10px 14 | padding: 10px 0 -------------------------------------------------------------------------------- /src/components/loading/Loading.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import './loading.styl' 4 | 5 | function Loading(props) { 6 | let { complete, show } = props 7 | 8 | return ( 9 |
10 | { 11 | complete ? 12 |

已经到底了

: 13 |

正在加载...

14 | } 15 |
16 | ) 17 | } 18 | 19 | Loading.defaultProps = { 20 | complete: false 21 | } 22 | 23 | Loading.propTypes = { 24 | show: PropTypes.bool.isRequired, 25 | complete: PropTypes.bool 26 | } 27 | 28 | export default Loading -------------------------------------------------------------------------------- /src/components/loading/loading.styl: -------------------------------------------------------------------------------- 1 | .loading-more 2 | text-align: center 3 | .loading 4 | font-size: 14px 5 | color: #888888 6 | height 30px 7 | line-height: 30px 8 | padding-left: 30px 9 | display: inline-block 10 | position relative 11 | &:before 12 | content: '' 13 | background: url('data:image/gif;base64,R0lGODlhKAAoAIABANM6Mf///yH/C05FVFNDQVBFMi4wAwEAAAAh/wtYTVAgRGF0YVhNUDw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMDY3IDc5LjE1Nzc0NywgMjAxNS8wMy8zMC0yMzo0MDo0MiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKFdpbmRvd3MpIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOkI2QURFODRFRkZBRTExRTU4NTg0QTdFMUQ4QkI2MTI1IiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOkI2QURFODRGRkZBRTExRTU4NTg0QTdFMUQ4QkI2MTI1Ij4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6QjZBREU4NENGRkFFMTFFNTg1ODRBN0UxRDhCQjYxMjUiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6QjZBREU4NERGRkFFMTFFNTg1ODRBN0UxRDhCQjYxMjUiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz4B//79/Pv6+fj39vX08/Lx8O/u7ezr6uno5+bl5OPi4eDf3t3c29rZ2NfW1dTT0tHQz87NzMvKycjHxsXEw8LBwL++vby7urm4t7a1tLOysbCvrq2sq6qpqKempaSjoqGgn56dnJuamZiXlpWUk5KRkI+OjYyLiomIh4aFhIOCgYB/fn18e3p5eHd2dXRzcnFwb25tbGtqaWhnZmVkY2JhYF9eXVxbWllYV1ZVVFNSUVBPTk1MS0pJSEdGRURDQkFAPz49PDs6OTg3NjU0MzIxMC8uLSwrKikoJyYlJCMiISAfHh0cGxoZGBcWFRQTEhEQDw4NDAsKCQgHBgUEAwIBAAAh+QQJCgABACwAAAAAKAAoAAACeIyPqcvtD6OctNoD8rUZ7Np9VChKZAmdqKOuTOsqcIzMtGHfuaxxfbRrBGu/UfExXCRxxwRsGdg9m0IqpgmVYq1KbnTrMXmnYeAYzCtf1em2E11lf+VkFpxIP+f37td93dfF9zboVwhIaHfItJjoiBd4IzlJWalQAAAh+QQJCgABACwAAAAAKAAoAAACfIyPqcvtD6OctNqLs94WeB55AAiJ5GOeTaoubJu8nBzQGm0zuYF/0l7zIYAxocvIQzqAvVHROVRGoYemgqm0PpfZLjX53YqnV2nVWw5j1ejxkQ1Pc+Nu8FxeD4bJea2uzRf4hidotwJ4RvenmEg42IfoaFioB2N5iZmZUAAAIfkECQoAAQAsAAAAACgAKAAAAn+Mj6nL7Q+jnLRaCPK1GezafVTIaRIJmhE6qpg7sY98wg692g3+egnNm9mAup6C6EslD8hL8zcsGp4I6sI6jS6PWu42EAR3od8wVlyWor1s8m1chV/l2fQ3bm/j33m3n3F2tvfHREdYp1d4p5iIePgYqBbZB2goKIKZqbnJ+VAAACH5BAUKAAEALAAAAAAoACgAAAJ8jI+py+0PIwRUWkbB3Sjz731bKFpkGZ1mJaktm8KX29CT/Ng57ug9XwPeNC/iSLjwBY1DEFKhxDwTLl/0UH1eDVkmlNf9eqng8thqFquX606aTT6/pfJ6OznH5u/cfQBtF8cnSOgWSDcYBmeYqOWniFiod4hSaXmJmWlRAAA7') no-repeat 14 | display: inline-block 15 | position: absolute 16 | top: 5px 17 | left 0 18 | background-size: 20px 19 | width: 20px 20 | height 20px 21 | .complete 22 | font-size: 14px 23 | color: #888888 24 | height 30px 25 | line-height: 30px 26 | display: inline-block 27 | position relative -------------------------------------------------------------------------------- /src/components/miniPlay/MiniPlay.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import Hammer from 'react-hammerjs' 4 | import './miniPlay.styl' 5 | 6 | export default function MiniPlay(props) { 7 | let { music, togglePlay, paused, borderW, r, percentage, showMusicList, swipe, contentClick } = props 8 | 9 | let borderR = borderW + r 10 | let width = borderR * 2 11 | let perimeter = Math.ceil(2 * Math.PI * r) 12 | 13 | return ( 14 |
15 | 16 |
contentClick()}> 17 | swipe(e)}> 18 |
19 |
{music.name}
20 |
横滑可以切换上下首哦
21 |
22 |
23 |
24 | togglePlay()}> 25 | 26 | 27 | 28 | 29 | 30 | 31 | showMusicList()}> 32 |
33 | ) 34 | } 35 | 36 | MiniPlay.defaultProps = { 37 | borderW: 1, 38 | r: 14, 39 | percentage: 0 40 | } 41 | 42 | MiniPlay.propTypes = { 43 | music: PropTypes.shape({ 44 | name: PropTypes.string, 45 | id: PropTypes.string, 46 | duration: PropTypes.number, 47 | artistName: PropTypes.string, 48 | picUrl: PropTypes.string, 49 | url: PropTypes.string 50 | }), 51 | show: PropTypes.bool, 52 | togglePlay: PropTypes.func, 53 | paused: PropTypes.bool, 54 | percentage: PropTypes.number, 55 | showMusicList: PropTypes.func, 56 | swipe: PropTypes.func, 57 | contentClick: PropTypes.func 58 | } -------------------------------------------------------------------------------- /src/components/miniPlay/miniPlay.styl: -------------------------------------------------------------------------------- 1 | .mini-play-wrapper 2 | position: fixed 3 | left 0 4 | right 0 5 | bottom: 0 6 | height: 50px 7 | display: flex 8 | align-items: center 9 | z-index: 99 10 | background-color #ffffff 11 | padding: 0 15px 0 5px 12 | box-sizing: border-box 13 | box-shadow: 0 0 2px #e6e6e6 14 | .content 15 | flex: 1 16 | margin-left: 5px 17 | overflow hidden 18 | .tip 19 | font-size: 12px 20 | color: #707070 21 | margin-top: 5px 22 | .name 23 | overflow hidden 24 | white-space nowrap 25 | text-overflow: ellipsis 26 | line-height: 1.2 27 | .icon 28 | width: 40px 29 | height: 40px 30 | .list 31 | color: rgb(74, 74, 74) 32 | margin-left: 15px 33 | font-size: 18px 34 | .play-icon 35 | position relative 36 | margin-left: 15px 37 | .iconfont 38 | position absolute 39 | top: 50% 40 | left 50% 41 | transform: translate(-50%, -50%) 42 | margin-left: 2px 43 | &.play 44 | color: #707070 45 | &.pause 46 | color: #c62f2f 47 | margin-left: 0 -------------------------------------------------------------------------------- /src/components/musicList/MusicList.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Motion, spring } from 'react-motion' 4 | import classNames from 'classnames' 5 | import Scroll from '../scroll/Scroll' 6 | import { modeData } from 'utils' 7 | import './musicList.styl' 8 | 9 | export default function MusicList(props) { 10 | let { list, hideMusicList, switchMusic, activeId, show, removeMusic, mode, toggleMode, clearAll } = props 11 | 12 | return ( 13 |
14 | 15 | { 16 | ({ y }) => ( 17 |
18 |
19 | toggleMode()}> 20 | toggleMode()}>{modeData[mode].text} ({list.length}) 21 | 22 | 收藏全部 23 | 24 | clearAll()}> 25 |
26 |
27 | 28 |
29 | { 30 | list.map(item => ( 31 |
32 |
switchMusic(item.id)}> 33 | 34 | {item.name} 35 |  - {item.artistName} 36 |
37 | removeMusic(item)}> 38 |
39 | )) 40 | } 41 |
42 |
43 |
44 |
45 | ) 46 | } 47 |
48 | 49 | { 50 | ({ opacity }) => ( 51 |
hideMusicList()} style={{ opacity, display: opacity === 0 ? 'none' : 'block' }}>
52 | ) 53 | } 54 |
55 |
56 | ) 57 | } 58 | 59 | MusicList.propTypes = { 60 | list: PropTypes.arrayOf(PropTypes.shape({ 61 | name: PropTypes.string, 62 | id: PropTypes.string, 63 | artistName: PropTypes.string 64 | })), 65 | hideMusicList: PropTypes.func, 66 | switchMusic: PropTypes.func, 67 | activeId: PropTypes.string, 68 | show: PropTypes.bool, 69 | removeMusic: PropTypes.func, 70 | mode: PropTypes.string, 71 | toggleMode: PropTypes.func, 72 | clearAll: PropTypes.func 73 | } -------------------------------------------------------------------------------- /src/components/musicList/musicList.styl: -------------------------------------------------------------------------------- 1 | .music-list-wrapper 2 | position: fixed 3 | top: 40% 4 | left 0 5 | right: 0 6 | bottom: 0 7 | background-color #ffffff 8 | z-index: 101 9 | .header 10 | padding: 13px 7px 11 | display: flex 12 | align-items: center 13 | color: #333333 14 | border-bottom: 1px solid #eee 15 | .text 16 | flex: 1 17 | margin-left: 5px 18 | .iconfont 19 | color: #989898 20 | .line 21 | border-left: 1px solid #989898 22 | margin-left: 10px 23 | height 17px 24 | .collect 25 | font-size: 20px 26 | .clear 27 | margin-left: 7px 28 | .collect-text 29 | margin-left: 5px 30 | .music-list 31 | position: absolute 32 | left 0 33 | right 0 34 | bottom: 0 35 | top: 46px 36 | overflow hidden 37 | .item 38 | display: flex 39 | padding: 13px 7px 40 | border-bottom: 1px solid #eee 41 | align-items: center 42 | .content 43 | flex: 1 44 | overflow hidden 45 | white-space nowrap 46 | text-overflow: ellipsis 47 | line-height: 1.2 48 | &.active 49 | color: #c62f2f 50 | .artist-name 51 | color: #c62f2f 52 | .iconfont 53 | color: #c62f2f 54 | margin-right: 7px 55 | display: inline 56 | .artist-name 57 | font-size: 12px 58 | color: #989898 59 | .iconfont 60 | display: none 61 | .close 62 | color: #989898 63 | margin-left: 10px 64 | .music-list-cover 65 | background-color: rgba(120, 120, 120, 0.6) 66 | position: fixed 67 | top: 0 68 | left: 0 69 | right: 0 70 | bottom: 0 71 | z-index: 100 -------------------------------------------------------------------------------- /src/components/mvList/MvList.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { numFormat, timeFormat } from 'utils/index' 4 | import './mvList.styl' 5 | 6 | function MvList(props) { 7 | let { list } = props 8 | 9 | return ( 10 |
11 | { 12 | list.map((item, index) => ( 13 |
14 |
15 | 16 | 17 | {numFormat(item.playCount)} 18 | 19 |
20 |
21 |
{item.name}
22 |
23 | {timeFormat(item.duration)}  24 | {item.artistName} 25 |
26 |
27 |
28 | )) 29 | } 30 |
31 | ) 32 | } 33 | 34 | MvList.propTypes = { 35 | list: PropTypes.arrayOf(PropTypes.object).isRequired 36 | } 37 | 38 | export default MvList -------------------------------------------------------------------------------- /src/components/mvList/mvList.styl: -------------------------------------------------------------------------------- 1 | .mv-list 2 | .item 3 | padding: 5px 4 | border-bottom: 1px solid #dddddd 5 | display: flex 6 | align-items: center 7 | .icon 8 | width: 30% 9 | padding-top: 19% 10 | background-repeat: no-repeat 11 | background-size: cover 12 | margin-right: 10px 13 | position relative 14 | .play-count 15 | position: absolute 16 | top: 4px 17 | right: 4px 18 | font-size: 12px 19 | color: #d0d0d0 20 | .iconfont 21 | font-size: 14px 22 | color: #d0d0d0 23 | margin-right: 2px 24 | .content 25 | flex: 1 26 | .name 27 | font-size: 16px 28 | line-height: 1.2 29 | margin-bottom: 10px 30 | word-break: break-all 31 | overflow hidden 32 | display: -webkit-box 33 | -webkit-line-clamp: 2 34 | color: #333333 35 | .info 36 | font-size: 12px 37 | .duration 38 | color: #707070 39 | .artist-name 40 | color: #507daf -------------------------------------------------------------------------------- /src/components/playList/PlayList.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { numFormat } from 'utils/index' 4 | import './playList.styl' 5 | 6 | function PlayList(props) { 7 | let { list, onContentClick } = props 8 | return ( 9 |
10 | { 11 | list.map((item, index) => ( 12 |
onContentClick(index)}> 13 |
14 |
15 |
{item.name}
16 |
17 | {item.trackCount}首 18 | by {item.creator.nickname} 19 | , 播放{numFormat(item.playCount)}次 20 |
21 |
22 |
23 | )) 24 | } 25 |
26 | ) 27 | } 28 | 29 | PlayList.propTypes = { 30 | list: PropTypes.arrayOf(PropTypes.object).isRequired, 31 | onContentClick: PropTypes.func.isRequired 32 | } 33 | 34 | export default PlayList -------------------------------------------------------------------------------- /src/components/playList/playList.styl: -------------------------------------------------------------------------------- 1 | .play-list 2 | .item 3 | padding: 5px 4 | border-bottom: 1px solid #dddddd 5 | display: flex 6 | align-items: center 7 | .icon 8 | width: 20% 9 | padding-top: 20% 10 | min-width: 20% 11 | margin-right: 10px 12 | background-repeat: no-repeat 13 | background-size: cover 14 | .info 15 | font-size: 16px 16 | color: #333333 17 | line-height: 1.2 18 | overflow hidden 19 | .name 20 | color: #333333 21 | overflow hidden 22 | white-space nowrap 23 | text-overflow: ellipsis 24 | .other 25 | font-size: 12px 26 | margin-top: 8px 27 | color: #707070 28 | .nickname 29 | margin-left: 5px -------------------------------------------------------------------------------- /src/components/popup/Popup.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import classNames from 'classnames' 4 | import './popup.styl' 5 | 6 | class Popup extends React.Component { 7 | constructor(props) { 8 | super(props) 9 | this.state = { 10 | show: false 11 | } 12 | } 13 | 14 | show() { 15 | this.setState({ 16 | show: true 17 | }) 18 | } 19 | 20 | hide() { 21 | this.setState({ 22 | show: false 23 | }) 24 | } 25 | 26 | confirmFunc() { 27 | this.hide() 28 | this.props.confirmFunc && this.props.confirmFunc() 29 | } 30 | 31 | cancelFunc() { 32 | this.hide() 33 | this.props.cancelFunc && this.props.cancelFunc() 34 | } 35 | 36 | render() { 37 | let { show } = this.state 38 | let { title, message, cancelText, confirmText } = this.props 39 | 40 | return ( 41 |
42 |
43 |
44 | { 45 | title &&

{title}

46 | } 47 | { 48 | message &&
{message}
49 | } 50 |
51 |
52 | this.cancelFunc()}>{cancelText} 53 | this.confirmFunc()}>{confirmText} 54 |
55 |
56 |
57 |
58 | ) 59 | } 60 | } 61 | 62 | Popup.defaultProps = { 63 | cancelText: '取消', 64 | confirmText: '确定' 65 | } 66 | 67 | Popup.propTypes = { 68 | title: PropTypes.string, 69 | message: PropTypes.string, 70 | cancelText: PropTypes.string, 71 | confirmText: PropTypes.string, 72 | cancelFunc: PropTypes.func, 73 | confirmFunc: PropTypes.func 74 | } 75 | 76 | export default Popup -------------------------------------------------------------------------------- /src/components/popup/popup.styl: -------------------------------------------------------------------------------- 1 | .popup-wrapper 2 | position: fixed 3 | top: 0 4 | left: 0 5 | right 0 6 | bottom: 0 7 | z-index: 998 8 | align-items: center 9 | display: none 10 | &.active 11 | display: flex 12 | .popup 13 | position: relative 14 | width: 75% 15 | max-width: 300px 16 | margin: 0 auto 17 | z-index: 1 18 | background-color: #ffffff 19 | display: none 20 | border-radius: 10px 21 | &.active 22 | animation scale 0.4s 23 | display: block 24 | .content 25 | text-align: center 26 | padding: 15px 27 | line-height: 30px 28 | .title 29 | color: #333333 30 | .message 31 | color: #848484 32 | font-size: 14px 33 | .footer 34 | display: flex 35 | border-top: 1px solid #e2e2e2 36 | .btn 37 | flex: 1 38 | font-size: 14px 39 | color: #525252 40 | text-align: center 41 | padding: 10px 0 42 | box-sizing: border-box 43 | .btn + .btn 44 | border-left: 1px solid #e2e2e2 45 | .cover 46 | position: absolute 47 | background-color: rgba(120, 120, 120, 0.6) 48 | top: 0 49 | left: 0 50 | right: 0 51 | bottom: 0 52 | display: none 53 | opacity: 0 54 | &.active 55 | display: block 56 | opacity: 1 57 | transition: all 0.4s 58 | 59 | @keyframes scale 60 | 0% 61 | transform: scale(0) 62 | 60% 63 | transform: scale(1.1) 64 | 100% 65 | transform: scale(1) -------------------------------------------------------------------------------- /src/components/programList/ProgramList.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { numFormat, timeFormat, dateFormat } from 'utils/index' 4 | import './programList.styl' 5 | 6 | export default function ProgramList(props) { 7 | let { list } = props 8 | return ( 9 |
10 | { 11 | list.map(item => ( 12 |
13 | {item.serialNum} 14 |
15 |
{item.name}
16 |
17 | {dateFormat('MM-dd', new Date(item.createTime))} 18 | 19 | 20 | {numFormat(item.listenerCount)} 21 | 22 | 23 | 24 | {timeFormat(item.duration)} 25 | 26 |
27 |
28 | 29 |
30 | )) 31 | } 32 |
33 | ) 34 | } 35 | 36 | ProgramList.propTypes = { 37 | list: PropTypes.arrayOf(PropTypes.shape({ 38 | name: PropTypes.string, 39 | id: PropTypes.string, 40 | duration: PropTypes.number, 41 | listenerCount: PropTypes.number, 42 | serialNum: PropTypes.number, 43 | createTime: PropTypes.number 44 | })).isRequired 45 | } -------------------------------------------------------------------------------- /src/components/programList/programList.styl: -------------------------------------------------------------------------------- 1 | .program-list-wrapper 2 | .item 3 | padding: 10px 4 | display: flex 5 | align-items: center 6 | border-bottom: 1px solid #eee 7 | .content 8 | flex: 1 9 | margin-left: 10px 10 | overflow hidden 11 | .name 12 | color: #333333 13 | overflow hidden 14 | white-space nowrap 15 | text-overflow: ellipsis 16 | .info 17 | color: rgb(146, 150, 149) 18 | font-size: 12px 19 | margin-top: 5px 20 | .iconfont 21 | font-size: 12px 22 | margin-left: 20px 23 | .serial-num 24 | font-size: 14px 25 | color: rgb(146, 150, 149) 26 | .more 27 | color: rgb(146, 150, 149) 28 | margin-left: 10px -------------------------------------------------------------------------------- /src/components/progressBar/ProgressBar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import './progressBar.styl' 4 | 5 | const pointW = 14 6 | 7 | export default class ProgressBar extends React.Component { 8 | constructor(props) { 9 | super(props) 10 | this.state = { 11 | percentage: this.props.percentage || 0 12 | } 13 | 14 | this.refresh = this.refresh.bind(this) 15 | } 16 | 17 | componentDidMount() { 18 | this.initEvents() 19 | } 20 | 21 | componentWillReceiveProps(nextProps) { 22 | if (!this.isTouch && nextProps.percentage !== this.props.percentage) { 23 | this.setState({ 24 | percentage: nextProps.percentage 25 | }) 26 | } 27 | } 28 | 29 | componentWillUnmount() { 30 | this.destroyEvents() 31 | } 32 | 33 | initEvents() { 34 | window.addEventListener('resize', this.refresh) 35 | } 36 | 37 | destroyEvents() { 38 | window.removeEventListener('resize', this.refresh) 39 | } 40 | 41 | progressBarClick(e) { 42 | let pageX = e.pageX 43 | 44 | let { left, width } = this.progressBar.getBoundingClientRect() 45 | let offsetX = pageX - left - pointW / 2 46 | let progressW = width - pointW 47 | let percentage = Math.max(0, Math.min(1, offsetX / progressW)) 48 | 49 | this.setState({ 50 | percentage 51 | }) 52 | 53 | this.props.percentageChangeFunc(percentage) 54 | } 55 | 56 | touchStart(e) { 57 | let startX = e.changedTouches[0].pageX 58 | 59 | this.isTouch = true 60 | this.startX = startX 61 | this.startPercentage = this.state.percentage 62 | } 63 | 64 | touchMove(e) { 65 | if (!this.isTouch) { 66 | return 67 | } 68 | 69 | let moveX = e.changedTouches[0].pageX 70 | let diffX = moveX - this.startX 71 | let width = this.progressBar.getBoundingClientRect().width - pointW 72 | let offsetX = Math.min(Math.max(width * this.startPercentage + diffX, 0), width) 73 | 74 | this.setState({ 75 | percentage: offsetX / width 76 | }) 77 | } 78 | 79 | touchEnd() { 80 | this.isTouch = false 81 | this.props.percentageChangeFunc(this.state.percentage) 82 | } 83 | 84 | refresh() { 85 | this.forceUpdate() 86 | } 87 | 88 | render() { 89 | let { percentage } = this.state 90 | let offsetX = 0 91 | if (this.progressBar) { 92 | offsetX = (this.progressBar.getBoundingClientRect().width - pointW) * percentage 93 | } 94 | 95 | return ( 96 |
this.progressBarClick(e)} ref={progressBar => this.progressBar = progressBar}> 97 |
98 | this.touchStart(e)} 102 | onTouchMove={(e) => this.touchMove(e)} 103 | onTouchEnd={(e) => this.touchEnd(e)} 104 | /> 105 |
106 | ) 107 | } 108 | } 109 | 110 | ProgressBar.propTypes = { 111 | percentage: PropTypes.number, 112 | percentageChangeFunc: PropTypes.func 113 | } -------------------------------------------------------------------------------- /src/components/progressBar/progressBar.styl: -------------------------------------------------------------------------------- 1 | .progress-bar-wrapper 2 | position relative 3 | width: 100% 4 | background-color #e2e2e2 5 | height: 2px 6 | border-radius: 1px 7 | .percentage 8 | width: 0 9 | background-color: rgb(210, 59, 50) 10 | height 100% 11 | .point 12 | position: absolute 13 | top: 50% 14 | left 0 15 | width: 14px 16 | height: 14px 17 | border-radius: 50% 18 | background-color #ffffff 19 | &:after 20 | position: absolute 21 | content: '' 22 | width: 3px 23 | height 3px 24 | border-radius: 50% 25 | background-color: rgb(210, 59, 50) 26 | top: 50% 27 | left 50% 28 | transform translate(-50%, -50%) -------------------------------------------------------------------------------- /src/components/radioList/RadioList.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import './radioList.styl' 4 | 5 | function RadioList(props) { 6 | let { list, onContentClick} = props 7 | 8 | return ( 9 |
10 | { 11 | list.map((item, index) => ( 12 |
onContentClick(index)}> 13 |
14 |
15 |
{item.name}
16 |
{item.dj.nickname}
17 |
18 |
19 | )) 20 | } 21 |
22 | ) 23 | } 24 | 25 | RadioList.propTypes = { 26 | list: PropTypes.arrayOf(PropTypes.object).isRequired, 27 | onContentClick: PropTypes.func.isRequired 28 | } 29 | 30 | export default RadioList -------------------------------------------------------------------------------- /src/components/radioList/radioList.styl: -------------------------------------------------------------------------------- 1 | .radio-list 2 | .item 3 | padding: 5px 4 | border-bottom: 1px solid #dddddd 5 | display: flex 6 | align-items: center 7 | .icon 8 | width: 20% 9 | padding-top: 20% 10 | min-width: 20% 11 | margin-right: 10px 12 | background-repeat: no-repeat 13 | background-size: cover 14 | .info 15 | font-size: 16px 16 | color: #333333 17 | line-height: 1.2 18 | overflow hidden 19 | .name 20 | color: #333333 21 | overflow hidden 22 | white-space nowrap 23 | text-overflow: ellipsis 24 | .other 25 | font-size: 12px 26 | margin-top: 8px 27 | color: #707070 -------------------------------------------------------------------------------- /src/components/radiothumbnailList/RadiothumbnailList.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import './radiothumbnailList.styl' 4 | 5 | export default function RadioThumbnailList(props) { 6 | let { title, list, showNum, onItemClick } = props 7 | 8 | return ( 9 |
10 |

11 | {title} 12 | 13 |

14 |
15 | { 16 | list.map((item, index) => { 17 | if (index + 1 > showNum) { 18 | return null 19 | } 20 | 21 | return ( 22 |
onItemClick(item.id)}> 23 |
24 | {item.name} 25 |
26 |
{item.rcmdtext}
27 |
28 | ) 29 | }) 30 | } 31 |
32 |
33 | ) 34 | } 35 | 36 | RadioThumbnailList.defaultProps = { 37 | showNum: 6 38 | } 39 | 40 | RadioThumbnailList.propTypes = { 41 | title: PropTypes.string.isRequired, 42 | list: PropTypes.arrayOf(PropTypes.shape({ 43 | name: PropTypes.string, 44 | rcmdtext: PropTypes.string, 45 | picUrl: PropTypes.string, 46 | id: PropTypes.oneOfType([ 47 | PropTypes.string, 48 | PropTypes.number 49 | ]), 50 | })).isRequired, 51 | showNum: PropTypes.number, 52 | onItemClick: PropTypes.func 53 | } -------------------------------------------------------------------------------- /src/components/radiothumbnailList/radiothumbnailList.styl: -------------------------------------------------------------------------------- 1 | .radio-thumbnail-list-wrapper 2 | .title 3 | font-size: 16px 4 | color: #333333 5 | padding: 15px 10px 6 | position relative 7 | &:before 8 | content: '' 9 | position: absolute 10 | left: 0 11 | top: 50% 12 | transform: translateY(-50%) 13 | border-left: 2px solid #c62f2f 14 | height: 15px 15 | .thumbnail-list 16 | display: flex 17 | flex-wrap: wrap 18 | .item:nth-child(3n + 1) 19 | margin-left: 0 20 | .item 21 | flex: 0 0 calc(100% / 3 - 2px) 22 | margin-left: 3px 23 | position relative 24 | .img 25 | padding-top: 100% 26 | background-size: cover 27 | background-position center center 28 | margin-bottom: 5px 29 | position relative 30 | .name 31 | position: absolute 32 | left: 5px 33 | bottom: 7px 34 | color: #ffffff 35 | font-size: 12px 36 | overflow hidden 37 | white-space nowrap 38 | text-overflow: ellipsis 39 | width: 100% 40 | .text 41 | font-size: 14px 42 | color: #333333 43 | display: -webkit-box 44 | text-overflow: ellipsis 45 | overflow: hidden 46 | -webkit-line-clamp: 2 47 | line-height: normal 48 | padding: 0 5px 49 | margin-bottom: 10px -------------------------------------------------------------------------------- /src/components/scroll/Scroll.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import BScroll from 'better-scroll' 4 | import { debounce } from 'utils' 5 | import './scroll.styl' 6 | 7 | export default class Scroll extends React.Component { 8 | constructor(props) { 9 | super(props) 10 | this.refresh = this.refresh.bind(this) 11 | } 12 | 13 | componentDidMount() { 14 | this.initScroll() 15 | 16 | this.resizeRefresh = debounce(this.refresh) 17 | window.addEventListener('resize', this.resizeRefresh) 18 | } 19 | 20 | componentDidUpdate() { 21 | this.refresh() 22 | } 23 | 24 | componentWillUnmount() { 25 | this.scroll.destroy() 26 | window.removeEventListener('resize', this.resizeRefresh) 27 | } 28 | 29 | initScroll() { 30 | let { probeType, click, scrollX, listenScroll } = this.props 31 | 32 | this.scroll = new BScroll(this.wrapper, { 33 | probeType, 34 | click, 35 | scrollX, 36 | listenScroll 37 | }) 38 | 39 | // 是否派发滚动到底部事件,用于上拉加载 40 | if (this.props.pullupFunc) { 41 | this.scroll.on('scrollEnd', () => { 42 | // 滚动到底部 43 | if (this.scroll.y <= this.scroll.maxScrollY + 50) { 44 | this.props.pullupFunc() 45 | } 46 | }) 47 | } 48 | 49 | // 实时派发滚动坐标 50 | if (this.props.scrollFunc) { 51 | this.scroll.on('scroll', e => { 52 | this.props.scrollFunc(e) 53 | }) 54 | 55 | this.scroll.on('scrollEnd', e => { 56 | this.props.scrollFunc(e) 57 | }) 58 | } 59 | } 60 | 61 | refresh() { 62 | this.scroll && this.scroll.refresh() 63 | } 64 | 65 | scrollTo(x, y, time, easing) { 66 | this.scroll && this.scroll.scrollTo(x, y, time, easing) 67 | } 68 | 69 | getMaxScrollY() { 70 | return (this.scroll && this.scroll.maxScrollY) || 0 71 | } 72 | 73 | render() { 74 | return ( 75 |
this.wrapper = wrapper} className="scroll"> 76 | {this.props.children} 77 |
78 | ) 79 | } 80 | } 81 | 82 | Scroll.defaultProps = { 83 | probeType: 1, 84 | click: true, 85 | scrollX: false, 86 | listenScroll: false 87 | } 88 | 89 | Scroll.propTypes = { 90 | probeType: PropTypes.number, 91 | click: PropTypes.bool, 92 | scrollX: PropTypes.bool, 93 | listenScroll: PropTypes.bool, 94 | pullupFunc: PropTypes.func, 95 | scrollFunc: PropTypes.func 96 | } 97 | -------------------------------------------------------------------------------- /src/components/scroll/scroll.styl: -------------------------------------------------------------------------------- 1 | .scroll 2 | height 100% 3 | overflow hidden -------------------------------------------------------------------------------- /src/components/slidebar/Slidebar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | import { Motion, spring } from 'react-motion' 4 | import { setSlidebarState } from '../../actions/index' 5 | import Scroll from '../scroll/Scroll' 6 | import './slidebar.styl' 7 | 8 | class Slidebar extends React.Component { 9 | render() { 10 | let { slidebarState, setSlidebarState } = this.props 11 | 12 | return ( 13 |
14 | 15 | { 16 | ({ x }) => ( 17 |
18 | 19 |
20 |
21 |
登录网易云音乐
22 |
320K高音质无限下载,手机电脑多端同步
23 |
24 | 立即登录 25 |
26 |
27 |
28 |
29 | 30 | 我的消息 31 |
32 |
33 | 34 | VIP会员 35 |
36 |
37 | 38 | 商城 39 |
40 |
41 |
42 |
43 | 44 | 我的好友 45 |
46 |
47 | 48 | 附近的人 49 |
50 |
51 |
52 |
53 | 54 | 个性换肤 55 |
56 |
57 | 58 | 定时停止播放 59 |
60 |
61 | 62 | 扫一扫 63 |
64 |
65 | 66 | 音乐闹钟 67 |
68 |
69 |
70 |
71 |
72 |
73 | 74 | 夜间模式 75 |
76 |
77 | 78 | 设置 79 |
80 |
81 | 82 | 退出 83 |
84 |
85 |
86 | ) 87 | } 88 |
89 | 90 | 91 | { 92 | ({ opacity }) => ( 93 |
setSlidebarState(false)} style={{ opacity, display: opacity === 0 ? 'none' : 'block' }}>
94 | ) 95 | } 96 |
97 |
98 | ) 99 | } 100 | } 101 | 102 | const mapStateToProps = (state) => ({ 103 | slidebarState: state.slidebarState 104 | }) 105 | 106 | export default connect(mapStateToProps, { setSlidebarState })(Slidebar) -------------------------------------------------------------------------------- /src/components/slidebar/slidebar.styl: -------------------------------------------------------------------------------- 1 | .slidebar-wrapper 2 | .content 3 | background-color: rgb(240, 244, 243) 4 | position: fixed 5 | top: 0 6 | left: 0 7 | bottom: 38px 8 | width: 80% 9 | max-width: 300px 10 | z-index: 105 11 | .header 12 | padding: 40px 0 30px 13 | text-align: center 14 | background-color: rgb(31, 32, 33) 15 | .text 16 | font-size: 13px 17 | color: #a2a4a7 18 | line-height: normal 19 | .login-btn 20 | font-size: 15px 21 | color: #ffffff 22 | padding: 6px 35px 23 | border: 1px solid #a2a4a7 24 | border-radius: 29px 25 | display: inline-block 26 | margin-top: 20px 27 | .menu 28 | background-color: #ffffff 29 | margin-bottom: 10px 30 | .item 31 | padding: 10px 32 | display: flex 33 | align-items: center 34 | .iconfont 35 | font-size: 18px 36 | color: #aaa 37 | .text 38 | font-size: 14px 39 | color: #333333 40 | margin-left: 8px 41 | .footer 42 | display: flex 43 | justify-content: space-between 44 | background-color: #ffffff 45 | position: fixed 46 | left: 0 47 | bottom: -38px 48 | right: 0 49 | .item 50 | padding: 10px 51 | display: flex 52 | align-items: center 53 | .iconfont 54 | font-size: 18px 55 | color: #aaa 56 | .text 57 | font-size: 14px 58 | color: #333333 59 | margin-left: 8px 60 | .cover 61 | background-color: rgba(120, 120, 120, 0.6) 62 | position: fixed 63 | top: 0 64 | left: 0 65 | right: 0 66 | bottom: 0 67 | z-index: 104 -------------------------------------------------------------------------------- /src/components/songList/SongList.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import './songList.styl' 4 | 5 | function SongList(props) { 6 | let { list, showRank, onContentClick } = props 7 | 8 | return ( 9 |
10 | { 11 | list.map((item, index) => ( 12 |
13 | { 14 | showRank && {index + 1} 15 | } 16 |
onContentClick(index)}> 17 |
{item.name}
18 |
19 | SQ 20 | {item.artists[0].name} 21 |  - {item.name} 22 |
23 |
24 | 25 | 26 |
27 | )) 28 | } 29 |
30 | ) 31 | } 32 | 33 | SongList.propTypes = { 34 | list: PropTypes.arrayOf(PropTypes.object).isRequired, 35 | showRank: PropTypes.bool, 36 | onContentClick: PropTypes.func.isRequired 37 | } 38 | 39 | export default SongList -------------------------------------------------------------------------------- /src/components/songList/songList.styl: -------------------------------------------------------------------------------- 1 | .song-list 2 | .item 3 | display: flex 4 | align-items: center 5 | padding: 10px 6 | border-bottom: 1px solid #dddddd 7 | .rank 8 | font-size: 16px 9 | color: rgb(141, 144, 143) 10 | padding: 0 20px 0 10px 11 | .song 12 | flex: 1 13 | overflow hidden 14 | .name 15 | font-size: 16px 16 | color: #333333 17 | line-height: 1.4 18 | overflow: hidden 19 | white-space: nowrap 20 | text-overflow: ellipsis 21 | .info 22 | font-size: 12px 23 | overflow hidden 24 | white-space nowrap 25 | text-overflow: ellipsis 26 | .quality 27 | display: inline-block 28 | color: red 29 | border: 1px solid red 30 | padding: 3px 31 | transform: scale(0.65, 0.52) 32 | border-radius: 4px 33 | position: relative 34 | top: 1px 35 | .singer 36 | color: #507daf 37 | .small-name 38 | color: #707070 39 | .iconfont 40 | color: #aaa 41 | margin-left: 15px 42 | .play 43 | font-size: 18px 44 | -------------------------------------------------------------------------------- /src/components/swipe/Swipe.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactSwipe from 'react-swipe' 3 | import PropTypes from 'prop-types' 4 | import classNames from 'classnames' 5 | import './swipe.styl' 6 | 7 | export default class Swipe extends React.Component { 8 | constructor(props) { 9 | super(props) 10 | this.state = { 11 | currentIndex: this.props.startSlide 12 | } 13 | } 14 | 15 | next() { 16 | this.reactSwipe.next() 17 | return this 18 | } 19 | 20 | prev() { 21 | this.reactSwipe.prev() 22 | return this 23 | } 24 | 25 | getPos() { 26 | return this.reactSwipe.getPos() 27 | } 28 | 29 | slide(index) { 30 | this.reactSwipe.slide(index) 31 | return this 32 | } 33 | 34 | callback(index, el) { 35 | this.setState({ 36 | currentIndex: index 37 | }) 38 | 39 | this.props.callback(index, el) 40 | } 41 | 42 | render() { 43 | if (!this.props.children || !this.props.children.length) { 44 | return null 45 | } 46 | 47 | let { currentIndex } = this.state 48 | 49 | return ( 50 |
51 | this.reactSwipe = reactSwipe} swipeOptions={{ ...this.props, callback: (...args) => this.callback(...args) }}> 52 | {this.props.children} 53 | 54 | { 55 | this.props.showDots && ( 56 |
57 | { 58 | new Array(this.props.children.length).fill(null).map((item, index) => ( 59 | 60 | )) 61 | } 62 |
63 | ) 64 | } 65 |
66 | ) 67 | } 68 | } 69 | 70 | Swipe.defaultProps = { 71 | startSlide: 0, 72 | auto: 3000, 73 | speed: 300, 74 | disableScroll: false, 75 | continuous: true, // 是否无缝切换 76 | showDots: true, 77 | callback() {}, 78 | transitionEnd() {} 79 | } 80 | 81 | Swipe.propTypes = { 82 | startSlide: PropTypes.number, 83 | auto: PropTypes.number, 84 | speed: PropTypes.number, 85 | disableScroll: PropTypes.bool, 86 | continuous: PropTypes.bool, 87 | callback: PropTypes.func, 88 | transitionEnd: PropTypes.func, 89 | showDots: PropTypes.bool 90 | } -------------------------------------------------------------------------------- /src/components/swipe/swipe.styl: -------------------------------------------------------------------------------- 1 | .carousel-wrapper 2 | position relative 3 | .dots 4 | position: absolute 5 | bottom: 10px 6 | text-align: center 7 | width: 100% 8 | .dot 9 | display: inline-block 10 | width: 8px 11 | height: 8px 12 | border-radius: 50% 13 | background-color: #aaaaaa 14 | margin: 0 5px 15 | &.active 16 | background-color: #c62f2f -------------------------------------------------------------------------------- /src/components/tabMenu/TabMenu.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import classNames from 'classnames' 4 | import './tabMenu.styl' 5 | 6 | export default class TabMenu extends React.Component { 7 | tabSwitch(index) { 8 | this.setState({ 9 | currentIndex: index 10 | }) 11 | this.props.onTabClick(index) 12 | } 13 | 14 | render() { 15 | let { tabs, currentIndex } = this.props 16 | 17 | return ( 18 |
19 | { 20 | tabs.map((tab, index) => ( 21 | this.tabSwitch(index)} 25 | > 26 | {tab.text} 27 | 28 | )) 29 | } 30 |
31 |
32 | ) 33 | } 34 | } 35 | 36 | TabMenu.propTypes = { 37 | tabs: PropTypes.arrayOf(PropTypes.shape({ 38 | text: PropTypes.string.isRequired 39 | })).isRequired, 40 | currentIndex: PropTypes.number.isRequired, 41 | onTabClick: PropTypes.func.isRequired 42 | } -------------------------------------------------------------------------------- /src/components/tabMenu/tabMenu.styl: -------------------------------------------------------------------------------- 1 | .tab-wrapper 2 | display: flex 3 | height: 30px 4 | align-items: center 5 | position: relative 6 | overflow: scroll 7 | &::-webkit-scrollbar 8 | width: 0 9 | height: 0 10 | .tab 11 | font-size: 14px 12 | color: #2c3e50 13 | text-align: center 14 | min-width: 75px 15 | &.active 16 | color: #c62f2f 17 | .underline 18 | position: absolute 19 | bottom: 0 20 | left: 0 21 | transition: all 0.2s 22 | min-width: 75px 23 | &:before 24 | content: '' 25 | display: block 26 | width: 60% 27 | border: 1px solid #c62f2f 28 | margin: 0 auto -------------------------------------------------------------------------------- /src/components/thumbnailList/ThumbnailList.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { numFormat } from 'utils/index' 4 | import './thumbnailList.styl' 5 | 6 | export default function ThumbnailList(props) { 7 | let { title, list, showNum } = props 8 | 9 | return ( 10 |
11 |

12 | {title} 13 | 14 |

15 |
16 | { 17 | list.map((item, index) => { 18 | if (index + 1 > showNum) { 19 | return null 20 | } 21 | 22 | return ( 23 |
props.onClick(index)}> 24 |
25 | 26 | 27 | {numFormat(item.playCount)} 28 | 29 |
{item.name}
30 |
31 | ) 32 | }) 33 | } 34 |
35 |
36 | ) 37 | } 38 | 39 | ThumbnailList.defaultProps = { 40 | showNum: 6 41 | } 42 | 43 | ThumbnailList.propTypes = { 44 | title: PropTypes.string.isRequired, 45 | list: PropTypes.arrayOf(PropTypes.shape({ 46 | name: PropTypes.string, 47 | playCount: PropTypes.number, 48 | picUrl: PropTypes.string, 49 | id: PropTypes.oneOfType([ 50 | PropTypes.string, 51 | PropTypes.number 52 | ]), 53 | })).isRequired, 54 | showNum: PropTypes.number, 55 | onClick: PropTypes.func.isRequired 56 | } -------------------------------------------------------------------------------- /src/components/thumbnailList/thumbnailList.styl: -------------------------------------------------------------------------------- 1 | .thumbnail-list-wrapper 2 | .title 3 | font-size: 16px 4 | color: #333333 5 | padding: 15px 10px 6 | position relative 7 | &:before 8 | content: '' 9 | position: absolute 10 | left: 0 11 | top: 50% 12 | transform: translateY(-50%) 13 | border-left: 2px solid #c62f2f 14 | height: 15px 15 | .thumbnail-list 16 | display: flex 17 | flex-wrap: wrap 18 | .item:nth-child(3n + 1) 19 | margin-left: 0 20 | .item 21 | flex: 0 0 calc(100% / 3 - 2px) 22 | margin-left: 3px 23 | position relative 24 | .img 25 | padding-top: 100% 26 | background-size: cover 27 | background-position center center 28 | margin-bottom: 5px 29 | .count 30 | position: absolute 31 | right: 5px 32 | top: 3px 33 | font-size: 12px 34 | color: #ffffff 35 | .icon-erji 36 | font-size: 14px 37 | .name 38 | font-size: 14px 39 | color: #333333 40 | display: -webkit-box 41 | text-overflow: ellipsis 42 | overflow: hidden 43 | -webkit-line-clamp: 2 44 | line-height: normal 45 | padding: 0 5px 46 | margin-bottom: 10px 47 | word-break: break-all -------------------------------------------------------------------------------- /src/containers/Found.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | import Scroll from '../components/scroll/Scroll' 4 | import Swipe from '../components/swipe/Swipe' 5 | import ThumbnailList from '../components/thumbnailList/ThumbnailList' 6 | import { getHomeData } from '../actions/home' 7 | import { refreshScroll } from 'utils' 8 | import './found.styl' 9 | 10 | class Found extends React.Component { 11 | componentDidMount() { 12 | this.props.getHomeData() 13 | } 14 | 15 | componentDidUpdate(prevProps) { 16 | refreshScroll(this.scroll, this.props.showPlay, prevProps.showPlay) 17 | } 18 | 19 | onItemClick(index) { 20 | let id = this.props.personalized[index].id 21 | 22 | this.props.history.push(`/playlistDetail/${id}`) 23 | } 24 | 25 | render() { 26 | let { banners, personalized } = this.props 27 | 28 | return ( 29 | this.scroll = scroll}> 30 |
31 | 32 | { 33 | banners.map((banner, index) => ( 34 |
35 |
36 | {banner.typeTitle} 37 |
38 | )) 39 | } 40 |
41 |
42 |
43 | 44 | 45 | 46 |
私人FM
47 |
48 |
49 | 50 | 51 | 52 |
开启每日推荐
53 |
54 |
55 | 56 | 57 | 58 |
歌单
59 |
60 |
61 | 62 | 63 | 64 |
排行榜
65 |
66 |
67 | { 68 | this.onItemClick(index)} title="推荐歌单" list={personalized} showNum={9} /> 69 | } 70 |
71 |
72 | ) 73 | } 74 | } 75 | 76 | const mapStateToProps = state => ({ 77 | banners: state.homeData.banners, 78 | personalized: state.homeData.personalized, 79 | showPlay: state.music.showPlay 80 | }) 81 | 82 | const mapDispatchToProps = { 83 | getHomeData 84 | } 85 | 86 | export default connect(mapStateToProps, mapDispatchToProps)(Found) -------------------------------------------------------------------------------- /src/containers/Home.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | import CommonHeader from '../components/commonHeader/CommonHeader' 4 | import TabMenu from '../components/tabMenu/TabMenu' 5 | import { refreshScroll } from 'utils' 6 | import './home.styl' 7 | 8 | class Home extends React.Component { 9 | constructor(props) { 10 | super(props) 11 | this.state = { 12 | tabs: [ 13 | { path: '/home', text: '发现' }, 14 | { path: '/home/mime', text: '我的' }, 15 | { path: '/home/radio', text: '电台' } 16 | ] 17 | } 18 | } 19 | 20 | componentDidUpdate(prevProps) { 21 | refreshScroll(this.scrollWrapper, null, this.props.showPlay, prevProps.showPlay) 22 | } 23 | 24 | onTabClick(index) { 25 | let path = this.state.tabs[index].path 26 | this.props.history.push(path) 27 | } 28 | 29 | currentIndex() { 30 | let { pathname } = this.props.location 31 | let index = this.state.tabs.findIndex(tab => pathname === tab.path) 32 | return index 33 | } 34 | 35 | render() { 36 | return ( 37 |
38 | 39 | this.onTabClick(index)} /> 40 |
41 | {/* 添加嵌套路由 */} 42 | {this.props.children} 43 |
44 |
45 | ) 46 | } 47 | } 48 | 49 | const mapStateToProps = state => ({ 50 | showPlay: state.music.showPlay 51 | }) 52 | 53 | export default connect(mapStateToProps)(Home) -------------------------------------------------------------------------------- /src/containers/Mime.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Scroll from '../components/scroll/Scroll' 3 | import './mime.styl' 4 | 5 | export default class Mime extends React.Component { 6 | render() { 7 | return ( 8 | 9 |
10 |
11 |
12 | 13 | 本地音乐 14 | (0) 15 |
16 |
17 | 18 | 最近播放 19 | (0) 20 |
21 |
22 | 23 | 下载管理 24 | (0) 25 |
26 |
27 | 28 | 我的电台 29 | (0) 30 |
31 |
32 | 33 | 我的收藏 34 | (专辑/歌手/视频/专栏) 35 |
36 |
37 |
38 |
39 |
40 | 41 | 创建的歌单(1) 42 |
43 | 44 |
45 |
46 |
47 |
48 |
49 |
我喜欢的音乐
50 |
0 首
51 |
52 | 53 |
54 |
55 |
56 |
57 |
58 | ) 59 | } 60 | } -------------------------------------------------------------------------------- /src/containers/Radio.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | import RadioThumbnailList from 'components/radiothumbnailList/RadiothumbnailList' 4 | import Scroll from '../components/scroll/Scroll' 5 | import { getRadio } from '../actions/radio' 6 | import { refreshScroll } from 'utils' 7 | 8 | class Radio extends React.Component { 9 | componentDidMount() { 10 | this.props.getRadio() 11 | } 12 | 13 | componentDidUpdate(prevProps) { 14 | refreshScroll(this.scroll, this.props.showPlay, prevProps.showPlay) 15 | } 16 | 17 | itemClick(id) { 18 | this.props.history.push(`/radioDetail/${id}`) 19 | } 20 | 21 | render() { 22 | let { radioRecommendType, radioRecommends } = this.props 23 | 24 | return ( 25 | this.scroll = scroll}> 26 |
27 | this.itemClick(id)} /> 28 | this.itemClick(id)} /> 29 |
30 |
31 | ) 32 | } 33 | } 34 | 35 | const mapStateToProps = state => ({ 36 | radioRecommends: state.radio.radioRecommends, 37 | radioRecommendType: state.radio.radioRecommendType, 38 | showPlay: state.music.showPlay 39 | }) 40 | 41 | export default connect(mapStateToProps, { getRadio })(Radio) -------------------------------------------------------------------------------- /src/containers/found.styl: -------------------------------------------------------------------------------- 1 | .item 2 | position relative 3 | .pic 4 | width: 100% 5 | height: 0 6 | padding-top: 38.6% 7 | background-repeat: no-repeat 8 | background-size: cover 9 | .theme 10 | position: absolute 11 | right: 0 12 | bottom: 5px 13 | font-size: 12px 14 | padding: 5px 5px 5px 10px 15 | color: #ffffff 16 | border-radius: 11px 0 0 11px 17 | .recommend-list 18 | display: flex 19 | border-bottom: 1px solid #ccc 20 | padding: 15px 0 21 | margin-bottom: 10px 22 | .item 23 | flex: 1 24 | font-size: 12px 25 | color: #333333 26 | text-align: center 27 | .border 28 | border: 1px solid #c62f2f 29 | border-radius: 50% 30 | display: inline-block 31 | width: 50px 32 | height: 50px 33 | line-height: 50px 34 | margin-bottom: 7px 35 | .iconfont 36 | font-size: 26px 37 | color: #c62f2f -------------------------------------------------------------------------------- /src/containers/home.styl: -------------------------------------------------------------------------------- 1 | .scroll-wrapper 2 | position: fixed 3 | top: 80px 4 | left: 0 5 | bottom: 0 6 | right: 0 7 | overflow hidden -------------------------------------------------------------------------------- /src/containers/mime.styl: -------------------------------------------------------------------------------- 1 | .user-list 2 | .item 3 | display: flex 4 | align-items: center 5 | border-bottom: 1px solid #f3f3f3 6 | padding: 12px 15px 7 | .iconfont 8 | font-size: 20px 9 | color: #c62f2f 10 | .text 11 | color: #333333 12 | margin-left: 15px 13 | .info 14 | margin-left: 5px 15 | color: #c5c4c4 16 | font-size: 12px 17 | .play-list-header 18 | display: flex 19 | justify-content: space-between 20 | background-color: rgb(231, 233, 232) 21 | padding: 5px 10px 22 | .title 23 | font-size: 12px 24 | color: #565656 25 | margin-left: 5px 26 | .iconfont 27 | color: #b5b5b5 28 | display: inline-block 29 | .arrow 30 | transition: all 0.3s 31 | transform: rotate(90deg) 32 | .personalized-list 33 | .item 34 | display: flex 35 | align-items: center 36 | padding: 5px 37 | position relative 38 | &:after 39 | content: '' 40 | position: absolute 41 | left: calc(20% + 10px) 42 | right 0 43 | bottom: 0 44 | border-bottom: 1px solid #f3f3f3 45 | .icon 46 | width: 20% 47 | padding-top: 20% 48 | min-width 20% 49 | margin-right: 10px 50 | background: url(/images/personalized.png) no-repeat 51 | background-size: cover 52 | .content 53 | flex: 1 54 | .text 55 | color: #333333 56 | .num 57 | font-size: 12px 58 | color: #909090 59 | margin-top: 10px 60 | .iconfont 61 | font-size: 18px 62 | color: #909090 -------------------------------------------------------------------------------- /src/containers/playlistDetail.styl: -------------------------------------------------------------------------------- 1 | .play-list-nav-header 2 | display: flex 3 | height 50px 4 | align-items: center 5 | padding: 0 15px 0 20px 6 | position relative 7 | z-index: 1 8 | background-color: #c62f2f 9 | .back 10 | color: #ffffff 11 | .content 12 | flex: 1 13 | margin-left: 15px 14 | overflow hidden 15 | .name 16 | font-size: 16px 17 | color: #ffffff 18 | overflow hidden 19 | white-space nowrap 20 | text-overflow: ellipsis 21 | .info 22 | font-size: 12px 23 | margin-top: 3px 24 | color: #e8e4df 25 | .search 26 | font-size: 20px 27 | color: #ffffff 28 | .more 29 | font-size: 20px 30 | margin-left: 20px 31 | color: #ffffff 32 | .play-list-detail-wrapper 33 | position: fixed 34 | top: 0 35 | left 0 36 | right 0 37 | bottom: 0 38 | .introduce 39 | display flex 40 | padding: 60px 20px 10px 41 | .cover-img 42 | width: 114px 43 | height 114px 44 | position relative 45 | img 46 | width: 100% 47 | height 100% 48 | .count 49 | position: absolute 50 | top: 5px 51 | right 5px 52 | color: #ffffff 53 | font-size: 12px 54 | .iconfont 55 | font-size: 12px 56 | color: #ffffff 57 | .content 58 | flex: 1 59 | display: flex 60 | align-items: center 61 | margin-left: 20px 62 | flex-wrap: wrap 63 | .name 64 | font-size: 17px 65 | color: #fefefe 66 | line-height: 1.3 67 | width: 100% 68 | display: -webkit-box 69 | text-overflow: ellipsis 70 | overflow: hidden 71 | -webkit-line-clamp: 2 72 | .author 73 | display: flex 74 | align-items: center 75 | img 76 | width: 30px 77 | height 30px 78 | border-radius: 50% 79 | .nickname 80 | color: hsla(0, 0%, 100%, .7) 81 | margin-left: 5px 82 | font-size: 14px 83 | .cover-bg-wrapper 84 | position: absolute 85 | top: 0 86 | left 0 87 | right 0 88 | height: 240px 89 | z-index: -1 90 | overflow hidden 91 | .cover-bg 92 | background-repeat: no-repeat 93 | background-position: center 94 | filter: blur(10px) 95 | transform: scale(1.5) 96 | width: 100% 97 | height 100% 98 | .operation-wrapper 99 | display: flex 100 | text-align: center 101 | padding: 10px 0 102 | .item 103 | flex: 1 104 | font-size: 13px 105 | color: rgba(255,255,255,0.7) 106 | .text 107 | display: block 108 | margin-top: 5px 109 | .iconfont 110 | font-size: 18px 111 | .play-operation 112 | display: flex 113 | align-items: center 114 | height: 50px 115 | justify-content: space-between 116 | padding: 0 20px 117 | border-bottom: 1px solid #ddd 118 | .left 119 | display: flex 120 | align-items: center 121 | .play 122 | font-size: 20px 123 | color: #333333 124 | margin-right: 10px 125 | .count 126 | font-size: 15px 127 | color: #b1b1b1 128 | .right 129 | display: flex 130 | align-items: center 131 | font-size: 14px 132 | color: #333333 133 | .more 134 | color: #525252 135 | margin-right: 5px -------------------------------------------------------------------------------- /src/containers/radioDetail.styl: -------------------------------------------------------------------------------- 1 | .radio-detail-nav-header 2 | display: flex 3 | height 50px 4 | align-items: center 5 | padding: 0 15px 0 20px 6 | position relative 7 | z-index: 1 8 | background-color #c62f2f 9 | .back 10 | color: #ffffff 11 | .title 12 | font-size: 16px 13 | color: #ffffff 14 | margin-left: 15px 15 | flex: 1 16 | .shape 17 | font-size: 20px 18 | color: #ffffff 19 | .more 20 | font-size: 20px 21 | margin-left: 20px 22 | color: #ffffff 23 | .radio-detail-wrapper 24 | position: fixed 25 | top: 0 26 | left 0 27 | right 0 28 | bottom: 0 29 | .banner 30 | height 260px 31 | position relative 32 | background-repeat: no-repeat 33 | background-size cover 34 | background-position: center 35 | &:after 36 | content: '' 37 | position: absolute 38 | top: 0 39 | left 0 40 | right 0 41 | bottom: 0 42 | background: linear-gradient(rgba(120, 120, 120, 0.7), rgba(140, 140, 140, 0), rgba(140, 140, 140, 0), rgba(120, 120, 120, 0.7)) 43 | .content 44 | position: absolute 45 | left 0 46 | right 0 47 | bottom: 20px 48 | display: flex 49 | justify-content: space-between 50 | align-items: center 51 | padding: 0 15px 52 | z-index: 1 53 | .name 54 | font-size: 18px 55 | color: #ffffff 56 | .count 57 | font-size: 14px 58 | margin-top: 8px 59 | color: #ececec 60 | .sub-btn 61 | padding: 10px 20px 62 | background-color rgb(205, 61, 61) 63 | color: #ffffff 64 | font-size: 14px 65 | border-radius: 34px 66 | .radio-tab-wrapper 67 | display: flex 68 | height: 30px 69 | align-items: center 70 | position: relative 71 | .tab 72 | font-size: 14px 73 | color: #2c3e50 74 | text-align: center 75 | flex: 1 76 | &.active 77 | color: #c62f2f 78 | .programCount 79 | font-size: 12px 80 | color: #b3b3b3 81 | .underline 82 | position: absolute 83 | bottom: 0 84 | left: 0 85 | width: 50% 86 | transition: all 0.2s 87 | border-bottom: 2px solid #c62f2f 88 | .author-info-wrapper 89 | .title 90 | padding: 20px 0 20px 10px 91 | border-bottom: 1px solid #eeeeee 92 | color: #333333 93 | font-size: 15px 94 | position relative 95 | &:before 96 | content: '' 97 | position: absolute 98 | left: 0 99 | top: 0 100 | bottom: 0 101 | margin: auto 102 | border-left: 2px solid #c62f2f 103 | height 15px 104 | .info 105 | display: flex 106 | align-items: center 107 | padding: 10px 108 | border-bottom: 1px solid #eeeeee 109 | img 110 | height 55px 111 | width 55px 112 | border-radius: 50% 113 | .content 114 | flex: 1 115 | margin-left: 10px 116 | overflow hidden 117 | .desc 118 | font-size: 12px 119 | color: #aaaaaa 120 | margin-top: 5px 121 | display: -webkit-box 122 | text-overflow: ellipsis 123 | overflow: hidden 124 | -webkit-line-clamp: 2 125 | .reward 126 | font-size: 14px 127 | color: #c62f2f 128 | border: 1px solid #c62f2f 129 | padding: 5px 15px 130 | border-radius: 26px 131 | .count 132 | font-size: 12px 133 | color: #aaaaaa 134 | margin-top: 3px 135 | text-align: center 136 | .radio-info-wrapper 137 | padding: 30px 0 138 | .title 139 | color: #333333 140 | font-size: 15px 141 | position relative 142 | padding-left: 10px 143 | &:before 144 | content: '' 145 | position: absolute 146 | left: 0 147 | top: 0 148 | bottom: 0 149 | margin: auto 150 | border-left: 2px solid #c62f2f 151 | height 15px 152 | .content 153 | padding: 15px 10px 154 | color: rgb(100, 104, 103) 155 | font-size: 14px 156 | line-height: 21px 157 | .category 158 | color: #c62f2f 159 | border: 1px solid #c62f2f 160 | font-size: 12px 161 | padding: 0 2px 162 | border-radius: 3px 163 | .program-header 164 | display: flex 165 | justify-content: space-between 166 | padding: 5px 10px 167 | background-color rgb(231, 233, 232) 168 | font-size: 14px 169 | color: rgb(99, 101, 100) 170 | .choose 171 | margin-left: 20px 172 | .iconfont 173 | font-size: 15px -------------------------------------------------------------------------------- /src/containers/search.styl: -------------------------------------------------------------------------------- 1 | .search-box 2 | display: flex 3 | align-items: center 4 | height 50px 5 | background-color: #c62f2f 6 | padding: 0 15px 7 | .back 8 | font-size: 18px 9 | color: #ffffff 10 | margin-right: 5px 11 | .search-input 12 | font-size: 16px 13 | color: rgb(196, 137, 136) 14 | border: none 15 | border-bottom: 2px solid rgb(219, 88, 79) 16 | outline: none 17 | background-color: #c62f2f 18 | padding: 7px 19 | flex: 1 20 | &::-webkit-input-placeholder 21 | color: rgb(196, 137, 136) 22 | .history-scroll-wrapper 23 | position: fixed 24 | top: 50px 25 | left 0 26 | right 0 27 | bottom: 0 28 | .search-result-wrapper 29 | position: relative 30 | width: 100vw 31 | height calc(100vh - 80px) 32 | background-color: rgb(240, 244, 243) -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill' 2 | import React from 'react' 3 | import ReactDOM from 'react-dom' 4 | import FastClick from 'fastclick' 5 | import { createStore, applyMiddleware } from 'redux' 6 | import { Provider } from 'react-redux' 7 | // import thunk from 'redux-thunk' 8 | import { createLogger } from 'redux-logger' 9 | import { callAPIMiddleware } from './utils/reactUtil' 10 | import reducers from './reducers' 11 | import App from './App' 12 | import registerServiceWorker from './registerServiceWorker' 13 | 14 | import './styles/index.styl' 15 | 16 | FastClick.attach(document.body) 17 | 18 | const middleware = [callAPIMiddleware] 19 | 20 | if (process.env.NODE_ENV !== 'production') { 21 | middleware.push(createLogger()) 22 | } 23 | 24 | const store = createStore( 25 | reducers, applyMiddleware(...middleware) 26 | ) 27 | 28 | ReactDOM.render( 29 | 30 | 31 | , 32 | document.getElementById('root') 33 | ) 34 | 35 | registerServiceWorker() 36 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/reducers/homeData.js: -------------------------------------------------------------------------------- 1 | import { createReducer } from '../utils/reactUtil' 2 | import * as types from '../actions/actionTypes' 3 | 4 | const initialState = { 5 | banners: [], 6 | personalized: [] 7 | } 8 | 9 | const homeData = createReducer(initialState, { 10 | [types.GET_HOME_DATA](state, action) { 11 | return { 12 | ...state, 13 | ...action.response 14 | } 15 | } 16 | }) 17 | 18 | export default homeData -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import homeData from './homeData' 3 | import slidebarState from './slidebar' 4 | import search from './search' 5 | import radio from './radio' 6 | import music from './music' 7 | 8 | export default combineReducers({ 9 | homeData, 10 | slidebarState, 11 | search, 12 | radio, 13 | music 14 | }) -------------------------------------------------------------------------------- /src/reducers/music.js: -------------------------------------------------------------------------------- 1 | import { createReducer } from '../utils/reactUtil' 2 | import * as types from '../actions/actionTypes' 3 | import storage from 'good-storage' 4 | import watcher from 'utils/watcher' 5 | import { distinctList, shuffle } from '../utils' 6 | 7 | const MUSIC_KEY = 'musicKey' 8 | 9 | const _music = storage.get(MUSIC_KEY, { 10 | originList: [], 11 | mode: 'order', // random order loop 12 | }) 13 | 14 | const musicDistinctList = (state, action) => distinctList(state.originList, item => item.id === action.music.id) 15 | 16 | const getCurrentList = ({ originList, mode }) => { 17 | if (mode === 'random') { 18 | return shuffle(originList) 19 | } 20 | 21 | return originList 22 | } 23 | 24 | const initialState = { 25 | originList: _music.originList, 26 | currentList: getCurrentList(_music), 27 | mode: _music.mode, 28 | currentIndex: 0, 29 | showPlay: !!_music.originList.length 30 | } 31 | 32 | const music = createReducer(initialState, { 33 | [types.ADD_MUSIC](state, action) { 34 | let originList = [action.music].concat(musicDistinctList(state, action)) 35 | 36 | _music.originList = originList 37 | storage.set(MUSIC_KEY, _music) 38 | 39 | watcher.emit('addMusic', action.music.id) 40 | 41 | return { 42 | ...state, 43 | originList, 44 | currentList: [action.music].concat(musicDistinctList(state, action)), 45 | currentIndex: 0, 46 | showPlay: true 47 | } 48 | }, 49 | [types.REMOVE_MUSIC](state, action) { 50 | let originList = musicDistinctList(state, action) 51 | 52 | _music.originList = originList 53 | storage.set(MUSIC_KEY, _music) 54 | 55 | let index = state.currentList.findIndex(item => item.id === action.music.id) 56 | let currentIndex = state.currentIndex 57 | 58 | if (index < currentIndex) { 59 | currentIndex-- 60 | } 61 | 62 | return { 63 | ...state, 64 | originList, 65 | currentList: musicDistinctList(state, action), 66 | currentIndex 67 | } 68 | }, 69 | [types.PREV_MUSIC](state, action) { 70 | let index = state.currentIndex - 1 71 | if (index === -1) { 72 | index = state.currentList.length - 1 73 | } 74 | 75 | return { 76 | ...state, 77 | currentIndex: index 78 | } 79 | }, 80 | [types.NEXT_MUSIC](state, action) { 81 | let len = state.currentList.length 82 | let index = state.currentIndex + 1 83 | 84 | if (index === len) { 85 | if (action.isAuto) { 86 | return state 87 | } else { 88 | index = 0 89 | } 90 | } 91 | 92 | return { 93 | ...state, 94 | currentIndex: index 95 | } 96 | }, 97 | [types.SWITCH_MUSIC](state, action) { 98 | let index = state.currentList.findIndex(item => item.id === action.id) 99 | if (index === -1) { 100 | return state 101 | } 102 | 103 | return { 104 | ...state, 105 | currentIndex: index 106 | } 107 | }, 108 | [types.TOGGLE_MODE](state, action) { 109 | let { mode, originList, currentList, currentIndex } = state 110 | if (mode === 'order') { 111 | mode = 'random' 112 | } else if (mode === 'random') { 113 | mode = 'loop' 114 | } else { 115 | mode = 'order' 116 | } 117 | 118 | // 设置当前歌曲在切换模式后的index 119 | let currentMusic = currentList[currentIndex] 120 | currentList = getCurrentList({ mode, originList }) 121 | currentIndex = currentList.findIndex(item => item.id === currentMusic.id) 122 | 123 | _music.mode = mode 124 | storage.set(MUSIC_KEY, _music) 125 | 126 | return { 127 | ...state, 128 | currentList, 129 | currentIndex, 130 | mode 131 | } 132 | }, 133 | [types.REMOVE_ALL_MUSIC](state, action) { 134 | _music.originList = [] 135 | storage.set(MUSIC_KEY, _music) 136 | 137 | return { 138 | ...state, 139 | originList: [], 140 | currentList: [], 141 | currentIndex: 0, 142 | showPlay: false 143 | } 144 | } 145 | }) 146 | 147 | export default music -------------------------------------------------------------------------------- /src/reducers/radio.js: -------------------------------------------------------------------------------- 1 | import { createReducer } from '../utils/reactUtil' 2 | import * as types from '../actions/actionTypes' 3 | 4 | const initialState = { 5 | radioRecommends: [], 6 | radioRecommendType: [], 7 | radioDetail: {}, 8 | radioPrograms: [] 9 | } 10 | 11 | const radio = createReducer(initialState, { 12 | [types.GET_RADIO](state, action) { 13 | return { 14 | ...state, 15 | ...action.response 16 | } 17 | } 18 | }) 19 | 20 | export default radio -------------------------------------------------------------------------------- /src/reducers/search.js: -------------------------------------------------------------------------------- 1 | import { createReducer } from '../utils/reactUtil' 2 | import * as types from '../actions/actionTypes' 3 | import storage from 'good-storage' 4 | 5 | const SEARCH_HISTORY_KEY = 'searchHistoryKey' 6 | 7 | const initialState = { 8 | searchSuggest: {}, 9 | history: storage.get(SEARCH_HISTORY_KEY, []) 10 | } 11 | 12 | const searchInfo = createReducer(initialState, { 13 | [types.GET_SEARCH_SUGGEST](state, action) { 14 | return { 15 | ...state, 16 | searchSuggest: action.response.searchSuggest 17 | } 18 | }, 19 | [types.ADD_SEARCH_HISTORY](state, action) { 20 | let history = state.history 21 | let index = history.findIndex(value => value === action.keywords) 22 | 23 | history = [...history] 24 | if (index !== -1) { 25 | history.splice(index, 1) 26 | } 27 | 28 | history.unshift(action.keywords) 29 | 30 | storage.set(SEARCH_HISTORY_KEY, history) 31 | 32 | return { 33 | ...state, 34 | history 35 | } 36 | }, 37 | [types.RM_SEARCH_HISTORY](state, action) { 38 | let history = state.history 39 | let index = history.findIndex(value => value === action.keywords) 40 | 41 | if (index === -1) { 42 | return state 43 | } 44 | 45 | history = [...history] 46 | history.splice(index, 1) 47 | 48 | storage.set(SEARCH_HISTORY_KEY, history) 49 | 50 | return { 51 | ...state, 52 | history 53 | } 54 | } 55 | }) 56 | 57 | export default searchInfo -------------------------------------------------------------------------------- /src/reducers/slidebar.js: -------------------------------------------------------------------------------- 1 | import { createReducer } from '../utils/reactUtil' 2 | import * as types from '../actions/actionTypes' 3 | 4 | const slidebarState = createReducer(false, { 5 | [types.SET_SLIDEBAR_STATE](state, action) { 6 | return action.slidebarState 7 | } 8 | }) 9 | 10 | export default slidebarState -------------------------------------------------------------------------------- /src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | const isLocalhost = Boolean( 12 | window.location.hostname === 'localhost' || 13 | // [::1] is the IPv6 localhost address. 14 | window.location.hostname === '[::1]' || 15 | // 127.0.0.1/8 is considered localhost for IPv4. 16 | window.location.hostname.match( 17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 18 | ) 19 | ); 20 | 21 | export default function register() { 22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location); 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 29 | return; 30 | } 31 | 32 | window.addEventListener('load', () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 34 | 35 | if (isLocalhost) { 36 | // This is running on localhost. Lets check if a service worker still exists or not. 37 | checkValidServiceWorker(swUrl); 38 | 39 | // Add some additional logging to localhost, pointing developers to the 40 | // service worker/PWA documentation. 41 | navigator.serviceWorker.ready.then(() => { 42 | console.log( 43 | 'This web app is being served cache-first by a service ' + 44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ' 45 | ); 46 | }); 47 | } else { 48 | // Is not local host. Just register service worker 49 | registerValidSW(swUrl); 50 | } 51 | }); 52 | } 53 | } 54 | 55 | function registerValidSW(swUrl) { 56 | navigator.serviceWorker 57 | .register(swUrl) 58 | .then(registration => { 59 | registration.onupdatefound = () => { 60 | const installingWorker = registration.installing; 61 | installingWorker.onstatechange = () => { 62 | if (installingWorker.state === 'installed') { 63 | if (navigator.serviceWorker.controller) { 64 | // At this point, the old content will have been purged and 65 | // the fresh content will have been added to the cache. 66 | // It's the perfect time to display a "New content is 67 | // available; please refresh." message in your web app. 68 | console.log('New content is available; please refresh.'); 69 | } else { 70 | // At this point, everything has been precached. 71 | // It's the perfect time to display a 72 | // "Content is cached for offline use." message. 73 | console.log('Content is cached for offline use.'); 74 | } 75 | } 76 | }; 77 | }; 78 | }) 79 | .catch(error => { 80 | console.error('Error during service worker registration:', error); 81 | }); 82 | } 83 | 84 | function checkValidServiceWorker(swUrl) { 85 | // Check if the service worker can be found. If it can't reload the page. 86 | fetch(swUrl) 87 | .then(response => { 88 | // Ensure service worker exists, and that we really are getting a JS file. 89 | if ( 90 | response.status === 404 || 91 | response.headers.get('content-type').indexOf('javascript') === -1 92 | ) { 93 | // No service worker found. Probably a different app. Reload the page. 94 | navigator.serviceWorker.ready.then(registration => { 95 | registration.unregister().then(() => { 96 | window.location.reload(); 97 | }); 98 | }); 99 | } else { 100 | // Service worker found. Proceed as normal. 101 | registerValidSW(swUrl); 102 | } 103 | }) 104 | .catch(() => { 105 | console.log( 106 | 'No internet connection found. App is running in offline mode.' 107 | ); 108 | }); 109 | } 110 | 111 | export function unregister() { 112 | if ('serviceWorker' in navigator) { 113 | navigator.serviceWorker.ready.then(registration => { 114 | registration.unregister(); 115 | }); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import { getAsyncComponent } from 'utils/reactUtil' 2 | const Home = getAsyncComponent(() => import(/* webpackChunkName: 'pageHome' */ 'containers/Home')) 3 | const Found = getAsyncComponent(() => import(/* webpackChunkName: 'pageFound' */ 'containers/Found')) 4 | const Mime = getAsyncComponent(() => import(/* webpackChunkName: 'pageMime' */ 'containers/Mime')) 5 | const Radio = getAsyncComponent(() => import(/* webpackChunkName: 'pageRadio' */ 'containers/Radio')) 6 | const Search = getAsyncComponent(() => import(/* webpackChunkName: 'pageSearch' */ 'containers/Search')) 7 | const PlaylistDetail = getAsyncComponent(() => import(/* webpackChunkName: 'pagePlaylistDetail' */ 'containers/PlaylistDetail')) 8 | const RadioDetail = getAsyncComponent(() => import(/* webpackChunkName: 'pageRadioDetail' */ 'containers/RadioDetail')) 9 | 10 | const routeConfig = [ 11 | { 12 | path: '/home', 13 | component: Home, 14 | routes: [ 15 | { 16 | path: '/', 17 | component: Found, 18 | exact: true 19 | }, 20 | { 21 | path: '/mime', 22 | component: Mime 23 | }, 24 | { 25 | path: '/radio', 26 | component: Radio 27 | } 28 | ] 29 | }, 30 | { 31 | path: '/search', 32 | component: Search, 33 | exact: true 34 | }, 35 | { 36 | path: '/search/:keywords', 37 | component: Search 38 | }, 39 | { 40 | path: '/playlistDetail/:id', 41 | component: PlaylistDetail 42 | }, 43 | { 44 | path: '/radioDetail/:rid', 45 | component: RadioDetail 46 | } 47 | ] 48 | 49 | export default routeConfig -------------------------------------------------------------------------------- /src/styles/icon.styl: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'iconfont'; /* project id 623934 */ 3 | src: url('../assets/font/iconfont.eot'); 4 | src: url('../assets/font/iconfont.eot#iefix') format('embedded-opentype'), 5 | url('../assets/font/iconfont.woff') format('woff'), 6 | url('../assets/font/iconfont.ttf') format('truetype'), 7 | url('../assets/font/iconfont.svg#iconfont') format('svg'); 8 | } 9 | 10 | .iconfont 11 | font-family: "iconfont" !important 12 | font-size: 16px 13 | font-style: normal 14 | -webkit-font-smoothing: antialiased 15 | -webkit-text-stroke-width: 0.2px 16 | -moz-osx-font-smoothing: grayscale -------------------------------------------------------------------------------- /src/styles/index.styl: -------------------------------------------------------------------------------- 1 | @import "./reset.styl" 2 | @import "./icon.styl" -------------------------------------------------------------------------------- /src/styles/reset.styl: -------------------------------------------------------------------------------- 1 | /** 2 | * Eric Meyer's Reset CSS v2.0 (http://meyerweb.com/eric/tools/css/reset/) 3 | * http://cssreset.com 4 | */ 5 | html, body, div, span, applet, object, iframe, 6 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 7 | a, abbr, acronym, address, big, cite, code, 8 | del, dfn, em, img, ins, kbd, q, s, samp, 9 | small, strike, strong, sub, sup, tt, var, 10 | b, u, i, center, 11 | dl, dt, dd, ol, ul, li, 12 | fieldset, form, label, legend, 13 | table, caption, tbody, tfoot, thead, tr, th, td, 14 | article, aside, canvas, details, embed, 15 | figure, figcaption, footer, header, 16 | menu, nav, output, ruby, section, summary, 17 | time, mark, audio, video, input 18 | margin: 0 19 | padding: 0 20 | border: 0 21 | font-size: 100% 22 | font-weight: normal 23 | vertical-align: baseline 24 | 25 | /* HTML5 display-role reset for older browsers */ 26 | article, aside, details, figcaption, figure, 27 | footer, header, menu, nav, section 28 | display: block 29 | 30 | body 31 | line-height: 1 32 | 33 | blockquote, q 34 | quotes: none 35 | 36 | blockquote:before, blockquote:after, 37 | q:before, q:after 38 | content: none 39 | 40 | table 41 | border-collapse: collapse 42 | border-spacing: 0 43 | 44 | /* custom */ 45 | 46 | a 47 | color: #7e8c8d 48 | -webkit-backface-visibility: hidden 49 | text-decoration: none 50 | 51 | li 52 | list-style: none 53 | 54 | body 55 | -webkit-text-size-adjust: none 56 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0) 57 | -------------------------------------------------------------------------------- /src/utils/axios.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | axios.defaults.baseURL = '/graphql' 4 | 5 | axios.interceptors.response.use(res => { 6 | return res && res.data 7 | }, err => { 8 | return Promise.reject(err) 9 | }) 10 | 11 | export default axios -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | export function numFormat(num) { 2 | if (num >= 100000) { 3 | return (num / 10000).toFixed(1) + '万' 4 | } 5 | return num 6 | } 7 | 8 | export function timeFormat(time) { 9 | time = ~~(time / 1000) 10 | let second = time % 60 11 | let min = ~~(time / 60) 12 | let hour 13 | 14 | if (min >= 60) { 15 | hour = ~~(min / 60) 16 | min = min % 60 17 | } 18 | 19 | min = padLeft(min) 20 | second = padLeft(second) 21 | 22 | return hour ? `${hour}:${min}:${second}` : `${min}:${second}` 23 | } 24 | 25 | function padLeft(value, str = '0', len = 2) { 26 | let length = (''+ value).length 27 | while (length++ < len) { 28 | value = str + value 29 | } 30 | 31 | return value 32 | } 33 | 34 | export function getDate(time, connectStr = '.') { 35 | let date = new Date(time) 36 | let year = date.getFullYear() 37 | let month = date.getMonth() + 1 38 | let day = date.getDate() 39 | 40 | return [year, month, day].join(connectStr) 41 | } 42 | 43 | export function dateFormat(fmt, date = new Date()) { 44 | const o = { 45 | "M+": date.getMonth() + 1, //月份 46 | "d+": date.getDate(), //日 47 | "h+": date.getHours(), //小时 48 | "m+": date.getMinutes(), //分 49 | "s+": date.getSeconds(), //秒 50 | "q+": Math.floor((date.getMonth() + 3) / 3), //季度 51 | "S": date.getMilliseconds() //毫秒 52 | }; 53 | if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (date.getFullYear() + "").substr(4 - RegExp.$1.length)); 54 | for (let k in o) 55 | if (new RegExp("(" + k + ")").test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length === 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length))); 56 | return fmt; 57 | } 58 | 59 | /** 60 | * 找到并删除对应项 61 | * @param {array} list 62 | * @param {function} fn 查找函数 63 | */ 64 | export function distinctList(list, fn) { 65 | let index = list.findIndex(fn) 66 | if (index === -1) { 67 | return list 68 | } 69 | 70 | let _list = list.slice() 71 | _list.splice(index, 1) 72 | 73 | return _list 74 | } 75 | 76 | export const debounce = function(fn, delay = 100, context) { 77 | let timer 78 | return (...args) => { 79 | clearTimeout(timer) 80 | timer = setTimeout(() => fn.apply(context, args), delay) 81 | } 82 | } 83 | 84 | export const refreshScroll = (scroll, showPlay, prevShowPlay) => { 85 | if (showPlay === prevShowPlay) { 86 | return 87 | } 88 | 89 | scroll && scroll.refresh() 90 | } 91 | 92 | export const shuffle = (arr) => { 93 | let _arr = arr.slice() 94 | _arr.forEach((value, i) => { 95 | let j = ~~(Math.random() * (i + 1)) 96 | let temp = _arr[j] 97 | _arr[j] = _arr[i] 98 | _arr[i] = temp 99 | }) 100 | 101 | return _arr 102 | } 103 | 104 | export const modeData = { 105 | random: { 106 | text: '随机播放', 107 | icon: '' 108 | }, 109 | order: { 110 | text: '顺序播放', 111 | icon: '' 112 | }, 113 | loop: { 114 | text: '单曲循环', 115 | icon: '' 116 | } 117 | } -------------------------------------------------------------------------------- /src/utils/music.js: -------------------------------------------------------------------------------- 1 | class Music { 2 | constructor({ name, id, duration, artistName, picUrl, url, lyric }) { 3 | this.name = name 4 | this.id = id 5 | this.duration = duration 6 | this.artistName = artistName 7 | this.picUrl = picUrl 8 | this.url = url 9 | this.lyric = lyric 10 | } 11 | } 12 | 13 | export default Music -------------------------------------------------------------------------------- /src/utils/reactUtil.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Route } from 'react-router-dom' 3 | 4 | /** 5 | * 根据路由配置生成相应路由 6 | * @param {array} routeConfig 路由配置 7 | * @param {string} parentPath 父级路由 8 | */ 9 | export function routes(routeConfig, parentPath = '') { 10 | if (!routeConfig || routeConfig.length === 0) { 11 | return null 12 | } 13 | 14 | return ( 15 | routeConfig.map(route => ( 16 | ( 17 | 18 | {/* 在父级路由通过 this.props.children 即可添加嵌套路由*/} 19 | {routes(route.routes, parentPath + route.path)} 20 | 21 | )} /> 22 | )) 23 | ) 24 | } 25 | 26 | /** 27 | * 调用示例 28 | * 29 | export const getHomeData = (userId) => { 30 | return { 31 | // 要在之前和之后发送的 action types 32 | types: { 33 | requestType: 'requestType', 可选 34 | successType: 'successType', 35 | failureType: 'failureType' 可选 36 | }, 37 | // 检查缓存 (可选): 38 | shouldCallAPI: state => !state.users[userId], 39 | // 进行取: 40 | callAPI: () => fetch(`http://myapi.com/users/${userId}/posts`), 41 | // 在 actions 的开始和结束注入的参数 42 | payload: { userId } 43 | } 44 | } 45 | */ 46 | export function callAPIMiddleware({ dispatch, getState }) { 47 | return next => action => { 48 | const { 49 | types, 50 | callAPI, 51 | shouldCallAPI = () => true, 52 | payload = {} 53 | } = action 54 | 55 | if (!types) { 56 | // Normal action: pass it on 57 | return next(action) 58 | } 59 | 60 | if (types.toString() !== '[object Object]' || types.successType === undefined) { 61 | throw new Error('Expected an object or property success undefined') 62 | } 63 | 64 | if (typeof callAPI !== 'function') { 65 | throw new Error('Expected callAPI to be a function.') 66 | } 67 | 68 | if (!shouldCallAPI(getState())) { 69 | return 70 | } 71 | 72 | const { requestType, successType, failureType } = types 73 | 74 | requestType && dispatch( 75 | Object.assign({}, payload, { 76 | type: requestType 77 | }) 78 | ) 79 | 80 | return callAPI().then( 81 | response => 82 | dispatch( 83 | Object.assign({}, payload, { 84 | response: response.data, // 简化数据结构层次 85 | type: successType 86 | }) 87 | ), 88 | error => 89 | failureType && dispatch( 90 | Object.assign({}, payload, { 91 | error, 92 | type: failureType 93 | }) 94 | ) 95 | ) 96 | } 97 | } 98 | 99 | export function createReducer(initialState, handlers) { 100 | return function reducer(state = initialState, action) { 101 | if (handlers.hasOwnProperty(action.type)) { 102 | return handlers[action.type](state, action) 103 | } else { 104 | return state 105 | } 106 | } 107 | } 108 | 109 | /** 110 | * 异步加载组件 111 | * @param load 组件加载函数,load 函数会返回一个 Promise,在文件加载完成时 resolve 112 | * @returns {AsyncComponent} 返回一个高阶组件用于封装需要异步加载的组件 113 | */ 114 | export function getAsyncComponent(load) { 115 | return class AsyncComponent extends React.Component { 116 | componentDidMount() { 117 | load().then(({ default: component }) => { 118 | this.setState({ 119 | Component: component 120 | }) 121 | }) 122 | } 123 | 124 | render() { 125 | const { Component } = this.state || {} 126 | 127 | return Component ? : null 128 | } 129 | } 130 | } -------------------------------------------------------------------------------- /src/utils/watcher.js: -------------------------------------------------------------------------------- 1 | class Watcher { 2 | constructor() { 3 | this.events = {} 4 | } 5 | 6 | emit(type, ...args) { 7 | if (!this.events[type]) { 8 | return 9 | } 10 | 11 | this.events[type].forEach(fn => fn(...args)) 12 | } 13 | 14 | on(type, fn) { 15 | !this.events[type] && (this.events[type] = []) 16 | 17 | this.events[type].push(fn) 18 | } 19 | 20 | destroy(type, fn) { 21 | let typeArr = this.events[type] 22 | if (!typeArr) { 23 | return 24 | } 25 | 26 | let index = typeArr.findIndex(value => value === fn) 27 | return this.events[type].splice(index, 1)[0] 28 | } 29 | } 30 | 31 | export default new Watcher() --------------------------------------------------------------------------------