Repository: ruichengping/react-mobile-qqMusic Branch: master Commit: ba3311803ba3 Files: 58 Total size: 130.1 KB Directory structure: gitextract_qwih4zgh/ ├── .babelrc ├── .gitignore ├── .postcssrc.js ├── README.md ├── build/ │ ├── build.js │ ├── check-versions.js │ ├── utils.js │ ├── webpack.base.conf.js │ ├── webpack.dev.conf.js │ └── webpack.prod.conf.js ├── config/ │ ├── dev.env.js │ ├── index.js │ └── prod.env.js ├── index.html ├── package.json ├── src/ │ ├── App.js │ ├── api/ │ │ ├── index.js │ │ └── url.js │ ├── components/ │ │ ├── Bandstand/ │ │ │ ├── index.js │ │ │ └── style.scss │ │ ├── Control/ │ │ │ ├── index.js │ │ │ └── style.scss │ │ ├── Header/ │ │ │ ├── index.js │ │ │ └── style.scss │ │ ├── Loading/ │ │ │ ├── index.js │ │ │ └── style.scss │ │ ├── MusicList/ │ │ │ ├── index.js │ │ │ └── style.scss │ │ ├── NewSongMenu/ │ │ │ ├── index.js │ │ │ └── style.scss │ │ ├── Search/ │ │ │ ├── index.js │ │ │ └── style.scss │ │ ├── Slider/ │ │ │ ├── index.js │ │ │ └── style.scss │ │ ├── SongMenu/ │ │ │ ├── index.js │ │ │ └── style.scss │ │ └── SongMenuMangement/ │ │ ├── index.js │ │ └── style.scss │ ├── constant/ │ │ └── music.js │ ├── layouts/ │ │ └── MainLayout/ │ │ ├── index.js │ │ └── style.scss │ ├── main.js │ ├── pages/ │ │ ├── Discovery/ │ │ │ ├── index.js │ │ │ └── style.scss │ │ ├── MusicClub/ │ │ │ ├── index.js │ │ │ └── style.scss │ │ └── MyCenter/ │ │ ├── index.js │ │ └── style.scss │ ├── router/ │ │ └── index.js │ ├── scss/ │ │ ├── reset.scss │ │ └── variable.scss │ ├── store/ │ │ ├── actionTypes.js │ │ ├── actions.js │ │ ├── index.js │ │ └── reducer.js │ └── utils/ │ ├── http.js │ └── index.js └── theme.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .babelrc ================================================ { "presets": [ ["@babel/preset-env", { "modules": false, "targets": { "browsers": ["> 1%", "last 2 versions", "not ie <= 8"] }, "useBuiltIns": "entry" }], "@babel/preset-react" ], "plugins": [ "@babel/plugin-transform-runtime", // Stage 2 ["@babel/plugin-proposal-decorators", { "legacy": true }], "@babel/plugin-proposal-function-sent", "@babel/plugin-proposal-export-namespace-from", "@babel/plugin-proposal-numeric-separator", "@babel/plugin-proposal-throw-expressions", // Stage 3 "@babel/plugin-syntax-dynamic-import", "@babel/plugin-syntax-import-meta", ["@babel/plugin-proposal-class-properties", { "loose": false }], "@babel/plugin-proposal-json-strings", ["import", { "libraryName": "antd-mobile" ,"style": true}] ] } ================================================ FILE: .gitignore ================================================ dist node_modules .idea ================================================ FILE: .postcssrc.js ================================================ module.exports = { "plugins": { "postcss-import": {}, "postcss-url": {}, // to edit target browsers: use "browserslist" field in package.json "autoprefixer": {} } } ================================================ FILE: README.md ================================================ # react-mobile-qqMusic # 技术栈 1. react 2. react-router 3. react-redux 4. es6 5. axios 6. webpack # 关于项目 ## 1.安装依赖包 ``` yarn ``` ## 2.启动服务 ### 开发者 ``` npm run dev ``` ## 3.编译 ``` npm run build ``` # 已实现功能 ## Tab-我的 ![Tab-我的](https://segmentfault.com/img/bVVCk9?w=660&h=1174) ## Tab-音乐馆 ![Tab-音乐馆](https://segmentfault.com/img/bVVClw?w=662&h=1182) ## Tab-发现 ![Tab-发现](https://segmentfault.com/img/bVVClz?w=660&h=1174) ## 侧滑栏 ![Tab-侧滑栏](https://segmentfault.com/img/bVVClH?w=660&h=1176) ## 播放列表 ![Tab-播放列表](https://segmentfault.com/img/bVVCmT?w=660&h=1174) ## 播放器 ![Tab-播放器1](https://segmentfault.com/img/bVVClM?w=662&h=1170) ![Tab-播放器2](https://segmentfault.com/img/bVVCl4?w=664&h=1174) ## 歌曲搜索 ![Tab-歌曲搜索1](https://segmentfault.com/img/bVVCl7?w=662&h=1174) ![Tab-歌曲搜索2](https://segmentfault.com/img/bVVCmi?w=654&h=1174) ## 歌单管理 ![Tab-歌单管理1](https://segmentfault.com/img/bVVCmA?w=660&h=1180) ![Tab-歌单管理2](https://segmentfault.com/img/bVVCmM?w=660&h=1176) # 项目总结 整个项目采用了React这个框架来构建,之前我都是用Vue用做开发的。正好借此机会做一个小小的对比,纯是个人使用的心得体会。如果你也有一些不一样的心得交流的话,欢迎交流。 1. React相比Vue给我感受最深的就是他的优雅的组件化,用起来是非常爽,谁用谁知道,引用即可使用。而Vue在这块相对来说就要弱一点,引用了组件之后还要注册一下。 2. Vue在双向数据绑定的体验上要优于React的,React采用的是Flux的单向数据流动。这在实现一些需要双向数据交互的功能上,Vue是占有优势的。 3. Vue相比React更加轻量级。Vue只需要引用一个Vue.js即可使用,而React则要引用React.js、React-dom.js、babel.js(用于转换jsx的语法)。 4. Vue在上手程度上要优于React。Vue学习成本很低,另外官方有比较完善的中文文档。而React官方则只有英文文档,另外学习成本也比较高。我见到网上某人喷只会Vue的是前端小白,我对这种人只能报以呵呵。简单本身是没有错误,一个东西能以简单的方式解决难道不好吗?关于这个中文文档居然还有人喷那些喜欢用Vue的是不是英文能力差,我就再报以呵呵一笑。本身拥有中文文档就是一个优势,结果还成了被喷的地方。首先,并不是所有人的英文能力都跟某些嘴炮大神那么牛逼的。其次,就算是英文能力牛逼的人,你敢说你阅读中文的能力会比你阅读英文能力差? 5. 我个人感觉Vue的全家桶(不包括Vue)使用起来,我个人感觉是要比React的全家桶(不包括React)使用起来舒服的。 6. 虽然Vue在一些细节上要比React好,但是不能觉得React就比Vue差。这种想法是错误。特别是大型应用上,使用React项目维护起来肯定是要比Vue要好的。当然这不代表Vue不能构建大型应用。 7. React在社区生态建设上是比Vue好很多的,而且后面站着FaceBook。不怕遇到问题没人可以帮你解决的情况,而Vue的话就要稍微担心一下。 > 最后强调一下:React和Vue都是非常棒的前端框架,建议大家都去学习一下。采用React或者是Vue还是要结合业务场景和现实情况做选择的。单纯说React还是Vue好,我个人觉得都是耍流氓。 ================================================ FILE: build/build.js ================================================ 'use strict' require('./check-versions')() process.env.NODE_ENV = 'production' const ora = require('ora') const rm = require('rimraf') const path = require('path') const chalk = require('chalk') const webpack = require('webpack') const config = require('../config') const webpackConfig = require('./webpack.prod.conf') const spinner = ora('building for production...') spinner.start() rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => { if (err) throw err webpack(webpackConfig, (err, stats) => { spinner.stop() if (err) throw err process.stdout.write(stats.toString({ colors: true, modules: false, children: false, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build. chunks: false, chunkModules: false }) + '\n\n') if (stats.hasErrors()) { console.log(chalk.red(' Build failed with errors.\n')) process.exit(1) } console.log(chalk.cyan(' Build complete.\n')) console.log(chalk.yellow( ' Tip: built files are meant to be served over an HTTP server.\n' + ' Opening index.html over file:// won\'t work.\n' )) }) }) ================================================ FILE: build/check-versions.js ================================================ 'use strict' const chalk = require('chalk') const semver = require('semver') const packageConfig = require('../package.json') const shell = require('shelljs') function exec (cmd) { return require('child_process').execSync(cmd).toString().trim() } const versionRequirements = [ { name: 'node', currentVersion: semver.clean(process.version), versionRequirement: packageConfig.engines.node } ] if (shell.which('npm')) { versionRequirements.push({ name: 'npm', currentVersion: exec('npm --version'), versionRequirement: packageConfig.engines.npm }) } module.exports = function () { const warnings = [] for (let i = 0; i < versionRequirements.length; i++) { const mod = versionRequirements[i] if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { warnings.push(mod.name + ': ' + chalk.red(mod.currentVersion) + ' should be ' + chalk.green(mod.versionRequirement) ) } } if (warnings.length) { console.log('') console.log(chalk.yellow('To use this template, you must update following to modules:')) console.log() for (let i = 0; i < warnings.length; i++) { const warning = warnings[i] console.log(' ' + warning) } console.log() process.exit(1) } } ================================================ FILE: build/utils.js ================================================ 'use strict' const path = require('path') const config = require('../config') const MiniCssExtractPlugin = require("mini-css-extract-plugin") const packageConfig = require('../package.json') const theme=require('../theme') const devMode = process.env.NODE_ENV !== 'production' exports.assetsPath = function (_path) { const assetsSubDirectory = process.env.NODE_ENV === 'production' ? config.build.assetsSubDirectory : config.dev.assetsSubDirectory return path.posix.join(assetsSubDirectory, _path) } exports.cssLoaders = function (options) { options = options || {} const cssLoader = { loader: 'css-loader', options: { sourceMap: options.sourceMap } } const postcssLoader = { loader: 'postcss-loader', options: { sourceMap: options.sourceMap } } const styleLoader={ loader:devMode ? 'style-loader' : MiniCssExtractPlugin.loader, } // generate loader string to be used with extract text plugin function generateLoaders (loader, loaderOptions) { const loaders = options.usePostCSS ? [styleLoader,cssLoader, postcssLoader] : [styleLoader,cssLoader] if (loader) { loaders.push({ loader: loader + '-loader', options: Object.assign({}, loaderOptions, { sourceMap: options.sourceMap }) }) } return loaders; } // https://vue-loader.vuejs.org/en/configurations/extract-css.html return { css: generateLoaders(), less: generateLoaders('less',{modifyVars:theme}), scss: generateLoaders('sass') } } // Generate loaders for standalone style files (outside of .vue) exports.styleLoaders = function (options) { const output = [] const loaders = exports.cssLoaders(options) for (const extension in loaders) { const loader = loaders[extension] output.push({ test: new RegExp('\\.' + extension + '$'), use: loader }) } return output } exports.createNotifierCallback = () => { const notifier = require('node-notifier') return (severity, errors) => { if (severity !== 'error') return const error = errors[0] const filename = error.file && error.file.split('!').pop() notifier.notify({ title: packageConfig.name, message: severity + ': ' + error.name, subtitle: filename || '', icon: path.join(__dirname, 'logo.png') }) } } ================================================ FILE: build/webpack.base.conf.js ================================================ 'use strict' const path = require('path') const utils = require('./utils') const config = require('../config') function resolve (dir) { return path.join(__dirname, '..', dir) } module.exports = { context: path.resolve(__dirname, '../'), entry: { app: './src/main.js' }, output: { path: config.build.assetsRoot, filename: '[name].js', publicPath: process.env.NODE_ENV === 'production' ? config.build.assetsPublicPath : config.dev.assetsPublicPath }, resolve: { alias:{ '@':path.join(__dirname,'../src') }, extensions: ['.js', '.jsx', '.json'] }, module: { rules: [ { test: /\.(js|jsx)$/, loader: 'babel-loader', exclude:/node_modules/ }, { test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, loader: 'url-loader', options: { limit: 10000, name: utils.assetsPath('img/[name].[hash:7].[ext]') } }, { test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, loader: 'url-loader', options: { limit: 10000, name: utils.assetsPath('media/[name].[hash:7].[ext]') } }, { test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, loader: 'url-loader', options: { limit: 10000, name: utils.assetsPath('fonts/[name].[hash:7].[ext]') } } ] }, node: { // prevent webpack from injecting useless setImmediate polyfill because Vue // source contains it (although only uses it if it's native). setImmediate: false, // prevent webpack from injecting mocks to Node native modules // that does not make sense for the client dgram: 'empty', fs: 'empty', net: 'empty', tls: 'empty', child_process: 'empty' } } ================================================ FILE: build/webpack.dev.conf.js ================================================ 'use strict' const utils = require('./utils') const webpack = require('webpack') const config = require('../config') const merge = require('webpack-merge') const path = require('path') const baseWebpackConfig = require('./webpack.base.conf') const CopyWebpackPlugin = require('copy-webpack-plugin') const HtmlWebpackPlugin = require('html-webpack-plugin') const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') const portfinder = require('portfinder') const HOST = process.env.HOST const PORT = process.env.PORT && Number(process.env.PORT) const devWebpackConfig = merge(baseWebpackConfig, { module: { rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true }) }, mode: config.dev.mode, // cheap-module-eval-source-map is faster for development devtool: config.dev.devtool, // these devServer options should be customized in /config/index.js devServer: { clientLogLevel: 'warning', historyApiFallback: { rewrites: [ { from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') }, ], }, hot: true, contentBase: false, // since we use CopyWebpackPlugin. compress: true, host: HOST || config.dev.host, port: PORT || config.dev.port, open: config.dev.autoOpenBrowser, overlay: config.dev.errorOverlay ? { warnings: false, errors: true } : false, publicPath: config.dev.assetsPublicPath, proxy: config.dev.proxyTable, quiet: true, // necessary for FriendlyErrorsPlugin watchOptions: { poll: config.dev.poll, } }, plugins: [ new webpack.HotModuleReplacementPlugin(), // https://github.com/ampedandwired/html-webpack-plugin new HtmlWebpackPlugin({ filename: 'index.html', template: 'index.html', inject: true }), // copy custom static assets new CopyWebpackPlugin([ { from: path.resolve(__dirname, '../static'), to: config.dev.assetsSubDirectory, ignore: ['.*'] } ]) ] }) module.exports = new Promise((resolve, reject) => { portfinder.basePort = process.env.PORT || config.dev.port portfinder.getPort((err, port) => { if (err) { reject(err) } else { // publish the new Port, necessary for e2e tests process.env.PORT = port // add port to devServer config devWebpackConfig.devServer.port = port // Add FriendlyErrorsPlugin devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({ compilationSuccessInfo: { messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`], }, onErrors: config.dev.notifyOnErrors ? utils.createNotifierCallback() : undefined })) resolve(devWebpackConfig) } }) }) ================================================ FILE: build/webpack.prod.conf.js ================================================ 'use strict' const path = require('path') const utils = require('./utils') const webpack = require('webpack') const config = require('../config') const merge = require('webpack-merge') const baseWebpackConfig = require('./webpack.base.conf') const CopyWebpackPlugin = require('copy-webpack-plugin') const HtmlWebpackPlugin = require('html-webpack-plugin') const MiniCssExtractPlugin = require("mini-css-extract-plugin") const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin') const UglifyJsPlugin = require('uglifyjs-webpack-plugin') const CleanWebpackPlugin = require('clean-webpack-plugin'); const webpackConfig = merge(baseWebpackConfig, { module: { rules: utils.styleLoaders({ sourceMap: config.build.productionSourceMap, usePostCSS: true }) }, mode: config.build.mode, devtool: config.build.productionSourceMap ? config.build.devtool : false, output: { path: config.build.assetsRoot, filename: utils.assetsPath('js/[name].[chunkhash].js'), chunkFilename: utils.assetsPath('js/[id].[chunkhash].js') }, optimization:{ splitChunks:{ chunks: 'all', minSize: 30000, minChunks: 1, maxAsyncRequests: 5, maxInitialRequests: 3, automaticNameDelimiter: '~', name: true, cacheGroups: { vendors: { test: /[\\/]node_modules[\\/]/, chunks: 'initial', reuseExistingChunk:true, priority: -10 } } }, runtimeChunk: { name: 'runtime' }, minimizer:[ new OptimizeCSSAssetsPlugin({ cssProcessorOptions: config.build.productionSourceMap ? { safe: true, map: { inline: false } } : { safe: true } }), new UglifyJsPlugin({ uglifyOptions: { compress: { warnings: false } }, cache:true, parallel: true, sourceMap: config.build.productionSourceMap }), ] }, plugins: [ new CleanWebpackPlugin(), // extract css into its own file new MiniCssExtractPlugin({ filename: utils.assetsPath('css/[name].[contenthash].css'), chunkFilename: utils.assetsPath('css/[id].css') }), // generate dist index.html with correct asset hash for caching. // you can customize output by editing /index.html // see https://github.com/ampedandwired/html-webpack-plugin new HtmlWebpackPlugin({ filename: config.build.index, template: 'index.html', inject: true, minify: { removeComments: true, collapseWhitespace: true, removeAttributeQuotes: true // more options: // https://github.com/kangax/html-minifier#options-quick-reference }, // necessary to consistently work with multiple chunks via CommonsChunkPlugin chunksSortMode: 'dependency' }), // keep module.id stable when vendor modules does not change new webpack.HashedModuleIdsPlugin(), // copy custom static assets new CopyWebpackPlugin([ { from: path.resolve(__dirname, '../static'), to: config.build.assetsSubDirectory, ignore: ['.*'] } ]) ] }) if (config.build.productionGzip) { const CompressionWebpackPlugin = require('compression-webpack-plugin') webpackConfig.plugins.push( new CompressionWebpackPlugin({ asset: '[path].gz[query]', algorithm: 'gzip', test: new RegExp( '\\.(' + config.build.productionGzipExtensions.join('|') + ')$' ), threshold: 10240, minRatio: 0.8 }) ) } if (config.build.bundleAnalyzerReport) { const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin webpackConfig.plugins.push(new BundleAnalyzerPlugin()) } module.exports = webpackConfig ================================================ FILE: config/dev.env.js ================================================ 'use strict' const merge = require('webpack-merge') const prodEnv = require('./prod.env') module.exports = merge(prodEnv, { NODE_ENV: '"development"' }) ================================================ FILE: config/index.js ================================================ 'use strict' // Template version: 1.3.1 // see http://vuejs-templates.github.io/webpack for documentation. const path = require('path') module.exports = { dev: { // Paths assetsSubDirectory: 'static', assetsPublicPath: '/', proxyTable: { '/api':'http://localhost:3000' }, // Various Dev Server settings host: 'localhost', // can be overwritten by process.env.HOST port: 8000, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined autoOpenBrowser: false, errorOverlay: true, notifyOnErrors: true, poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions- //https://webpack.js.org/concepts/mode/#mode-development mode:'development', /** * Source Maps */ // https://webpack.js.org/configuration/devtool/#development devtool: 'cheap-module-eval-source-map', // If you have problems debugging vue-files in devtools, // set this to false - it *may* help // https://vue-loader.vuejs.org/en/options.html#cachebusting cacheBusting: true, cssSourceMap: true }, build: { // Template for index.html index: path.resolve(__dirname, '../dist/index.html'), // Paths assetsRoot: path.resolve(__dirname, '../dist'), assetsSubDirectory: 'static', assetsPublicPath: '/', //https://webpack.js.org/concepts/mode/#mode-production mode:'production', /** * Source Maps */ productionSourceMap: true, // https://webpack.js.org/configuration/devtool/#production devtool: '#source-map', // Gzip off by default as many popular static hosts such as // Surge or Netlify already gzip all static assets for you. // Before setting to `true`, make sure to: // npm install --save-dev compression-webpack-plugin productionGzip: false, productionGzipExtensions: ['js', 'css'], // Run the build command with an extra argument to // View the bundle analyzer report after build finishes: // `npm run build --report` // Set to `true` or `false` to always turn it on or off bundleAnalyzerReport: process.env.npm_config_report } } ================================================ FILE: config/prod.env.js ================================================ 'use strict' module.exports = { NODE_ENV: '"production"' } ================================================ FILE: index.html ================================================ 仿QQ音乐 - 中国最新最全免费正版高品质音乐平台!
================================================ FILE: package.json ================================================ { "name": "sword", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "dev": "node_modules/.bin/webpack-dev-server --inline --progress --config build/webpack.dev.conf.js", "build": "node build/build.js", "clean": "rm -rf ./dist" }, "author": "wuming", "license": "ISC", "devDependencies": { "@babel/core": "^7.3.4", "@babel/plugin-proposal-class-properties": "^7.3.4", "@babel/plugin-proposal-decorators": "^7.3.0", "@babel/plugin-proposal-export-namespace-from": "^7.2.0", "@babel/plugin-proposal-function-sent": "^7.2.0", "@babel/plugin-proposal-json-strings": "^7.2.0", "@babel/plugin-proposal-numeric-separator": "^7.2.0", "@babel/plugin-proposal-throw-expressions": "^7.2.0", "@babel/plugin-syntax-dynamic-import": "^7.2.0", "@babel/plugin-syntax-import-meta": "^7.2.0", "@babel/plugin-transform-async-to-generator": "^7.3.4", "@babel/plugin-transform-runtime": "^7.3.4", "@babel/polyfill": "^7.2.5", "@babel/preset-env": "^7.3.4", "@babel/preset-react": "^7.0.0", "@babel/preset-stage-2": "^7.0.0", "@babel/runtime": "^7.3.4", "autoprefixer": "^7.1.2", "babel-loader": "^8.0.5", "babel-plugin-import": "^1.11.0", "clean-webpack-plugin": "^0.1.16", "copy-webpack-plugin": "^5.0.0", "css-loader": "^0.28.4", "file-loader": "^3.0.1", "friendly-errors-webpack-plugin": "^1.7.0", "html-webpack-plugin": "^3.2.0", "image-webpack-loader": "^3.3.1", "less": "^2.7.2", "less-loader": "^4.0.5", "mini-css-extract-plugin": "^0.5.0", "node-notifier": "^5.4.0", "node-sass": "^4.11.0", "optimize-css-assets-webpack-plugin": "^5.0.1", "ora": "^3.2.0", "postcss-import": "^12.0.1", "postcss-loader": "^3.0.0", "postcss-pxtorem": "^4.0.1", "postcss-url": "^8.0.0", "react-transition-group": "^2.2.0", "sass-loader": "^7.1.0", "shelljs": "^0.8.3", "style-loader": "^0.18.2", "svg-sprite-loader": "^0.3.1", "uglifyjs-webpack-plugin": "^2.1.2", "url-loader": "^1.1.2", "webpack": "^4.29.6", "webpack-bundle-analyzer": "^2.13.1", "webpack-cli": "^3.2.3", "webpack-dev-server": "^3.2.1", "webpack-merge": "^4.2.1" }, "dependencies": { "antd-mobile": "^2.2.9", "axios": "^0.16.2", "lodash": "^4.17.11", "qs": "^6.6.0", "rc-form": "^2.4.3", "react": "^16.8.4", "react-dom": "^16.8.4", "react-loadable": "^5.5.0", "react-redux": "^6.0.1", "react-router": "^4.3.1", "react-router-dom": "^4.3.1", "redux": "^4.0.1", "redux-thunk": "^2.3.0" }, "engines": { "node": ">= 6.0.0", "npm": ">= 3.0.0" } } ================================================ FILE: src/App.js ================================================ import React from 'react'; import {Router} from 'react-router-dom'; import {Switch, Route ,Redirect} from 'react-router'; import {history,routes} from '@/router'; function getRouterByRoutes(routes){ const renderedRoutesList = []; const renderRoutes = (routes,parentPath)=>{ Array.isArray(routes)&&routes.forEach((route)=>{ const {path,redirect,children,layout,component} = route; if(redirect){ renderedRoutesList.push() } if(component){ renderedRoutesList.push( layout?React.createElement(layout,props,React.createElement(component,props))} />: ) } if(Array.isArray(children)&&children.length>0){ renderRoutes(children,path) } }); } renderRoutes(routes,'') return renderedRoutesList; } class App extends React.PureComponent{ render(){ return ( {getRouterByRoutes(routes)} ) } } export default App; ================================================ FILE: src/api/index.js ================================================ import {keys} from 'lodash' import http from '@/utils/http' import API_URL from './url'; function mapUrlObjToFuncObj(urlObj){ const API = {}; keys(urlObj).forEach((key)=>{ const item = urlObj[key]; API[key]=function(params){ return http[item.method](item.url,params,item.option); } }); return API; } function mapUrlObjToStrObj(urlObj){ const Url = {}; keys(urlObj).forEach((key)=>{ const item = urlObj[key]; Url[key]=item.url; }); return Url; } export const API = mapUrlObjToFuncObj(API_URL); export const URL = mapUrlObjToStrObj(API_URL); ================================================ FILE: src/api/url.js ================================================ import Qs from 'qs' export default { //获取音乐播放链接 getMusicUrl:{ method:'get', url:'https://api.mlwei.com/music/api/?key=523077333&cache=0&type=song' }, //获取音乐歌词 getMusicLyric:{ method:'get', url:'https://api.mlwei.com/music/api/?key=523077333&cache=0&type=lrc' }, //查询音乐 queryMusic:{ method:'get', url:'https://api.mlwei.com/music/api/?key=523077333&cache=0&type=so' } } ================================================ FILE: src/components/Bandstand/index.js ================================================ import React from 'react'; import {bindActionCreators} from 'redux'; import classnames from 'classnames'; import Control from '@/components/Control'; import MusicList from '@/components/MusicList'; import * as actions from '@/store/actions'; import { connect } from 'react-redux'; import { Toast } from 'antd-mobile'; import {API} from '@/api'; import playImg from '@/assets/icon-music-play.png'; import pauseImg from '@/assets/icon-music-pause.png'; import playListImg from '@/assets/icon-play-list.png'; import "./style.scss"; @connect( (state)=>state.global, (dispatch)=>bindActionCreators(actions,dispatch) ) class Bandstand extends React.Component { state={ isMusicListShow: false, isControlShow: false, currentMusicUrl: '', currentSeconds:0, totalSeconds:0 } //改变播放状态 changePlayState=()=>{ const {changePlayStatus,isPlay,musicList} = this.props; if (musicList.length > 0) { changePlayStatus(!isPlay); if (!isPlay) { this.qqmusicAudio.play(); } else { this.qqmusicAudio.pause(); } } else { Toast.info('暂无可播放的音乐', 1); } } //根据歌曲id获取音乐url getMusicById(id, callback) { API.getMusicUrl({ id }).then((data)=>{ if (typeof callback === 'function') { callback(data); } }) } consoleSwitch=()=>{ this.setState({ isControlShow: !this.state.isControlShow }); } changeCurrentTime=(seconds)=>{ this.setState({ currentSeconds:seconds }); this.qqmusicAudio.currentTime=seconds; } musicListSwitch=()=>{ const {musicList} = this.props; const {isMusicListShow} = this.state; if (musicList.length > 0||isMusicListShow) { this.setState({ isMusicListShow: !isMusicListShow }); } else { Toast.info('暂无可播放的音乐', 1); } } componentDidMount(){ const {musicList,currentMusic,changePlayStatus,playSpecificMusicByMid,addAndChangeMusic} = this.props; this.getMusicById('000cwwze4FkFj4',(data)=>{ addAndChangeMusic(data,false); }); this.qqmusicAudio.oncanplay=()=>{ this.qqmusicAudio&&this.setState({ totalSeconds:this.qqmusicAudio.duration }); }; this.qqmusicAudio.ontimeupdate=()=>{ this.qqmusicAudio&&this.setState({ currentSeconds:this.qqmusicAudio.currentTime }); }; this.qqmusicAudio.onended=()=>{ if (musicList.length === 1) { changePlayStatus(false) } else { const currentIndex = musicList.findIndex((music)=>music.mid===currentMusic.mid) if(currentIndex

{title}

{author}

QQ音乐 听我想听的歌

) } } export default Bandstand; ================================================ FILE: src/components/Bandstand/style.scss ================================================ .qqmusic-home-footer { position: relative; display: flex; align-items: center; height: 65px; background: #fff; .left { .avatar { width: 50px; height: 50px; border-radius: 50%; margin-left: 10px; &.active { animation:animationRotate 10s linear infinite; } } } .center { padding-left: 15px; max-width: 200px; .song { font-size: 17px; color: #000; font-weight: 400; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } .singer { font-size: 13px; color: #6F6F6F; margin-top: 5px; } } .no-music{ display: none; position: absolute; top:0; left: 0; right: 100px; bottom: 0; padding-left: 15px; line-height: 65px; font-size: 15px; background-color: #fff; color: #373737; &.show{ display: block; } } .right { flex: 1; text-align: right; .qqmusic-play-switch { width: 35px; height: 35px; margin-right: 20px; } .qqmusic-play-list { width: 30px; height: 35px; margin-right: 15px; } } } // 动画 @keyframes animationRotate { from { transform: rotate(0); } to { transform: rotate(360deg); } } ================================================ FILE: src/components/Control/index.js ================================================ import React from 'react'; import { connect } from 'react-redux'; import {bindActionCreators} from 'redux'; import classnames from 'classnames'; import { Carousel } from 'antd-mobile'; import * as actions from '@/store/actions'; import utils from '@/utils'; import './style.scss'; import { API } from '@/api'; @connect( (state)=>state.global, (dispatch)=>bindActionCreators(actions,dispatch) ) class Control extends React.Component { state={ oldSongId:'', lyricArray: [], currentLyricIndex:0 } static getDerivedStateFromProps(nextProps,prevState){ const {lyricArray,currentLyricIndex} = prevState; const {currentSeconds} = nextProps; const newLyricIndex = lyricArray.findIndex((item)=>Math.abs(item.seconds-currentSeconds)<0.2); return { currentLyricIndex:newLyricIndex >-1?newLyricIndex:currentLyricIndex } } //播放上一首 prevMusic=()=>{ const {musicList,currentMusic={},playSpecificMusicByMid} = this.props; const currentIndex = musicList.findIndex((music)=>music.mid===currentMusic.mid); let nextMusic ; if (currentIndex > 0) { nextMusic = musicList[currentIndex - 1]; } else { nextMusic = musicList[musicList.length-1]; } playSpecificMusicByMid(nextMusic.mid); } //播放下一首 nextMusic=()=>{ const {musicList,currentMusic={},playSpecificMusicByMid} = this.props; const currentIndex = musicList.findIndex((music)=>music.mid===currentMusic.mid); let nextMusic ; if (currentIndex < musicList.length - 1) { nextMusic = musicList[currentIndex + 1]; } else { nextMusic = musicList[0]; } playSpecificMusicByMid(nextMusic.mid); } changePlayProgress=(event)=>{ const {totalSeconds} = this.props; let left = event.changedTouches[0].clientX - this.refs.progressParent.offsetLeft - event.target.offsetWidth / 2; let maxLeft = this.refs.progressParent.offsetWidth; let minLeft = 0; if (left < minLeft) { left = minLeft; } if (left > maxLeft) { left = maxLeft; } this.props.changeCurrentTime(left / maxLeft * totalSeconds); } componentDidUpdate(){ const {currentMusic={}} = this.props; const {oldSongId,currentLyricIndex} =this.state; const {mid} = currentMusic; if(mid&&oldSongId!==mid){ API.getMusicLyric({ id:mid }).then((response)=>{ this.setState({ oldSongId:mid, lyricArray:response.split('\n').map((item)=>{ const matchTimestamp = item.match(/\[.+\]/)[0]; return { seconds:isNaN(utils.parseStrToSeconds(matchTimestamp))?0:utils.parseStrToSeconds(matchTimestamp), text:item.split('').filter((char)=>matchTimestamp.indexOf(char)<0).join('') } }).filter((item)=>item.text.length>0) }); }); } this.lyricDom.scrollTop=currentLyricIndex*40; } render() { const {lyricArray,currentLyricIndex} = this.state; const {currentMusic={},isPlay,changePlayState,currentSeconds,totalSeconds,isControlShow,consoleSwitch} = this.props; const {title,author,pic} = currentMusic; return (

{title}

{ [ (

{author}

), (
    this.lyricDom=dom} className="lyricList"> { lyricArray.map((item, index) => { return (
  • {item.text}
  • ) }) }
) ] }
{utils.formatSeconds(currentSeconds)}
{utils.formatSeconds(totalSeconds)}
) } } export default Control; ================================================ FILE: src/components/Control/style.scss ================================================ .qqmusic-control { position: fixed; top: 0; left: 0; width: 100%; height: 100%; transform: translateY(100%); transition: transform 0.1s ease-out; background-color: #fff; &.show { transform: translateY(0); z-index: 1; .qqmusic-control-content { display: flex; flex-direction: column; position: absolute; top: 0; bottom: 0; left: 0; right: 0; z-index: 3; overflow: auto; .qqmusic-control-top { position: relative; flex: 0 0 50px; .icon-control-down { position: absolute; left: 15px; top: 10px; width: 25px; } .music-name { text-align: center; font-size: 20px; color: #fff; padding-top: 15px; } } .qqmusic-control-middle { text-align: center; flex: 1; .slider-list{ min-height: 370px !important; } .carousel-one { .music-signer { text-align: center; font-size: 16px; color: #fff; padding-top: 15px; } .music-signer:before { content: ''; display: inline-block; width: 20px; border-top: 1px solid #fff; vertical-align: 6px; margin-right: 10px; } .music-signer:after { content: ''; display: inline-block; width: 20px; border-top: 1px solid #fff; vertical-align: 6px; margin-left: 10px; } .music-cover { width: 210px; height: 210px; border-radius: 50%; margin-top: 60px; } } .carousel-two { position: relative; height: 100%; box-sizing: border-box; padding-top: 10px; .lyricList { overflow: auto; height: 315px; box-sizing: border-box; padding-top: 130px; .lyric { color: #8a8a8a; font-size: 13px; text-align: center; line-height: 40px; &.active { color: #31c37c; } } } } &.active { .music-cover { animation: animationRotate 10s linear infinite; } } } .qqmusic-control-bottom { flex: 0 0 200px; box-sizing: border-box; padding-bottom: 20px; .qqmusic-control-progress { position: relative; margin-top: 15px; text-align: center; .currentPlayTime, .totalPlayTime { position: absolute; color: #8a8a8a; font-size: 12px; } .currentPlayTime { left: 25px; top: 7px; } .totalPlayTime { right: 25px; top: 7px; } .progress-wrapper { position: relative; display: inline-block; width: 200px; height: 1px; background-color: #e6e6e6; .progress-inner { height: 1px; background-color: #31c37c; } .progress-btn { position: absolute; top: -8px; left: 0; width: 15px; height: 15px; border-radius: 50%; background-color: #31c37c; } } } .qqmusic-control-btns { display: flex; height: 90px; align-items: center; justify-content: center; margin-top: 25px; .status { width: 70px; height: 70px; margin: 0 15px; } .prev, .next { width: 40px; height: 40px; } } } } .qqmusic-control-bg { position: absolute; top: 0; bottom: 0; left: 0; right: 0; background-size: cover; background-position: bottom center; z-index: 1; -webkit-filter: blur(15px); -webkit-transform: scale(1.15); } .qqmusic-control-bg-mask { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: #000; z-index: 2; opacity: 0.6; } } } // 动画 @keyframes animationRotate { from { transform: rotate(0); } to { transform: rotate(360deg); } } ================================================ FILE: src/components/Header/index.js ================================================ import React from 'react'; import { NavLink } from 'react-router-dom'; import { Popover, Toast } from 'antd-mobile'; import './style.scss'; import Slider from '@/components/Slider'; import Search from '@/components/Search'; const Item = Popover.Item; class Header extends React.Component { state = { docked: false, search: false, popover:false } openChange=()=>{ const {docked} = this.state; this.setState({ docked: !docked }); } searchChange=()=>{ const {search} = this.state; this.setState({ search: !search }); } popoverChange=(visible)=>{ this.setState({ popover:visible }); } popoverSelect=(options)=>{ if(options.key==="1"){ Toast.offline('听歌识曲功能未开放', 1); }else if(options.key==="2"){ Toast.offline('扫一扫功能未开放', 1); } this.setState({ popover:false }); } render(){ const {popover,docked,search} = this.state; const {className} = this.props; return (
我的 音乐馆 发现 听歌识曲), (扫一扫), ]} onVisibleChange={this.popoverChange} onSelect={this.popoverSelect} >
搜索
); } } export default Header; ================================================ FILE: src/components/Header/style.scss ================================================ // 头部 $headerFontColor:#dbf3e8; $headerMainBackgroundColor:#31c37c; $headerSearchBackgroundColor:#2AAA73; @mixin flexCenter () { display: flex; align-items: center; justify-content: center; } .qqmusic-header { background-color: $headerMainBackgroundColor; .top { position: relative; @include flexCenter(); height: 40px; .qqmusic-tab { font-size: 20px; color: $headerFontColor; padding: 0 10px; font-weight: 300; } .qqmusic-tab-active { font-weight: 700; } .icon-left { position: absolute; left: 12px; top: 3px; width: 28px; height: 35px; background: url('../../assets/icon-menu-list.png'); background-size: 28px 35px; } .icon-right { position: absolute; right: 12px; top: 6px; width: 28px; height: 28px; background: url('../../assets/icon-menu-add.png'); background-size: 28px 28px; } } .bottom { padding: 7px 10px; .search { @include flexCenter(); height: 30px; width: 100%; background-color: $headerSearchBackgroundColor; border-radius: 5px; } .text { margin-left: 5px; font-size: 17px; font-style: normal; color: $headerFontColor; font-weight: 300; } .search-icon { display: inline-block; background: url('../../assets/icon-search-default.png'); height: 18px; width: 18px; background-size: 18px 18px; } } } .popover-item-img{ width: 20px; height: 20px; } .popover-item-text{ padding-left: 10px; font-size: 17px; vertical-align: 4px; } ================================================ FILE: src/components/Loading/index.js ================================================ import React from 'react'; import './style.scss'; export default function loading() { return (
) } ================================================ FILE: src/components/Loading/style.scss ================================================ @import '@/scss/variable.scss'; .comp-loading { display: flex; justify-content: center; align-items: center; height: 100vh; overflow: hidden; animation-delay: 1s; .item-1 { width: 20px; height: 20px; border-radius: 50%; background-color: $primary-color; margin: 7px; display: flex; justify-content: center; align-items: center; } @keyframes scale { 0% { transform: scale(1); } 50%, 75% { transform: scale(2.5); } 78%, 100% { opacity: 0; } } .item-1:before { content: ''; width: 20px; height: 20px; border-radius: 50%; background-color: $primary-color; opacity: 0.7; animation: scale 2s infinite cubic-bezier(0, 0, 0.49, 1.02); animation-delay: 200ms; transition: 0.5s all ease; transform: scale(1); } .item-2 { width: 20px; height: 20px; border-radius: 50%; background-color: $primary-color; margin: 7px; display: flex; justify-content: center; align-items: center; } @keyframes scale { 0% { transform: scale(1); } 50%, 75% { transform: scale(2.5); } 78%, 100% { opacity: 0; } } .item-2:before { content: ''; width: 20px; height: 20px; border-radius: 50%; background-color: $primary-color; opacity: 0.7; animation: scale 2s infinite cubic-bezier(0, 0, 0.49, 1.02); animation-delay: 400ms; transition: 0.5s all ease; transform: scale(1); } .item-3 { width: 20px; height: 20px; border-radius: 50%; background-color: $primary-color; margin: 7px; display: flex; justify-content: center; align-items: center; } @keyframes scale { 0% { transform: scale(1); } 50%, 75% { transform: scale(2.5); } 78%, 100% { opacity: 0; } } .item-3:before { content: ''; width: 20px; height: 20px; border-radius: 50%; background-color: $primary-color; opacity: 0.7; animation: scale 2s infinite cubic-bezier(0, 0, 0.49, 1.02); animation-delay: 600ms; transition: 0.5s all ease; transform: scale(1); } .item-4 { width: 20px; height: 20px; border-radius: 50%; background-color: $primary-color; margin: 7px; display: flex; justify-content: center; align-items: center; } @keyframes scale { 0% { transform: scale(1); } 50%, 75% { transform: scale(2.5); } 78%, 100% { opacity: 0; } } .item-4:before { content: ''; width: 20px; height: 20px; border-radius: 50%; background-color: $primary-color; opacity: 0.7; animation: scale 2s infinite cubic-bezier(0, 0, 0.49, 1.02); animation-delay: 800ms; transition: 0.5s all ease; transform: scale(1); } .item-5 { width: 20px; height: 20px; border-radius: 50%; background-color: $primary-color; margin: 7px; display: flex; justify-content: center; align-items: center; } @keyframes scale { 0% { transform: scale(1); } 50%, 75% { transform: scale(2.5); } 78%, 100% { opacity: 0; } } .item-5:before { content: ''; width: 20px; height: 20px; border-radius: 50%; background-color: $primary-color; opacity: 0.7; animation: scale 2s infinite cubic-bezier(0, 0, 0.49, 1.02); animation-delay: 1000ms; transition: 0.5s all ease; transform: scale(1); } } ================================================ FILE: src/components/MusicList/index.js ================================================ import React from 'react'; import { connect } from 'react-redux'; import {bindActionCreators} from 'redux'; import classnames from 'classnames'; import * as actions from '@/store/actions'; import './style.scss'; @connect( (state)=>state.global, (dispatch)=>bindActionCreators(actions,dispatch) ) class MusicList extends React.Component{ playSpecificMusic(music){ const {mid} = music; const {playSpecificMusicByMid} = this.props playSpecificMusicByMid(mid); } clearMusicList=()=>{ const {clearMusicList} = this.props; clearMusicList(); } removeMusicFromList(music){ const {mid} = music; const {removeMusicFromList} = this.props removeMusicFromList(mid); } componentWillReceiveProps(nextProps) { if(nextProps.musicList.length===0){ this.props.musicListSwitch(); } } render(){ const {currentMusic,mid,musicList,isMusicListShow,musicListSwitch} = this.props; return (

播放列表

    { musicList.map((item)=>{ return (
  • {item.title} - {item.author}
  • ); }) }
关闭
) } } export default MusicList; ================================================ FILE: src/components/MusicList/style.scss ================================================ .qqmusic-music-list-wrapper{ .qqmusic-music-list-bg{ position: fixed; top:0; right:0; bottom: 0; left:0; background: rgba( 0,0,0,0.4); opacity: 0; z-index: -1; transition: opacity 0.14s linear; } .qqmusic-music-list-content{ display: flex; flex-direction: column; position: fixed; left:0; bottom:0; width: 100%; height: 60%; z-index: 2; background-color:rgba(31,37,47,0.9); transform: translateY(100%); transition: transform 0.14s linear; .top{ position: relative; flex:0 0 50px; .title{ font-size: 17px; color: #fff; font-weight: 300; line-height: 50px; padding-left: 15px; } .clear-list{ position: absolute; top: 15px; right: 25px; width: 20px; } } .middle{ flex: 1; overflow: auto; .music-list{ .music-item{ position: relative; height: 40px; line-height: 40px; padding: 0 15px; font-size: 15px; .tag{ margin-left: 30px; height: 15px; } .delete{ position: absolute; height: 15px; right: 15px; top: 14px; } } } } .bottom{ color: #fff; font-size: 16px; text-align: center; flex:0 0 50px; line-height: 50px; } } &.show{ position: fixed; top:0; right: 0; bottom: 0; left:0; z-index: 3; .qqmusic-music-list-bg{ opacity: 1; z-index: 1; } .qqmusic-music-list-content{ transform: translateY(0); } } } ================================================ FILE: src/components/NewSongMenu/index.js ================================================ import React from 'react'; import { connect } from 'react-redux'; import {bindActionCreators} from 'redux'; import classnames from 'classnames'; import * as actions from '@/store/actions'; import {NoticeBar,Toast} from 'antd-mobile'; import './style.scss'; @connect( (state)=>state.global, (dispatch)=>bindActionCreators(actions,dispatch) ) class NewSongMenu extends React.Component{ state={ totalCount:0, isErrorShow:false } comeback=()=>{ this.setState({ totalCount:0 }); this.inputText.value=""; this.props.newSongMenuShowSwitch(); } changeStrLength=()=>{ if(this.inputText.value.length<=20){ this.setState({ totalCount:this.inputText.value.length }); }else{ this.inputText.value=this.inputText.value.substring(0,20); this.setState({ isErrorShow:true }); setTimeout(()=>{ this.setState({ isErrorShow:false }); },1200); } } saveSongMenu=()=>{ const {addSongMenu,songMenuArray} = this.props; const isCanAdd=!songMenuArray.some((item)=>{ return item===this.inputText.value }); if(isCanAdd){ addSongMenu(this.inputText.value) this.comeback(); }else{ Toast.fail('该歌单已存在', 1); } } render(){ const {isNewSongMenuShow} = this.props; const {totalCount,isErrorShow} = this.state; return(

新建歌单

保存
this.inputText=input} className="input-text" type="text" placeholder="请输入内容" onInput={this.changeStrLength}/>

{20-totalCount}

!最多输入20个字
); } } export default NewSongMenu; ================================================ FILE: src/components/NewSongMenu/style.scss ================================================ .qqmusic-new-songmenu{ position: fixed; top:0; left: 0; width: 100%; height: 100%; transform: translateY(100%); transition: transform 0.1s linear; background-color: #f5f5f9; z-index: 2; &.show{ transform: translateY(0); } .new-songmenu-header{ background: #31c37c; height: 40px; position: relative; .icon-arrow-left{ position: absolute; left: 15px; top: 10px; height: 20px; } .title{ color: #fff; } .save{ position: absolute; right: 15px; top:0; color: #fff; } } .new-songmenu-body{ .input-text{ width: 100%; height: 30px; padding-left: 10px; font-size: 14px; background-color: #fff; vertical-align: top; color: #31c37c; text-shadow: -1px 0px 0px #373737; -webkit-text-fill-color: transparent; } .total-count{ text-align: right; padding-right: 10px; font-size: 14px; line-height: 16px; color: #00a1d6; } .error-notice{ position: fixed; left: 0; top:0; width: 100%; background-color: #d81e06; color: #eee; text-align: left; display: none; &.show{ display: block; } } } } ================================================ FILE: src/components/Search/index.js ================================================ import React from 'react'; import { connect } from 'react-redux'; import {bindActionCreators} from 'redux'; import classnames from 'classnames'; import { Toast } from 'antd-mobile'; import * as actions from '@/store/actions'; import { API } from '@/api'; import './style.scss'; @connect( (state)=>state.global, (dispatch)=>bindActionCreators(actions,dispatch) ) class Search extends React.Component { state = { recordList: [], songList: [], pageNo: 1, totalCount: 0, isCanGet: true, isSearch: true, isRemindDivShow:true } comeback=()=>{ document.getElementsByClassName('input-text')[0].value = ''; this.setState({ songList: [], pageNo: 1, totalCount: 0, isRemindDivShow:true }); this.props.searchChange.bind(this)(); } //监听键盘事件 keyboardListener=(event)=>{ if (event.keyCode === 13) { this.setState({ isRemindDivShow:false }); this.addSearchRecord(document.getElementsByClassName('input-text')[0].value); this.getSearhListAjax(); } } //搜索数据 getSearhListAjax=(event)=>{ const {isSearch,pageNo,isCanGet,songList} = this.state; this.setState({ isRemindDivShow:false }); this.addSearchRecord(document.getElementsByClassName('input-text')[0].value); let searchText = document.getElementsByClassName('input-text')[0].value; let offset = pageNo * 20; if (isCanGet) { this.setState({ isCanGet: false, }); if (isSearch) { this.setState({ songList: [] }); } API.queryMusic({ nu:offset, id:searchText }).then((response)=>{ const {Code,Body,songnum} = response; if(Code==='OK'){ this.setState({ isCanGet: true, totalCount: songnum, songList: isSearch ? Body : songList.concat(Body), isSearch: true }); }else{ Toast.fail('查询失败'); } }) } } //下拉加载 getMoreSearchList=(event)=>{ const {totalCount,songList,isCanGet,pageNo} = this.state; const scrollHeight = event.target.scrollHeight; const scrollTop = event.target.scrollTop; const clientHeight = event.target.clientHeight; if (scrollHeight - scrollTop - clientHeight < 10) { if (totalCount > songList.length) { if (isCanGet) { this.setState({ pageNo: pageNo + 1, isSearch: false }, function () { this.getSearhListAjax(); }); } } } } clearInput=()=>{ document.getElementsByClassName('input-text')[0].value = ''; this.setState({ songList: [], pageNo: 1, totalCount: 0, isRemindDivShow:true }); } //快捷搜索 fastSearch(searchText){ document.getElementsByClassName('input-text')[0].value = searchText; this.setState({ isRemindDivShow:false }); this.addSearchRecord(searchText); this.getSearhListAjax(); } //添加搜索记录 addSearchRecord(recordStr) { const {recordList} = this.state; const isCanAdd = !recordList.some((item) => { return item === recordStr; }); if (isCanAdd&&recordStr!=='') { recordList.unshift(recordStr); } this.setState({ recordList:[].concat(recordList) }); localStorage["yqq_search_history"] = recordList.join(","); } //移除记录 removeRecord(record) { const {recordList} = this.state; const newRecordList = recordList.filter((item) => { return record !== item; }); this.setState({ recordList:newRecordList }); localStorage["yqq_search_history"] = newRecordListv.join(","); } //清楚历史记录 clearRecord=()=>{ localStorage["yqq_search_history"]=""; this.setState({ recordList:[] }); } //往播放列表中添加音乐 addMusic(musicItem) { const {addAndChangeMusic} = this.props; addAndChangeMusic(musicItem,true); this.comeback(); } componentDidMount() { if (localStorage["yqq_search_history"]) { this.setState({ recordList: localStorage["yqq_search_history"].split(",") }); } } render() { const {songList,isCanGet,recordList,isRemindDivShow} = this.state; const {search} = this.props; const searchTextList=["邓紫棋","全孝盛","张靓颖","周杰伦","薛之谦","林俊杰"] return (
搜索

热门搜索

    { searchTextList.map((item,index)=>{ return (
  • {item}
  • ); }) }

0?'block':'none'}} className="title-search-history border-bottom">搜索历史清空历史

    { recordList.map((item,index) => { return (
  • {item}

  • ) }) }
    { songList.map((item, index) => { return (
  • {item.title}

    {item.author}

    {item.album}

    {item.album}
  • ) }) }
  • 正在加载更多...
) } } export default Search; ================================================ FILE: src/components/Search/style.scss ================================================ .qqmusic-search-wrapper { position: relative; display: flex; flex-direction: column; position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 1; background: #f5f5f9; transform: translateY(100%); transition: transform 0.2s ease-out; &.show { transform: translateY(0); } .qqmusic-search-top { position: relative; background: #31c37c; box-sizing: border-box; padding: 10px 80px 10px 40px; .icon-arrow-left { position: absolute; top: 10px; left: 10px; width: 20px; height: 20px; } .input-text { width: 100%; background: #31c37c; color: #dbf3e8; font-size: 15px; } .input-text::placeholder { color: #dbf3e8; opacity: 0.6; } .icon-input-clear { position: absolute; right: 50px; top: 7px; background: url("../../assets/icon-input-clear.png"); background-size: cover; width: 25px; height: 25px; } .btn-search { position: absolute; display: inline-block; right: 10px; top: 10px; color: #dbf3e8; } } .qqmusic-search-bottom { flex: 1; overflow: scroll; .qqmusic-search-list { background: #fff; .qqmusic-search-list-item { display: flex; padding: 5px 15px; .left{ flex: 1; .title { font-size: 15px; font-weight: 500; } .singer { font-size: 13px; padding: 5px 0; color: #707070; } .intro { font-size: 11px; line-height: 14px; color: #bfbfbf; } } .right{ width: 60px; .cover{ width: 100%; } } } .hint { text-align: center; line-height: 31px; font-size: 15px; background-color: #f5f5f9; } } } .remind-mask { position: absolute; top:40px; right:0; bottom: 0; left: 0; .search-text-list-wrapper{ padding: 15px; background: #fff; .title-hot-search{ color: #8a8a8a; padding: 15px 0; font-weight: 400; font-size: 15px; } .search-text-list{ .search-text-item{ display: inline-block; border-radius: 40px; border: 1px solid #515151; color: #373737; height: 30px; line-height: 30px; padding: 0 10px; margin: 5px; } } } .title-search-history{ position: relative; color: #8a8a8a; padding: 15px 0 15px 15px; font-weight: 400; font-size: 15px; background-color: #fff; .clean-record{ position: absolute; right: 15px; top: 15px; color: #31c37c; } } .record-list { .record-item { position: relative; background: #fff; height: 45px; line-height: 45px; padding-left: 60px; .icon-recent { position: absolute; left: 15px; top: 7px; background: url("../../assets/icon-record-recent.png") no-repeat; background-size: cover; width: 26px; height: 26px; } .icon-close { position: absolute; right: 15px; top: 12px; background: url("../../assets/icon-record-close.png") no-repeat; background-size: cover; width: 21px; height: 21px; } } } } } ================================================ FILE: src/components/Slider/index.js ================================================ import React from 'react'; import classnames from 'classnames'; import {Switch } from 'antd-mobile'; import { createForm } from 'rc-form'; import './style.scss'; class Slider extends React.Component { state={ wifi:false, timingClose:true } switchChange(type,checked){ console.log(type+":"+checked); } render() { const {docked} = this.props; let SwitchExample = (props) => { const { getFieldProps } = props.form; return ( { this.switchChange(props.type,checked); }} /> ) }; SwitchExample = createForm()(SwitchExample); const headerSliderList = [ { imgSrc: require('@/assets/icon-slider-message.png'), title: '升级为VIP', text: '畅享音乐特权' }, { imgSrc: require('@/assets/icon-slider-skin.png'), title: '个性化中心', text: '默认主题' }, { imgSrc: require('@/assets/icon-slider-vip.png'), title: '消息中心', text: '' } ]; const bodySliderList = [ { text:'仅Wi-Fi联网', extra: }, { text:'定时关闭', extra: }, { text:'免流量服务', extra:null }, { text:'微云音乐网盘', extra:null }, { text:'传歌到手机', extra:null }, { text:'QPlay与车载音乐', extra:null }, { text:'清理占用空间', extra:null }, { text:'免费WIFI', extra:null }, { text:'帮助与反馈', extra:null }, { text:'关于QQ音乐', extra:null } ]; return (
{ headerSliderList.map(function (item, index) { return (

{item.title}

{item.text}

) }) }
    { bodySliderList.map(function (item, index) { return (
  • {item.text}{item.extra}
  • ) }) }
设置
退出登录/关闭
) } } export default Slider; ================================================ FILE: src/components/Slider/style.scss ================================================ // 菜单浮层 .qqmusic-slider-bg { position: fixed; left: 0; top: 0; right: 0; bottom: 0; background-color: rgba(0, 0, 0, 0.7); z-index: 2016; display: none; &.open { display: block; } } .qqmusic-slider { display: flex; flex-direction: column; position: fixed; left: 0; top: 0; bottom: 0; width: 80%; box-sizing: border-box; padding: 0 15px; background-color: #fff; transform: translateX(-100%); transition: transform 0.3s ease-out; z-index: 2017; &.open { transform: translateX(0); } .qqmusic-slider-header { display: flex; flex: 0 0 125px; padding-bottom: 20px; .qqmusic-slider-header-Item { flex: 1; text-align: center; .qqmusic-slider-header-Item-img { width: 35px; height: 35px; margin: 20px 0; } .qqmusic-slider-header-Item-title { font-size: 14px; font-weight: 400; } .qqmusic-slider-header-Item-text { font-size: 11px; font-weight: 300; margin: 5px 0; color: #c7c7c7; } } } .qqmusic-slider-body { flex: 1; -webkit-overflow-scrolling: touch; overflow: auto; .qqmusic-slider-body-item{ position: relative; height: 45px; .qqmusic-slider-body-item-text{ display: block; font-size: 14px; line-height: 45px; } .qqmusic-slider-body-item-extra{ position: absolute; right: 10px; top:5px; } } } .qqmusic-slider-footer { display: flex; flex: 0 0 50px; .qqmusic-slider-footer-left,.qqmusic-slider-footer-right{ flex: 1; display: flex; align-items: center; } .qqmusic-slider-footer-left{ .qqmusic-slider-footer-icon { display: inline-block; width: 20px; height: 20px; background: url('../../assets/icon-slider-setting.png'); background-size: 20px, 20px; } } .qqmusic-slider-footer-right{ justify-content: flex-end; .qqmusic-slider-footer-icon { display: inline-block; width: 15px; height: 15px; background: url('../../assets/icon-slider-exit.png'); background-size: 15px, 15px; } } .qqmusic-slider-footer-text { margin-left:5px; font-size: 14px; line-height: 25px; } } } ================================================ FILE: src/components/SongMenu/index.js ================================================ import React from 'react'; import { connect } from 'react-redux'; import {bindActionCreators} from 'redux'; import classnames from 'classnames'; import addImg from '@/assets/icon-songmenu-add.png'; import NewSongMenu from '@/components/NewSongMenu'; import SongMenuMangement from '@/components/SongMenuMangement'; import * as actions from '@/store/actions'; import './style.scss'; @connect( (state)=>state.global, (dispatch)=>bindActionCreators(actions,dispatch) ) class SongMenu extends React.Component { state={ isNewSongMenuShow:false, isSongMenuMangementShow:false, activeTab:1 } newSongMenuShowSwitch=()=>{ const {isNewSongMenuShow} = this.state; this.setState({ isNewSongMenuShow:!isNewSongMenuShow }); } songMenuMangementShowSwitch=()=>{ const {isSongMenuMangementShow} = this.state; this.setState({ isSongMenuMangementShow:!isSongMenuMangementShow }); } tabChange(tabIndex){ this.setState({ activeTab:tabIndex }); } render() { const {activeTab,isNewSongMenuShow,isSongMenuMangementShow} = this.state; const {songMenuArray} = this.props; return (
自建歌单 | 收藏歌单
    0?{display:'block'}:{display:'none'}}> { songMenuArray.map((item,index)=>{ return (
  • {item}

    0首

  • ) }) }
0?'none':'flex'}} onClick={this.newSongMenuShowSwitch}>

新建歌单

没有收藏的歌单

); } } export default SongMenu; ================================================ FILE: src/components/SongMenu/style.scss ================================================ .qqmusic-mycenter-bottom { text-align: center; margin-top: 5px; background: #fff; line-height: 40px; color: #8a8a8a; .qqmusic-mycenter-tabs { position: relative; .qqmusic-mycenter-tab { padding: 0 10px; &.active { color: #000; } } .add-songmenu { position: absolute; right: 40px; top: 11px; display: inline-block; height: 20px; width: 20px; background: url('../../assets/icon-songmenu-add.png'); background-size: 20px 20px; } .songmenu-manage { position: absolute; right: 10px; top: 12px; display: inline-block; height: 20px; width: 20px; background: url('../../assets/icon-songmenu.png'); background-size: 20px 20px; .no-collected-songMenu { font-size: 15px; text-align: center; color: #cdcdcd; } } } .qqmusic-mycenter-tab-content-one { background: #fff; min-height: 90px; .songmenu-array { .songmenu-item { display: flex; height: 60px; margin-top: 1px; .left { flex: 0 0 60px; background: #e6e6e6; display: flex; align-items: center; justify-content: center; .logo { width: 25px; height: 25px; } } .right { position: relative; flex: 1; padding-left: 10px; .name, .num { font-size: 15px; line-height: 30px; text-align: left; padding-left: 15px; } .icon-right { position: absolute; top: 22px; right: 15px; background: url("../../assets/icon-easy-right.png"); background-size: cover; height: 17px; width: 17px; } } } } .add-songmenu-wrapper { display: flex; height: 60px; .add-songmenu-wrapper-left { flex: 0 0 60px; background: #e6e6e6; display: flex; align-items: center; justify-content: center; .add-songmenu-img { width: 30px; height: 30px; } } .add-songmenu-wrapper-right { flex: 1; .add-songmenu-text { line-height: 60px; text-align: left; margin-left: 10px; } } } } .qqmusic-mycenter-tab-content-two { background: #fff; min-height: 60px; } } ================================================ FILE: src/components/SongMenuMangement/index.js ================================================ import React from 'react'; import { connect } from 'react-redux'; import {bindActionCreators} from 'redux'; import {Checkbox} from 'antd-mobile'; import * as actions from '@/store/actions'; import './style.scss'; const CheckboxItem = Checkbox.CheckboxItem; @connect( (state)=>state.global, (dispatch)=>bindActionCreators(actions,dispatch) ) class SongMenuMangement extends React.Component{ state={ selectedList:[] } comeback=()=>{ this.props.songMenuMangementShowSwitch(); } changeSelectedList(text){ const isCanAdd=!this.state.selectedList.some((item)=>{ return text===item; }); let selectedList=this.state.selectedList; if(isCanAdd){ selectedList.push(text); }else{ selectedList=selectedList.filter((item)=>{ return text!==item; }) } this.setState({ selectedList }); } removeSongMenu=()=>{ const {selectedList} = this.state; const {removeSongMenu} = this.props; removeSongMenu(selectedList); } render(){ const {songMenuArray,isSongMenuMangementShow} = this.props; return (

管理自建歌单

    { songMenuArray.map((item,index)=>{ return (
  • {item}

    0首

  • ) }) }

删除

) } } export default SongMenuMangement; ================================================ FILE: src/components/SongMenuMangement/style.scss ================================================ .qqmusic-songmenu-mangement{ display: flex; flex-direction: column; position: fixed; top:0; left: 0; width: 100%; height: 100%; transform: translateX(100%); transition: transform 0.1s linear; background-color: #f5f5f9; z-index: 2; &.show{ transform: translateX(0); } .songmenu-mangement-header{ background: #31c37c; height:0 0 40px; position: relative; .icon-arrow-left{ position: absolute; left: 15px; top:10px; height: 20px; } .title{ color: #fff; } } .songmenu-mangement-body{ flex: 1; overflow: auto; .songmenu-array{ .songmenu-item{ display: flex; height: 60px; margin-top: 4px; background-color: #fff; .left{ flex: 0 0 50px; display: flex; align-items: center; justify-content: center; .checkBox{ height: 100%; .am-checkbox.am-checkbox-checked .am-checkbox-inner{ border: 1px solid #31c37c; background-color: #31c37c; } .am-checkbox.am-checkbox-checked .am-checkbox-inner:after{ border-color: #fff; } } } .middle{ flex:0 0 60px; background: #e6e6e6; display: flex; align-items: center; justify-content: center; .logo{ width: 25px; height:25px; } } .right{ position: relative; flex: 1; .name,.num{ padding-left: 10px; font-size: 15px; line-height: 30px; text-align: left; padding-left: 15px; } } } } } .songmenu-mangement-footer{ flex: 0 0 75px; background: #fff; text-align: center; line-height: 25px; box-sizing: border-box; padding-top: 10px; .delete-wrapper{ .delete{ width: 25px; } } .text-wrapper{ text-align: center; .text{ font-size: 14px; } } } } ================================================ FILE: src/constant/music.js ================================================ //播放或暂停音乐 export const CHANGE_MUSIC_STATUS="CHANGE_MUSIC_STATUS"; //添加音乐到播放列表 export const ADD_MUSIC="ADD_MUSIC"; //更改当前音乐 export const CHANGE_CURRENT_MUSIC="CHANGE_CURRENT_MUSIC"; //添加并更改当前音乐 export const ADD_AND_CHANGE_MUSIC="ADD_AND_CHANGE_MUSIC"; //根据下标播放指定歌曲 export const PLAY_MUSIC_BY_INDEX="PLAY_MUSIC_BY_INDEX"; //清空播放列表 export const CLEAR_MUSIC_LIST="CLEAR_MUSIC_LIST"; //移除指定的音乐 export const REMOVE_MUSIC_FROM_LIST="REMOVE_MUSIC_FROM_LIST"; //添加歌单 export const ADD_SONG_LIST="ADD_SONG_LIST"; //删除歌单 export const REMOVE_SONG_LIST="REMOVE_SONG_LIST"; ================================================ FILE: src/layouts/MainLayout/index.js ================================================ import React from 'react'; import Header from '@/components/Header'; import Bandstand from '@/components/Bandstand'; import './style.scss'; class MainLayout extends React.Component { render() { const {children} = this.props; return (
{children}
); } } export default MainLayout; ================================================ FILE: src/layouts/MainLayout/style.scss ================================================ .qqmusic-home{ display: flex; flex-direction: column; height: 100%; } .qqmusic-home-body{ flex: 1; overflow: scroll; } ================================================ FILE: src/main.js ================================================ /** * Created by wuming on 2017/7/11. */ // import '@/utils/antm-viewport.min'; import '@/scss/reset.scss'; import React from 'react'; import { render } from 'react-dom'; import { Provider } from 'react-redux'; import App from './App'; import configureStore from '@/store'; import '@babel/polyfill'; const store = configureStore(); render( , document.getElementById("app") ); ================================================ FILE: src/pages/Discovery/index.js ================================================ import React from 'react'; import './style.scss'; class Discovery extends React.Component { render() { const discoveryListOne = [ { text: '乐见大牌:GAI说唱惊喜轰炸,PGON爆理想型', music: '天干物燥-GAI', image: '/static/images/news-cover-one.jpeg', author: '乐见大牌', read: 3820 }, { text: '有些男女之情,比爱情更让人羡慕', music: '富士山下-陈奕迅', image: '/static/images/news-cover-two.jpg', author: '淘漉音乐', read: 8230 }, { text: '评论志|最怕回忆突然锋利,翻滚不息', music: '突然好想你-五月天', image: '/static/images/news-cover-three.jpg', author: '大冲音像店', read: 5761 } ]; const discoveryListTwo = [ { text: 'S.H.E:十年女团,十年回忆', music: 'S.H.E | 十年女团,十年回忆-微音', image: '/static/images/discovery-she.jpg', author: '微音', read: 3555 }, { text: '张韶涵:好久不见,回来就好', music: '复仇时刻-张韶涵/我是赞助商派来的', image: '/static/images/discovery-zhangshaohan.jpeg', author: '淘漉音乐', read: 4223 }, { text: 'LOL背景音乐集锦:电子盛宴,自带BUFF', music: 'Time Leaper-Hinkik', image: '/static/images/discovery-ali.jpeg', author: '醉心琳琅', read: 9405 } ]; const topicList = [ { image: '/static/images/topic-lizongsheng.jpeg', title: '#又见·李宗盛', text: '戳到了心坎的一句歌词' }, { image: '/static/images/topic-linyoujia.jpg', title: '#又见·林宥嘉', text: '曾在哪首歌里泪流不止?' }, { image: '/static/images/topic-chenyixun.jpeg', title: '#又见·陈奕迅', text: '循环播放最多次的一首歌' }, { image: '/static/images/topic-tianfuzhen.jpeg', title: '#又见·田馥甄', text: '因为哪首歌爱上她的?' } ]; return (
    { discoveryListOne.map(function (item,index) { return (
  • {item.text}

    {item.music}

    {item.author} 阅读 {item.read}

  • ); }) }
发现·话题
    { topicList.map(function (item,index) { return (
  • {item.title}

    {item.text}

  • ); }) }
    { discoveryListTwo.map(function (item,index) { return (
  • {item.text}

    {item.music}

    {item.author} 阅读 {item.read}

  • ); }) }
) } } export default Discovery; ================================================ FILE: src/pages/Discovery/style.scss ================================================ .qqmusic-discovery-item { display: flex; padding: 15px 0 0 15px; .qqmusic-discovery-item-left { flex: 1; padding-right: 10px; overflow: hidden; .text { margin-top: 15px; font-size: 19px; } .music { margin-top: 10px; font-size: 13px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; .music-image { width: 21px; height: 21px; margin-right: 5px; vertical-align: -5px; } } .extra { font-size: 13px; margin-top: 10px; color: #6F6F6F; } } .qqmusic-discovery-item-right { flex: 0 0 110px; height: 110px; overflow: hidden; .image { width: 110px; } } } .qqmusic-discovery-carousel{ margin-top: 15px; .top{ .tag{ font-size: 11px; background: #000; color: #fff; padding: 1px 2px; margin-left: 15px; } } .bottom{ margin-top: 4px; height: 99px; overflow-y: hidden; .list{ padding-left: 12px; white-space:nowrap; overflow: auto; -webkit-overflow-scrolling: touch; .item{ display: inline-block; position: relative; margin-left: 3px; .image{ width: 168px; height: 100px; } .mask{ position: absolute; top:0; left:0; width: 168px; height: 100px; background: rgba(0,0,0,0.4); .title{ margin-top: 40px; color: #fff; text-align: center; font-size: 21px; } .text{ margin-top: 5px; color: #ccc; text-align: center; font-size: 14px; } } } } } } ================================================ FILE: src/pages/MusicClub/index.js ================================================ import React from 'react'; import { Carousel, Grid } from 'antd-mobile'; import './style.scss'; class MusicClub extends React.Component { render() { const imgList = [ "/static/images/carousel-cover-one.jpg", "/static/images/carousel-cover-two.jpg", "/static/images/carousel-cover-three.jpg", "/static/images/carousel-cover-four.jpg", "/static/images/carousel-cover-five.jpg", "/static/images/carousel-cover-six.jpg", "/static/images/carousel-cover-seven.jpg", "/static/images/carousel-cover-eight.jpg" ]; const menuList = [ { icon: require('@/assets/icon-grid-singer.png'), text: '歌手' }, { icon: require('@/assets/icon-grid-rank.png'), text: '排行' }, { icon: require('@/assets/icon-grid-radio.png'), text: '电台' }, { icon: require('@/assets/icon-grid-categories.png'), text: '分类歌单' }, { icon: require('@/assets/icon-grid-video.png'), text: '视频MV' }, { icon: require('@/assets/icon-grid-album.png'), text: '数字专辑' }, ]; const songMenuArray = [ { image: '/static/images/songmenu-one.jpeg', text: '浮游时光 | 品一杯慢情调的韩系布鲁斯', amount: '15.7万' }, { image: '/static/images/songmenu-two.jpeg', text: '达人周末 | 那些能激起中二病的动漫燃曲', amount: '65.7万' }, { image: '/static/images/songmenu-three.jpeg', text: '99位唱见歌手 :一人一首代表曲', amount: '272.2万' }, { image: '/static/images/songmenu-four.jpeg', text: '独立民谣 | 从大不列颠群岛吹来怡然清风', amount: '39.5万' }, { image: '/static/images/songmenu-five.jpeg', text: '《王者荣耀》风骚走位必备BGM', amount: '1319.1万' }, { image: '/static/images/songmenu-six.jpeg', text: '你一定听过却死活叫不上歌名的灵魂级配乐', amount: '162.5万' } ] return (
{imgList.map((item, index) => { return ( ); }) } (
{dataItem.text}
) } />

歌单推荐

{ return (
{dataItem.amount}

{dataItem.text}

) } } />
) } } export default MusicClub ================================================ FILE: src/pages/MusicClub/style.scss ================================================ .qqmusic-home-body { background: #fff; .slideshow-list { min-height: 154px; .slideshow-item-link { display: block; } .slideshow-item-img { width: 100%; } } .qqmusic-grid-list { margin-top: 16px; .am-flexbox { height: 60px !important; .qqmusic-grid-item { display: flex; align-items: center; padding-left: 15px; height: 60px; .qqmusic-grid-item-icon { width: 24px; height: 24px; } .qqmusic-grid-item-text { padding-left: 10px; font-size: 15px; } } } } .qqmusic-songMenu-recommend { margin: 15px 0; .title { position: relative; text-align: center; font-size: 18px; letter-spacing: 5px; .icon-circle-right { display: inline-block; width: 26px; height: 26px; position: absolute; right: 15px; top: -3px; background: url('../../assets/icon-circle-right.png'); background-size: 26px 26px; } } .qqmusic-recommend-list { margin-top: 15px; .am-flexbox { height: 165px !important; .qqmusic-recommend-item { .qqmusic-recommend-item-image-wrapper { position: relative; width: 100%; height: 125px; .image{ width: 100%; height: 100%; } .amount{ position: absolute; left: 10px; bottom: 5px; font-size: 12px; color: #fff; } .amount:before{ content: ''; display:inline-block; width: 12px; height: 12px; margin-right: 5px; background-image: url('../../assets/icon-music-amount.png'); background-size: 12px 12px; } .link-to-musicList-detail{ position: absolute; right: 5px; bottom: 5px; width: 25px; height: 25px; } } .text{ padding: 4px 2px; font-size: 11px; line-height: 16px; text-align: left; } } } } } } ================================================ FILE: src/pages/MyCenter/index.js ================================================ import React from 'react'; import { Grid } from 'antd-mobile'; import './style.scss'; import auditionImg from '@/assets/icon-user-audition.png'; import dredgeImg from '@/assets/icon-user-dredge.png'; import rankImg from '@/assets/icon-user-rank.png'; import SongMenu from '@/components/SongMenu'; class mycenter extends React.Component { render() { const girdList = [ { text: '本地歌曲', imgSrc: require('@/assets/icon-grid-music.png') }, { text: '下载歌曲', imgSrc: require('@/assets/icon-grid-download.png') }, { text: '最近播放', imgSrc: require('@/assets/icon-grid-recent.png') }, { text: '我喜欢', imgSrc: require('@/assets/icon-grid-favorite.png') }, { text: '下载MV', imgSrc: require('@/assets/icon-grid-mv.png') }, { text: '已购音乐', imgSrc: require('@/assets/icon-grid-buy.png') } ] return (
0分钟
开通
椰子油
(

{item.text}

) } />

个性电台

偶遇身边好音乐

跑步电台

QQ音乐 x Nike,让运动乐在其中

) } } export default mycenter; ================================================ FILE: src/pages/MyCenter/style.scss ================================================ .qqmusic-home-body { .qqmusic-mycenter-top { padding: 5px 0 20px; background: #f5f5f9; .qqmusic-mycenter-user { background: #fff; padding-bottom: 5px; .qqmusic-mycenter-user-module { text-align: center; padding-top: 5px; .qqmusic-mycenter-user-audition, .qqmusic-mycenter-user-dredge { display: inline-block; border: 1px solid #8a8a8a; text-align: center; width: 80px; height: 25px; border-radius: 15px; .text { padding-left: 4px; font-size: 14px; color: #cdcdcd; } .icon { width: 24px; height: 24px; vertical-align: -5px; } } .qqmusic-mycenter-user-photo { width: 50px; height: 50px; border-radius: 50%; vertical-align: middle; margin: 0 15px; } .userName:before, .userName:after { content: ""; display: inline-block; height: 1px; width: 40px; border-top: 1px solid #2c2c2c; vertical-align: middle; } .userName:before { margin-right: 5px; } .userName::after { margin-left: 5px; } .qqmusic-mycenter-user-rank { height: 19px; width: 19px; } } } .qqmusic-mycenter-grid { .am-flexbox { height: 75px !important; .qqmusic-mycenter-grid-item { .image { width: 35px; height: 35px; } .text { text-align: center; font-size: 15px; padding-top: 5px; } } } } } .qqmusic-mycenter-middle { margin-top: 5px; background: #fff; .qqmusic-mycenter-station { display: flex; height: 55px; .qqmusic-mycenter-station-left { flex: 0 0 65px; .station-image { width: 55px; height: 55px; } } .qqmusic-mycenter-station-right { flex: 1; display: flex; justify-content: center; flex-direction: column; .station-title { font-size: 16px; font-weight: 400; } .station-text { padding-top: 5px; font-size: 13px; color: #cacaca; font-weight: 300; } } } } } ================================================ FILE: src/router/index.js ================================================ import Loadable from 'react-loadable'; import createBrowserHistory from 'history/createBrowserHistory'; import MainLayout from '@/layouts/MainLayout'; import Loading from '@/components/Loading'; const Discovery = Loadable({loader: () => import('@/pages/Discovery'),loading: Loading}); const MusicClub = Loadable({loader: () => import('@/pages/MusicClub'),loading: Loading}); const MyCenter = Loadable({loader: () => import('@/pages/MyCenter'),loading: Loading}); export const history = createBrowserHistory(); export const routes = [ { path:'/', redirect:'/myCenter' }, { path:'/myCenter', layout:MainLayout, component:MyCenter }, { path:'/musicClub', layout:MainLayout, component:MusicClub }, { path:'/discovery', layout:MainLayout, component:Discovery }, ] ================================================ FILE: src/scss/reset.scss ================================================ *{ margin: 0; padding: 0; } html,body{ height: 100%; } li{ list-style: none; } a{ text-decoration: none; } input{ border: none; outline:none; } #app{ height: 100%; } .border-bottom{ position: relative; } .border-bottom:after { height: 1px; content: ''; width: 100%; border-top: 1px solid #c7c7c7; position: absolute; bottom: -1px; right: 0; transform: scaleY(0.5); -webkit-transform: scaleY(0.5); } .border-top{ position: relative; } .border-top:after { height: 1px; content: ''; width: 100%; border-top: 1px solid #c7c7c7; position: absolute; top: -1px; right: 0; transform: scaleY(0.5); -webkit-transform: scaleY(0.5); } ================================================ FILE: src/scss/variable.scss ================================================ $primary-color:#31c37c; $text-color-secondary:rgba(0,0,0,.45); $screen-xl: 1200px; $screen-sm: 576px; $screen-xs: 480px; ================================================ FILE: src/store/actionTypes.js ================================================ //播放或暂停音乐 export const CHANGE_MUSIC_STATUS="CHANGE_MUSIC_STATUS"; //添加音乐到播放列表 export const ADD_MUSIC="ADD_MUSIC"; //更改当前音乐 export const CHANGE_CURRENT_MUSIC="CHANGE_CURRENT_MUSIC"; //添加并更改当前音乐 export const ADD_AND_CHANGE_MUSIC="ADD_AND_CHANGE_MUSIC"; //播放指定歌曲 export const PLAY_SPECIFIC_MUSIC_BY_MID="PLAY_SPECIFIC_MUSIC_BY_MID"; //清空播放列表 export const CLEAR_MUSIC_LIST="CLEAR_MUSIC_LIST"; //移除指定的音乐 export const REMOVE_MUSIC_FROM_LIST="REMOVE_MUSIC_FROM_LIST"; //添加歌单 export const ADD_SONG_LIST="ADD_SONG_LIST"; //删除歌单 export const REMOVE_SONG_LIST="REMOVE_SONG_LIST"; ================================================ FILE: src/store/actions.js ================================================ import * as actionTypes from './actionTypes'; //添加音乐 export const addMusic=(data,callback)=> async (dispatch,getState,{API})=>{ dispatch({ type: actionTypes.ADD_MUSIC, payload:data }); callback(); } //播放或暂停音乐 export const changePlayStatus=(data)=> async (dispatch,getState,{API})=>{ dispatch({ type: actionTypes.CHANGE_MUSIC_STATUS, payload:data }); } //更改音乐 export const changeCurrentMusic=(data)=> async (dispatch,getState,{API})=>{ dispatch({ type: actionTypes.CHANGE_CURRENT_MUSIC, payload:data }); } //添加并更改当前音乐 export const addAndChangeMusic=(data,isPlay)=> async (dispatch,getState,{API})=>{ dispatch({ type: actionTypes.ADD_AND_CHANGE_MUSIC, payload:{ data, isPlay } }); } //播放指定音乐 export const playSpecificMusicByMid=(data)=> async (dispatch,getState,{API})=>{ dispatch({ type: actionTypes.PLAY_SPECIFIC_MUSIC_BY_MID, payload:data }); } //清除播放列表 export const clearMusicList=()=> async (dispatch,getState,{API})=>{ dispatch({ type: actionTypes.CLEAR_MUSIC_LIST }); } //将音乐从播放列表中移除 export const removeMusicFromList=(data)=> async (dispatch,getState,{API})=>{ dispatch({ type: actionTypes.REMOVE_MUSIC_FROM_LIST, payload:data }); } //添加歌单 export const addSongMenu=(data)=> async (dispatch,getState,{API})=>{ dispatch({ type: actionTypes.ADD_SONG_LIST, payload:data }); } //删除歌单 export const removeSongMenu=(data)=> async (dispatch,getState,{API})=>{ dispatch({ type: actionTypes.REMOVE_SONG_LIST, payload:data }); } ================================================ FILE: src/store/index.js ================================================ import { createStore,combineReducers, applyMiddleware, compose } from 'redux'; import thunk from 'redux-thunk'; import {API} from '@/api'; import global from './reducer'; const rootReducer = combineReducers({ global }) export default function configureStore(initialState) { const store = createStore( rootReducer, initialState, compose( applyMiddleware(thunk.withExtraArgument({API})), window.devToolsExtension ? window.devToolsExtension() : f => f ) ); return store; }; ================================================ FILE: src/store/reducer.js ================================================ import * as actionTypes from './actionTypes'; const initialState = { currentMusic: {}, isPlay: false, isCurrentMusicChange: false, musicList: [], songMenuArray:[] }; function isMusicExist(musicData,array) { return array.some((item)=>{ return item.mid === musicData.mid; }); } function isSongMenuExist(name,array){ return array.some((item)=>{ return item===name; }); } export default function music(state = initialState, action) { const {currentMusic,musicList,songMenuArray} = state; const {type,payload} = action; switch (type) { case actionTypes.ADD_MUSIC: if (!isMusicExist(payload,musicList)) { musicList.unshift(payload) return Object.assign({}, state, { musicList }); } else { return state; } case actionTypes.CHANGE_CURRENT_MUSIC: return Object.assign({}, state, { currentMusic: payload, isCurrentMusicChange: true }); case actionTypes.CHANGE_MUSIC_STATUS: return Object.assign({}, state, { isPlay: payload, isCurrentMusicChange: false }); case actionTypes.ADD_AND_CHANGE_MUSIC: if (!isMusicExist(payload.data,musicList)) { musicList.unshift(payload.data) } return Object.assign({}, state, { musicList, currentMusic: payload.data, isCurrentMusicChange: true, isPlay: payload.isPlay }); case actionTypes.PLAY_SPECIFIC_MUSIC_BY_MID: return Object.assign({}, state, { currentMusic: musicList.find((music)=>music.mid===payload), isCurrentMusicChange: true, isPlay: true }); case actionTypes.CLEAR_MUSIC_LIST: return Object.assign({}, state, { currentMusic: {}, isCurrentMusicChange: false, musicList: [], isPlay: false }); case actionTypes.REMOVE_MUSIC_FROM_LIST: const newMusicList = musicList.filter((music)=>music.mid!==payload); if (payload !== currentMusic.mid) { return Object.assign({}, state, { isCurrentMusicChange: false, musicList: newMusicList }); } else { return Object.assign({}, state, { isCurrentMusicChange: newMusicList.length>0?true:false, isPlay:newMusicList.length>0?true:false, currentMusic:musicList.length>0?newMusicList[0]:{}, musicList:newMusicList }); } case actionTypes.ADD_SONG_LIST: if(!isSongMenuExist(payload,songMenuArray)){ songMenuArray.unshift(payload); return Object.assign({},state,{ songMenuArray }); }else{ return state; } case actionTypes.REMOVE_SONG_LIST: let newSongMenuArray=songMenuArray.filter((item)=>{ return !isSongMenuExist(item,payload); }); return Object.assign({},state,{ songMenuArray:newSongMenuArray }); default: return state; } } ================================================ FILE: src/utils/http.js ================================================ import axios from 'axios'; import {Toast} from 'antd-mobile'; const instance=axios.create({ //超时时间 timeout:3000, //响应前处理 transformResponse:(responseData)=>{ return responseData; } }) //响应拦截 instance.interceptors.response.use(function (response) { const {status,data,statusText,headers}=response; if(status===200){ return headers['content-type']==='application/json'?JSON.parse(data):data; }else if(status===401){ //跳转登录 }else{ Toast.fail(`${status}-${statusText}`); return response; } }, function (error) { // 对响应错误做点什么 return Promise.reject(error); }); export default { get:(url,params,option)=>{ return instance.get(url,Object.assign({},option,{params})); }, post:(url,params,option)=>{ return instance.post(url,params,option); }, delete:(url,params,option)=>{ return instance.delete(url,Object.assign({},option,{params})); } } ================================================ FILE: src/utils/index.js ================================================ export default { //格式化数字位数 "fix":function(num, length){ return ('' + num).length < length ? ((new Array(length + 1)).join('0') + num).slice(-length) : '' + num; }, //根据秒转换成分:秒的形式 "formatSeconds":function(seconds){ var minute=Math.floor(seconds/60); var second=Math.round(seconds-minute*60); return this.fix(minute,2)+":"+this.fix(second,2); }, //根据字符串解析时间 "parseStrToSeconds":function(timeStr){ timeStr=timeStr.replace(/\[|\]/,""); var timeArray=timeStr.split(":"); return parseInt(timeArray[0])*60+parseFloat(timeArray[1]); }, //解析歌词 "parseLyric":function(text){ var lyricList=text.split("\n"); var timeReg=/\[[0-9]{2}:[0-9]*\.[0-9]*\]/; var array=[]; for(var i=0;i