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-音乐馆

## Tab-发现

## 侧滑栏

## 播放列表

## 播放器


## 歌曲搜索


## 歌单管理


# 项目总结
整个项目采用了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
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 (
)
}
}
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(
新建歌单
保存
);
}
}
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}
)
})
}
- 正在加载更多...
)
}
}
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 (
-
)
})
}
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 (
-
)
})
}
)
}
}
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 (
-
);
})
}
{
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 (
)
}
}
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 (
(
{item.text}
)
}
/>
)
}
}
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