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
================================================
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link rel="shortcut icon" href="//y.qq.com/favicon.ico?max_age=2592000">
<title>仿QQ音乐 - 中国最新最全免费正版高品质音乐平台!</title>
</head>
<body>
<div id="app"></div>
</body>
</html>
================================================
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(<Redirect key={`${parentPath}${path}`} exact from={path} to={`${parentPath}${redirect}`}/>)
}
if(component){
renderedRoutesList.push(
layout?<Route
key={`${parentPath}${path}`}
exact path={`${parentPath}${path}`}
render={(props)=>React.createElement(layout,props,React.createElement(component,props))} />:
<Route
key={`${parentPath}${path}`}
exact
path={`${parentPath}${path}`}
component={component}/>)
}
if(Array.isArray(children)&&children.length>0){
renderRoutes(children,path)
}
});
}
renderRoutes(routes,'')
return renderedRoutesList;
}
class App extends React.PureComponent{
render(){
return (
<Router history={history}>
<Switch>
{getRouterByRoutes(routes)}
</Switch>
</Router>
)
}
}
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<musicList.length-1){
playSpecificMusicByMid(currentIndex+1);
}else{
playSpecificMusicByMid(0);
}
}
};
}
componentDidUpdate() {
const {isPlay} = this.props;
if(isPlay){
this.qqmusicAudio.play();
}else{
this.qqmusicAudio.pause();
}
}
render() {
const {currentMusic={},isPlay,musicList} = this.props;
const {title,author,pic,url} = currentMusic;
return (
<div className="qqmusic-home-footer border-top">
<div className="left">
<img className={classnames('avatar',isPlay ? 'active' : '')} src={pic} onClick={this.consoleSwitch} />
</div>
<div className="center" onClick={this.consoleSwitch}>
<h4 className="song">{title}</h4>
<p className="singer">{author}</p>
</div>
<p className={musicList.length === 0 ? 'no-music show' : 'no-music'}>QQ音乐 听我想听的歌</p>
<div className="right">
<audio ref={(audio)=>this.qqmusicAudio=audio} src={url} ></audio>
<img className="qqmusic-play-switch" src={isPlay ? pauseImg : playImg} onClick={this.changePlayState} />
<img className="qqmusic-play-list" src={playListImg} onClick={this.musicListSwitch} />
</div>
<Control changeCurrentTime={this.changeCurrentTime} currentSeconds={this.state.currentSeconds} totalSeconds={this.state.totalSeconds} isControlShow={this.state.isControlShow} changePlayState={this.changePlayState} consoleSwitch={this.consoleSwitch}></Control>
<MusicList isMusicListShow={this.state.isMusicListShow} musicListSwitch={this.musicListSwitch}></MusicList>
</div>
)
}
}
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 (
<div className={classnames('qqmusic-control',isControlShow ? 'show' : '')}>
<div className="qqmusic-control-content">
<div className="qqmusic-control-top">
<img className="icon-control-down" src={require("@/assets/icon-control-down.png")} onClick={consoleSwitch} />
<p className="music-name">{title}</p>
</div>
<div className={classnames('qqmusic-control-middle',isPlay ? 'active' : '')}>
<Carousel autoplay={false}>
{
[
(
<div key="1" className="carousel-one">
<p className="music-signer">{author}</p>
<img className="music-cover" src={pic} />
</div>
),
(
<div key="2" className="carousel-two" style={{scrollMarginTop:currentLyricIndex*40}}>
<ul ref={(dom)=>this.lyricDom=dom} className="lyricList">
{
lyricArray.map((item, index) => {
return (
<li className={currentLyricIndex===index?"lyric active":"lyric"} key={index}>{item.text}</li>
)
})
}
</ul>
</div>
)
]
}
</Carousel>
</div>
<div className="qqmusic-control-bottom">
<div className="qqmusic-control-progress">
<span className="currentPlayTime">{utils.formatSeconds(currentSeconds)}</span>
<div ref="progressParent" className="progress-wrapper">
<div className="progress-inner" style={{ width: currentSeconds / totalSeconds * 200 + "px" }}></div>
<span className="progress-btn" onTouchMove={this.changePlayProgress.bind(this)} style={{ transform: `translateX(${currentSeconds / totalSeconds * 200 - 7}px)` }}></span>
</div>
<span className="totalPlayTime">{utils.formatSeconds(totalSeconds)}</span>
</div>
<div className="qqmusic-control-btns">
<img className="prev" src={require("@/assets/icon-music-prev.png")} onClick={this.prevMusic} />
<img className="status" src={isPlay ? require("@/assets/icon-control-pause.png") : require("@/assets/icon-control-play.png")} onClick={changePlayState} />
<img className="next" src={require("@/assets/icon-music-next.png")} onClick={this.nextMusic} />
</div>
</div>
</div>
<div className="qqmusic-control-bg" style={{ backgroundImage: `url(${pic}` }}></div>
<div className="qqmusic-control-bg-mask"></div>
</div>
)
}
}
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 (
<div className={className}>
<div className="qqmusic-header">
<div className="top">
<i className="icon-left" onClick={this.openChange}></i>
<NavLink className="qqmusic-tab" activeClassName="qqmusic-tab-active" to="/myCenter" replace>我的</NavLink>
<NavLink className="qqmusic-tab" activeClassName="qqmusic-tab-active" to="/musicClub" replace>音乐馆</NavLink>
<NavLink className="qqmusic-tab" activeClassName="qqmusic-tab-active" to="/discovery" replace>发现</NavLink>
<Popover mask style={{left:0,right:0}}
visible={popover}
overlay={[
(<Item key="1" value="scan"><img className="popover-item-img" src={require('@/assets/icon-popover-discriminate.png')}/><font className="popover-item-text">听歌识曲</font></Item>),
(<Item key="2" value="sweep"><img className="popover-item-img" src={require('@/assets/icon-popover-sweep.png')}/><font className="popover-item-text">扫一扫</font></Item>),
]}
onVisibleChange={this.popoverChange}
onSelect={this.popoverSelect}
><i className="icon-right" onClick={this.popoverChange.bind(this,true)}></i></Popover>
</div>
<div className="bottom" onTouchStart={this.searchChange}>
<div className="search">
<i className="search-icon"></i>
<span className="text">搜索</span>
</div>
</div>
</div>
<Slider docked={docked} openChange={this.openChange}></Slider>
<Search search={search} searchChange={this.searchChange}></Search>
</div>
);
}
}
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 (
<div className="comp-loading">
<div className="item-1"></div>
<div className="item-2"></div>
<div className="item-3"></div>
<div className="item-4"></div>
<div className="item-5"></div>
</div>
)
}
================================================
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 (
<div className={classnames('qqmusic-music-list-wrapper',isMusicListShow?'show':'')}>
<div className="qqmusic-music-list-content">
<div className="top border-bottom">
<h4 className="title">播放列表</h4>
<img className="clear-list" src={require("@/assets/icon-list-clear.png")} onClick={this.clearMusicList}/>
</div>
<div className="middle">
<ul className="music-list">
{
musicList.map((item)=>{
return (
<li className="music-item border-bottom" style={{color:item.mid===currentMusic.mid?"#31c37c":"#fff"}} key={item.mid}>
<span onClick={this.playSpecificMusic.bind(this,item)}>{item.title} - {item.author}</span>
<img className="tag" style={{display:item.mid===mid?'inline-block':'none'}} src={require('@/assets/icon-music-playing.png')}/>
<img className="delete" src={require("@/assets/icon-record-close.png")} onClick={this.removeMusicFromList.bind(this,item)}/>
</li>
);
})
}
</ul>
</div>
<div className="bottom" onClick={musicListSwitch}>关闭</div>
</div>
<div className="qqmusic-music-list-bg" onClick={musicListSwitch}></div>
</div>
)
}
}
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(
<div className={classnames('qqmusic-new-songmenu',isNewSongMenuShow?'show':'')}>
<div className="new-songmenu-header">
<img className="icon-arrow-left" src={require("@/assets/icon-arrow-left.png")} onClick={this.comeback} />
<p className="title">新建歌单</p>
<span className="save" onClick={this.saveSongMenu}>保存</span>
</div>
<div className="new-songmenu-body">
<input ref={(input)=>this.inputText=input} className="input-text" type="text" placeholder="请输入内容" onInput={this.changeStrLength}/>
<p className="total-count">{20-totalCount}</p>
<NoticeBar className={isErrorShow?'error-notice show':'error-notice'} mode="closable" icon={null}>!最多输入20个字</NoticeBar>
</div>
</div>
);
}
}
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 (
<div className={classnames('qqmusic-search-wrapper',search ? 'show' :'')} >
<div className="qqmusic-search-top">
<img ref='inputText' className="icon-arrow-left" src={require("@/assets/icon-arrow-left.png")} onClick={this.comeback} />
<input className="input-text" type="text" placeholder="支持音乐搜索" onKeyUp={this.keyboardListener} />
<span className="icon-input-clear" onClick={this.clearInput}></span>
<span className="btn-search" onClick={this.getSearhListAjax}>搜索</span>
</div>
<div className="qqmusic-search-bottom" onScroll={this.getMoreSearchList}>
<div className="remind-mask" style={{display:isRemindDivShow?'block':'none'}}>
<div className="search-text-list-wrapper">
<h4 className="title-hot-search">热门搜索</h4>
<ul className="search-text-list">
{
searchTextList.map((item,index)=>{
return (
<li className="search-text-item" onClick={this.fastSearch.bind(this,item)} key={index}>{item}</li>
);
})
}
</ul>
</div>
<h4 style={{display:recordList.length>0?'block':'none'}} className="title-search-history border-bottom">搜索历史<span className="cleanRecord" onClick={this.clearRecord}>清空历史</span></h4>
<ul className="record-list">
{
recordList.map((item,index) => {
return (
<li className="record-item border-bottom" key={index}>
<span className="icon-recent"></span>
<p onClick={this.fastSearch.bind(this,item)}>{item}</p>
<span className="icon-close" onClick={this.removeRecord.bind(this, item)}></span>
</li>
)
})
}
</ul>
</div>
<ul className="qqmusic-search-list">
{
songList.map((item, index) => {
return (
<li className="qqmusic-search-list-item border-bottom" key={index} onClick={this.addMusic.bind(this, item)}>
<div className="left">
<h4 className="title">{item.title}</h4>
<p className="singer">{item.author}</p>
<p className="intro">{item.album}</p>
</div>
<div className="right">
<img className="cover" alt={item.album} src={item.pic}/>
</div>
</li>
)
})
}
<li className="hint" style={isCanGet ? { display: 'none' } : {}}>正在加载更多...</li>
</ul>
</div>
</div>
)
}
}
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 (
<Switch className='qqmusic-slider-body-item-extra' platform='ios' type={props.type}
{...getFieldProps('Switch',{
initialValue: false,
valuePropName: 'checked',
})}
onClick={(checked) => { 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:<SwitchExample type={'仅Wi-Fi联网'}/>
},
{
text:'定时关闭',
extra:<SwitchExample type={'定时关闭'}/>
},
{
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 (
<div>
<div className={classnames('qqmusic-slider',docked ? 'open' : '')}>
<div className="qqmusic-slider-header border-bottom">
{
headerSliderList.map(function (item, index) {
return (
<div className="qqmusic-slider-header-Item" key={index}>
<img className="qqmusic-slider-header-Item-img" src={item.imgSrc} />
<h4 className="qqmusic-slider-header-Item-title">{item.title}</h4>
<p className="qqmusic-slider-header-Item-text">{item.text}</p>
</div>
)
})
}
</div>
<ul className="qqmusic-slider-body">
{
bodySliderList.map(function (item, index) {
return (
<li className='qqmusic-slider-body-item' key={index}>
<font className='qqmusic-slider-body-item-text'>{item.text}</font>{item.extra}
</li>
)
})
}
</ul>
<div className="qqmusic-slider-footer border-top">
<div className="qqmusic-slider-footer-left">
<i className="qqmusic-slider-footer-icon"></i>
<span className="qqmusic-slider-footer-text">设置</span>
</div>
<div className="qqmusic-slider-footer-right">
<i className="qqmusic-slider-footer-icon"></i>
<span className="qqmusic-slider-footer-text">退出登录/关闭</span>
</div>
</div>
</div>
<div className={classnames('qqmusic-slider-bg',docked ? 'open' : '')} onTouchStart={this.props.openChange}></div>
</div>
)
}
}
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 (
<div className="qqmusic-mycenter-bottom">
<div className="qqmusic-mycenter-tabs">
<span className={classnames('qqmusic-mycenter-tab',activeTab===1?'active':'')} onClick={this.tabChange.bind(this,1)} >自建歌单</span>
|
<span className={classnames('qqmusic-mycenter-tab',activeTab===2?'active':'')} onClick={this.tabChange.bind(this,2)}>收藏歌单</span>
<i style={activeTab===2?{display:'none'}:{}} className="add-songmenu" onClick={this.newSongMenuShowSwitch}/>
<i className="songmenu-manage" onClick={this.songMenuMangementShowSwitch}></i>
</div>
<div className="qqmusic-mycenter-tab-content-one" style={activeTab!==1?{display:'none'}:{}}>
<ul className="songmenu-array" style={songMenuArray.length>0?{display:'block'}:{display:'none'}}>
{
songMenuArray.map((item,index)=>{
return (
<li className="songmenu-item" key={index}>
<div className="left">
<img className="logo" src={require("@/assets/icon-qqmusic-logo.png")}/>
</div>
<div className="right">
<p className="name">{item}</p>
<p className="num border-bottom">0首</p>
<span className="icon-right"></span>
</div>
</li>
)
})
}
</ul>
<div className="add-songmenu-wrapper" style={{display:songMenuArray.length>0?'none':'flex'}} onClick={this.newSongMenuShowSwitch}>
<div className="add-songmenu-wrapper-left">
<img className="add-songmenu-img" src={addImg}/>
</div>
<div className="add-songmenu-wrapper-right">
<p className="add-songmenu-text border-bottom">新建歌单</p>
</div>
</div>
</div>
<div className="qqmusic-mycenter-tab-content-two" style={activeTab!==2?{display:'none'}:{}}>
<p className="no-collected-songmenu">没有收藏的歌单</p>
</div>
<NewSongMenu isNewSongMenuShow={isNewSongMenuShow} newSongMenuShowSwitch={this.newSongMenuShowSwitch} ></NewSongMenu>
<SongMenuMangement isSongMenuMangementShow={isSongMenuMangementShow} songMenuMangementShowSwitch={this.songMenuMangementShowSwitch}></SongMenuMangement>
</div>
);
}
}
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 (
<div className={isSongMenuMangementShow?"qqmusic-songmenu-mangement show":"qqmusic-songmenu-mangement"}>
<div className="songmenu-mangement-header">
<img className="icon-arrow-left" src={require("@/assets/icon-arrow-left.png")} onClick={this.comeback.bind(this)} />
<p className="title">管理自建歌单</p>
</div>
<div className="songmenu-mangement-body">
<ul className="songmenu-array">
{
songMenuArray.map((item,index)=>{
return (
<li className="songmenu-item" key={index}>
<div className="left">
<CheckboxItem className="checkBox" onChange={this.changeSelectedList.bind(this,item)}></CheckboxItem>
</div>
<div className="middle border-bottom">
<img className="logo" src={require("@/assets/icon-qqmusic-logo.png")}/>
</div>
<div className="right border-bottom">
<p className="name">{item}</p>
<p className="num">0首</p>
</div>
</li>
)
})
}
</ul>
</div>
<div className="songmenu-mangement-footer">
<div className="delete-wrapper">
<img className="delete" onClick={this.removeSongMenu} src={require("@/assets/icon-songmenu-delete.png")}/>
</div>
<p className="text-wrapper">
<span className="text" onClick={this.removeSongMenu}>删除</span>
</p>
</div>
</div>
)
}
}
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 (
<div className="qqmusic-home">
<Header className="qqmusic-home-header" />
{children}
<Bandstand/>
</div>
);
}
}
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(
<Provider store={store}>
<App/>
</Provider>,
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 (
<div className="qqmusic-home-body">
<ul className="qqmusic-discovery-list">
{
discoveryListOne.map(function (item,index) {
return (
<li className="qqmusic-discovery-item" key={index}>
<div className="qqmusic-discovery-item-left">
<p className="text">{item.text}</p>
<p className="music"><img className="music-image" src={require('@/assets/icon-music-black.png')} />{item.music}</p>
<p className="extra">{item.author} 阅读 {item.read}</p>
</div>
<div className="qqmusic-discovery-item-right">
<img className="image" src={item.image} />
</div>
</li>
);
})
}
</ul>
<div className="qqmusic-discovery-carousel">
<div className="top"><span className="tag">发现·话题</span></div>
<div className="bottom">
<ul className="list">
{
topicList.map(function (item,index) {
return (
<li className="item" key={index}>
<img className="image" src={item.image} />
<div className="mask">
<h4 className="title">{item.title}</h4>
<p className="text">{item.text}</p>
</div>
</li>
);
})
}
</ul>
</div>
</div>
<ul className="qqmusic-discovery-list" style={{marginBottom:'0.3rem'}}>
{
discoveryListTwo.map(function (item,index) {
return (
<li className="qqmusic-discovery-item" key={index}>
<div className="qqmusic-discovery-item-left">
<p className="text">{item.text}</p>
<p className="music"><img className="music-image" src={require('@/assets/icon-music-black.png')} />{item.music}</p>
<p className="extra">{item.author} 阅读 {item.read}</p>
</div>
<div className="qqmusic-discovery-item-right">
<img className="image" src={item.image} />
</div>
</li>
);
})
}
</ul>
</div>
)
}
}
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 (
<div className="qqmusic-home-body">
<Carousel
className="slideshow-list"
infinite
autoplay={true}
autoplayInterval={2000}
>
{imgList.map((item, index) => {
return (
<a key={index} className="slideshow-item-link" href="javascript:;">
<img className="slideshow-item-img" src={item} />
</a>
);
})
}
</Carousel>
<Grid
className="qqmusic-grid-list"
data={menuList}
columnNum={3}
hasLine={false}
renderItem={
dataItem => (
<div className="qqmusic-grid-item">
<img className="qqmusic-grid-item-icon" src={dataItem.icon} />
<span className="qqmusic-grid-item-text" >{dataItem.text}</span>
</div>
)
}
/>
<div className="qqmusic-songMenu-recommend">
<p className="title">歌单推荐<i className="icon-circle-right"></i></p>
<Grid
className="qqmusic-recommend-list"
data={songMenuArray}
columnNum={3}
hasLine={false}
renderItem={
(dataItem, index) => {
return (
<div className="qqmusic-recommend-item" style={{marginLeft:index%3===1?'0.06rem':'',marginRight:index%3===1?'0.06rem':''}}>
<div className="qqmusic-recommend-item-image-wrapper">
<img className="image" src={dataItem.image} />
<span className="amount">{dataItem.amount}</span>
<img className="link-to-musicList-detail" src={require('@/assets/icon-music-link.png')}/>
</div>
<p className="text" >{dataItem.text}</p>
</div>
)
}
}
/>
</div>
</div>
)
}
}
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 (
<div className="qqmusic-home-body">
<div className="qqmusic-mycenter-top">
<div className="qqmusic-mycenter-user">
<div className="qqmusic-mycenter-user-module">
<div className="qqmusic-mycenter-user-audition">
<img className="icon" src={auditionImg} />
<span className="text">0分钟</span>
</div>
<img className="qqmusic-mycenter-user-photo" src="https://wx.qlogo.cn/mmopen/LHU7CmulIEWaZgu4PRWXOScvVCC5npYoPvBFVLMXldibtQ1BRVMJy4RaHXliabaqJSazgI8QTuF9g2X7l9iafOvfX27vcHl2ksA/0" />
<div className="qqmusic-mycenter-user-dredge">
<img className="icon" src={dredgeImg} />
<span className="text">开通</span>
</div>
</div>
<div className="qqmusic-mycenter-user-module">
<span className="userName">椰子油</span>
</div>
<div className="qqmusic-mycenter-user-module">
<img className="qqmusic-mycenter-user-rank" src={rankImg} />
</div>
</div>
<Grid
className="qqmusic-mycenter-grid"
data={girdList}
columnNum={3}
hasLine={false}
renderItem={
item => (
<div className="qqmusic-mycenter-grid-item">
<img className="image" src={item.imgSrc} />
<p className="text">{item.text}</p>
</div>
)
}
/>
<div className="qqmusic-mycenter-middle">
<div className="qqmusic-mycenter-station">
<div className="qqmusic-mycenter-station-left">
<img className="station-image" src="/static/images/broadcasting-station-specific.jpeg" />
</div>
<div className="qqmusic-mycenter-station-right">
<h4 className="station-title">个性电台</h4>
<p className="station-text">偶遇身边好音乐</p>
</div>
</div>
<div className="qqmusic-mycenter-station">
<div className="qqmusic-mycenter-station-left">
<img className="station-image" src="/static/images/broadcasting-station-run.jpeg" />
</div>
<div className="qqmusic-mycenter-station-right border-top">
<h4 className="station-title">跑步电台</h4>
<p className="station-text">QQ音乐 x Nike,让运动乐在其中</p>
</div>
</div>
</div>
<SongMenu />
</div>
</div>
)
}
}
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<lyricList.length;i++){
if(lyricList[i]!=""){
var seconds=this.parseStrToSeconds(lyricList[i].match(timeReg)[0]);
var text=lyricList[i].replace(timeReg,"");
array.push( {
"seconds":seconds,
"text":text
});
}
}
return array;
}
}
================================================
FILE: theme.js
================================================
module.exports={
"brand-primary":"#31c37c"
}
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
SYMBOL INDEX (77 symbols across 24 files)
FILE: build/check-versions.js
function exec (line 7) | function exec (cmd) {
FILE: build/utils.js
function generateLoaders (line 39) | function generateLoaders (loader, loaderOptions) {
FILE: build/webpack.base.conf.js
function resolve (line 6) | function resolve (dir) {
FILE: build/webpack.dev.conf.js
constant HOST (line 13) | const HOST = process.env.HOST
constant PORT (line 14) | const PORT = process.env.PORT && Number(process.env.PORT)
FILE: src/App.js
function getRouterByRoutes (line 8) | function getRouterByRoutes(routes){
class App (line 37) | class App extends React.PureComponent{
method render (line 38) | render(){
FILE: src/api/index.js
function mapUrlObjToFuncObj (line 5) | function mapUrlObjToFuncObj(urlObj){
function mapUrlObjToStrObj (line 16) | function mapUrlObjToStrObj(urlObj){
constant API (line 25) | const API = mapUrlObjToFuncObj(API_URL);
constant URL (line 26) | const URL = mapUrlObjToStrObj(API_URL);
FILE: src/components/Bandstand/index.js
class Bandstand (line 15) | @connect(
method getMusicById (line 42) | getMusicById(id, callback) {
method componentDidMount (line 73) | componentDidMount(){
method componentDidUpdate (line 101) | componentDidUpdate() {
method render (line 109) | render() {
FILE: src/components/Control/index.js
class Control (line 10) | @connect(
method getDerivedStateFromProps (line 20) | static getDerivedStateFromProps(nextProps,prevState){
method componentDidUpdate (line 66) | componentDidUpdate(){
method render (line 88) | render() {
FILE: src/components/Header/index.js
class Header (line 8) | class Header extends React.Component {
method render (line 41) | render(){
FILE: src/components/Loading/index.js
function loading (line 4) | function loading() {
FILE: src/components/MusicList/index.js
class MusicList (line 8) | @connect(
method playSpecificMusic (line 13) | playSpecificMusic(music){
method removeMusicFromList (line 22) | removeMusicFromList(music){
method componentWillReceiveProps (line 27) | componentWillReceiveProps(nextProps) {
method render (line 32) | render(){
FILE: src/components/NewSongMenu/index.js
class NewSongMenu (line 8) | @connect(
method render (line 53) | render(){
FILE: src/components/Search/index.js
class Search (line 12) | @connect(
method fastSearch (line 112) | fastSearch(searchText){
method addSearchRecord (line 121) | addSearchRecord(recordStr) {
method removeRecord (line 135) | removeRecord(record) {
method addMusic (line 153) | addMusic(musicItem) {
method componentDidMount (line 158) | componentDidMount() {
method render (line 166) | render() {
FILE: src/components/Slider/index.js
class Slider (line 6) | class Slider extends React.Component {
method switchChange (line 11) | switchChange(type,checked){
method render (line 14) | render() {
FILE: src/components/SongMenu/index.js
class SongMenu (line 10) | @connect(
method tabChange (line 32) | tabChange(tabIndex){
method render (line 37) | render() {
FILE: src/components/SongMenuMangement/index.js
class SongMenuMangement (line 8) | @connect(
method changeSelectedList (line 19) | changeSelectedList(text){
method render (line 40) | render(){
FILE: src/constant/music.js
constant CHANGE_MUSIC_STATUS (line 2) | const CHANGE_MUSIC_STATUS="CHANGE_MUSIC_STATUS";
constant ADD_MUSIC (line 4) | const ADD_MUSIC="ADD_MUSIC";
constant CHANGE_CURRENT_MUSIC (line 6) | const CHANGE_CURRENT_MUSIC="CHANGE_CURRENT_MUSIC";
constant ADD_AND_CHANGE_MUSIC (line 8) | const ADD_AND_CHANGE_MUSIC="ADD_AND_CHANGE_MUSIC";
constant PLAY_MUSIC_BY_INDEX (line 10) | const PLAY_MUSIC_BY_INDEX="PLAY_MUSIC_BY_INDEX";
constant CLEAR_MUSIC_LIST (line 12) | const CLEAR_MUSIC_LIST="CLEAR_MUSIC_LIST";
constant REMOVE_MUSIC_FROM_LIST (line 14) | const REMOVE_MUSIC_FROM_LIST="REMOVE_MUSIC_FROM_LIST";
constant ADD_SONG_LIST (line 16) | const ADD_SONG_LIST="ADD_SONG_LIST";
constant REMOVE_SONG_LIST (line 18) | const REMOVE_SONG_LIST="REMOVE_SONG_LIST";
FILE: src/layouts/MainLayout/index.js
class MainLayout (line 5) | class MainLayout extends React.Component {
method render (line 6) | render() {
FILE: src/pages/Discovery/index.js
class Discovery (line 3) | class Discovery extends React.Component {
method render (line 4) | render() {
FILE: src/pages/MusicClub/index.js
class MusicClub (line 4) | class MusicClub extends React.Component {
method render (line 5) | render() {
FILE: src/pages/MyCenter/index.js
class mycenter (line 8) | class mycenter extends React.Component {
method render (line 9) | render() {
FILE: src/store/actionTypes.js
constant CHANGE_MUSIC_STATUS (line 2) | const CHANGE_MUSIC_STATUS="CHANGE_MUSIC_STATUS";
constant ADD_MUSIC (line 4) | const ADD_MUSIC="ADD_MUSIC";
constant CHANGE_CURRENT_MUSIC (line 6) | const CHANGE_CURRENT_MUSIC="CHANGE_CURRENT_MUSIC";
constant ADD_AND_CHANGE_MUSIC (line 8) | const ADD_AND_CHANGE_MUSIC="ADD_AND_CHANGE_MUSIC";
constant PLAY_SPECIFIC_MUSIC_BY_MID (line 10) | const PLAY_SPECIFIC_MUSIC_BY_MID="PLAY_SPECIFIC_MUSIC_BY_MID";
constant CLEAR_MUSIC_LIST (line 12) | const CLEAR_MUSIC_LIST="CLEAR_MUSIC_LIST";
constant REMOVE_MUSIC_FROM_LIST (line 14) | const REMOVE_MUSIC_FROM_LIST="REMOVE_MUSIC_FROM_LIST";
constant ADD_SONG_LIST (line 16) | const ADD_SONG_LIST="ADD_SONG_LIST";
constant REMOVE_SONG_LIST (line 18) | const REMOVE_SONG_LIST="REMOVE_SONG_LIST";
FILE: src/store/index.js
function configureStore (line 8) | function configureStore(initialState) {
FILE: src/store/reducer.js
function isMusicExist (line 9) | function isMusicExist(musicData,array) {
function isSongMenuExist (line 14) | function isSongMenuExist(name,array){
function music (line 20) | function music(state = initialState, action) {
Condensed preview — 58 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (145K chars).
[
{
"path": ".babelrc",
"chars": 830,
"preview": "{\n \"presets\": [\n [\"@babel/preset-env\", {\n \"modules\": false,\n \"targets\": {\n \"browsers\": [\"> 1%\", \"la"
},
{
"path": ".gitignore",
"chars": 23,
"preview": "dist\nnode_modules\n.idea"
},
{
"path": ".postcssrc.js",
"chars": 184,
"preview": "module.exports = {\n \"plugins\": {\n \"postcss-import\": {},\n \"postcss-url\": {},\n // to edit target browsers: use \""
},
{
"path": "README.md",
"chars": 1934,
"preview": "# react-mobile-qqMusic\n# 技术栈\n 1. react\n 2. react-router\n 3. react-redux\n 4. es6\n 5. axios\n 6. webpack\n# 关于项目 \n## 1.安装依赖包"
},
{
"path": "build/build.js",
"chars": 1198,
"preview": "'use strict'\nrequire('./check-versions')()\n\nprocess.env.NODE_ENV = 'production'\n\nconst ora = require('ora')\nconst rm = r"
},
{
"path": "build/check-versions.js",
"chars": 1290,
"preview": "'use strict'\nconst chalk = require('chalk')\nconst semver = require('semver')\nconst packageConfig = require('../package.j"
},
{
"path": "build/utils.js",
"chars": 2355,
"preview": "'use strict'\nconst path = require('path')\nconst config = require('../config')\nconst MiniCssExtractPlugin = require(\"mini"
},
{
"path": "build/webpack.base.conf.js",
"chars": 1800,
"preview": "'use strict'\nconst path = require('path')\nconst utils = require('./utils')\nconst config = require('../config')\n\nfunction"
},
{
"path": "build/webpack.dev.conf.js",
"chars": 2808,
"preview": "'use strict'\nconst utils = require('./utils')\nconst webpack = require('webpack')\nconst config = require('../config')\ncon"
},
{
"path": "build/webpack.prod.conf.js",
"chars": 3819,
"preview": "'use strict'\nconst path = require('path')\nconst utils = require('./utils')\nconst webpack = require('webpack')\nconst conf"
},
{
"path": "config/dev.env.js",
"chars": 156,
"preview": "'use strict'\nconst merge = require('webpack-merge')\nconst prodEnv = require('./prod.env')\n\nmodule.exports = merge(prodEn"
},
{
"path": "config/index.js",
"chars": 2189,
"preview": "'use strict'\n// Template version: 1.3.1\n// see http://vuejs-templates.github.io/webpack for documentation.\n\nconst path ="
},
{
"path": "config/prod.env.js",
"chars": 61,
"preview": "'use strict'\nmodule.exports = {\n NODE_ENV: '\"production\"'\n}\n"
},
{
"path": "index.html",
"chars": 414,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width"
},
{
"path": "package.json",
"chars": 2758,
"preview": "{\n \"name\": \"sword\",\n \"version\": \"1.0.0\",\n \"description\": \"\",\n \"main\": \"index.js\",\n \"scripts\": {\n \"test\": \"echo \\"
},
{
"path": "src/App.js",
"chars": 1386,
"preview": "import React from 'react';\nimport {Router} from 'react-router-dom';\nimport {Switch, Route ,Redirect} from 'react-router'"
},
{
"path": "src/api/index.js",
"chars": 589,
"preview": "import {keys} from 'lodash'\nimport http from '@/utils/http'\nimport API_URL from './url';\n\nfunction mapUrlObjToFuncObj(ur"
},
{
"path": "src/api/url.js",
"chars": 411,
"preview": "\nimport Qs from 'qs'\nexport default {\n //获取音乐播放链接\n getMusicUrl:{\n method:'get',\n url:'https://api.mlwei.com/musi"
},
{
"path": "src/components/Bandstand/index.js",
"chars": 4894,
"preview": "import React from 'react';\nimport {bindActionCreators} from 'redux';\nimport classnames from 'classnames';\nimport Control"
},
{
"path": "src/components/Bandstand/style.scss",
"chars": 1570,
"preview": ".qqmusic-home-footer {\n position: relative;\n display: flex;\n align-items: center;\n height: 65px;\n backgro"
},
{
"path": "src/components/Control/index.js",
"chars": 7194,
"preview": "import React from 'react';\nimport { connect } from 'react-redux';\nimport {bindActionCreators} from 'redux';\nimport class"
},
{
"path": "src/components/Control/style.scss",
"chars": 6326,
"preview": ".qqmusic-control {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n transform: trans"
},
{
"path": "src/components/Header/index.js",
"chars": 3127,
"preview": "import React from 'react';\nimport { NavLink } from 'react-router-dom';\nimport { Popover, Toast } from 'antd-mobile';\nimp"
},
{
"path": "src/components/Header/style.scss",
"chars": 1966,
"preview": "// 头部\n$headerFontColor:#dbf3e8;\n$headerMainBackgroundColor:#31c37c;\n$headerSearchBackgroundColor:#2AAA73;\n@mixin flexCen"
},
{
"path": "src/components/Loading/index.js",
"chars": 337,
"preview": "import React from 'react';\nimport './style.scss';\n\nexport default function loading() {\n return (\n <div className=\"co"
},
{
"path": "src/components/Loading/style.scss",
"chars": 3540,
"preview": "\n@import '@/scss/variable.scss';\n.comp-loading {\n display: flex;\n justify-content: center;\n align-items: center;\n he"
},
{
"path": "src/components/MusicList/index.js",
"chars": 2839,
"preview": "import React from 'react';\nimport { connect } from 'react-redux';\nimport {bindActionCreators} from 'redux';\nimport class"
},
{
"path": "src/components/MusicList/style.scss",
"chars": 2244,
"preview": ".qqmusic-music-list-wrapper{\n .qqmusic-music-list-bg{\n position: fixed;\n top:0;\n right:0;\n "
},
{
"path": "src/components/NewSongMenu/index.js",
"chars": 2530,
"preview": "import React from 'react';\nimport { connect } from 'react-redux';\nimport {bindActionCreators} from 'redux';\nimport class"
},
{
"path": "src/components/NewSongMenu/style.scss",
"chars": 1597,
"preview": ".qqmusic-new-songmenu{\n position: fixed;\n top:0;\n left: 0;\n width: 100%;\n height: 100%;\n transform: tr"
},
{
"path": "src/components/Search/index.js",
"chars": 8885,
"preview": "import React from 'react';\nimport { connect } from 'react-redux';\nimport {bindActionCreators} from 'redux';\nimport class"
},
{
"path": "src/components/Search/style.scss",
"chars": 4724,
"preview": ".qqmusic-search-wrapper {\n position: relative;\n display: flex;\n flex-direction: column;\n position: fixed;\n "
},
{
"path": "src/components/Slider/index.js",
"chars": 4844,
"preview": "import React from 'react';\nimport classnames from 'classnames';\nimport {Switch } from 'antd-mobile';\nimport { createForm"
},
{
"path": "src/components/Slider/style.scss",
"chars": 2967,
"preview": "// 菜单浮层\n.qqmusic-slider-bg {\n position: fixed;\n left: 0;\n top: 0;\n right: 0;\n bottom: 0;\n background-c"
},
{
"path": "src/components/SongMenu/index.js",
"chars": 4271,
"preview": "import React from 'react';\nimport { connect } from 'react-redux';\nimport {bindActionCreators} from 'redux';\nimport class"
},
{
"path": "src/components/SongMenu/style.scss",
"chars": 3308,
"preview": ".qqmusic-mycenter-bottom {\n text-align: center;\n margin-top: 5px;\n background: #fff;\n line-height: 40px;\n "
},
{
"path": "src/components/SongMenuMangement/index.js",
"chars": 3492,
"preview": "import React from 'react';\nimport { connect } from 'react-redux';\nimport {bindActionCreators} from 'redux';\nimport {Chec"
},
{
"path": "src/components/SongMenuMangement/style.scss",
"chars": 2791,
"preview": ".qqmusic-songmenu-mangement{\n display: flex;\n flex-direction: column;\n position: fixed;\n top:0;\n left: 0;"
},
{
"path": "src/constant/music.js",
"chars": 558,
"preview": "//播放或暂停音乐\nexport const CHANGE_MUSIC_STATUS=\"CHANGE_MUSIC_STATUS\";\n//添加音乐到播放列表\nexport const ADD_MUSIC=\"ADD_MUSIC\";\n//更改当前"
},
{
"path": "src/layouts/MainLayout/index.js",
"chars": 541,
"preview": "import React from 'react';\nimport Header from '@/components/Header';\nimport Bandstand from '@/components/Bandstand';\nimp"
},
{
"path": "src/layouts/MainLayout/style.scss",
"chars": 138,
"preview": ".qqmusic-home{\n display: flex;\n flex-direction: column;\n height: 100%;\n}\n.qqmusic-home-body{\n flex: 1;\n o"
},
{
"path": "src/main.js",
"chars": 440,
"preview": "/**\n * Created by wuming on 2017/7/11.\n */\n// import '@/utils/antm-viewport.min';\nimport '@/scss/reset.scss';\nimport Rea"
},
{
"path": "src/pages/Discovery/index.js",
"chars": 5634,
"preview": "import React from 'react';\nimport './style.scss';\nclass Discovery extends React.Component {\n render() {\n const"
},
{
"path": "src/pages/Discovery/style.scss",
"chars": 2346,
"preview": ".qqmusic-discovery-item {\n display: flex;\n padding: 15px 0 0 15px;\n .qqmusic-discovery-item-left {\n flex"
},
{
"path": "src/pages/MusicClub/index.js",
"chars": 5114,
"preview": "import React from 'react';\nimport { Carousel, Grid } from 'antd-mobile';\nimport './style.scss';\nclass MusicClub extends "
},
{
"path": "src/pages/MusicClub/style.scss",
"chars": 3143,
"preview": ".qqmusic-home-body {\n background: #fff;\n .slideshow-list {\n min-height: 154px;\n .slideshow-item-link"
},
{
"path": "src/pages/MyCenter/index.js",
"chars": 4530,
"preview": "import React from 'react';\nimport { Grid } from 'antd-mobile';\nimport './style.scss';\nimport auditionImg from '@/assets/"
},
{
"path": "src/pages/MyCenter/style.scss",
"chars": 3344,
"preview": ".qqmusic-home-body {\n .qqmusic-mycenter-top {\n padding: 5px 0 20px;\n background: #f5f5f9;\n .qqmu"
},
{
"path": "src/router/index.js",
"chars": 822,
"preview": "import Loadable from 'react-loadable';\nimport createBrowserHistory from 'history/createBrowserHistory';\nimport MainLayou"
},
{
"path": "src/scss/reset.scss",
"chars": 732,
"preview": "*{\n margin: 0;\n padding: 0;\n}\nhtml,body{\n height: 100%;\n}\nli{\n list-style: none;\n}\na{\n text-decoration: none;\n}\ninp"
},
{
"path": "src/scss/variable.scss",
"chars": 120,
"preview": "$primary-color:#31c37c;\n$text-color-secondary:rgba(0,0,0,.45);\n$screen-xl: 1200px;\n$screen-sm: 576px;\n$screen-xs: 480px;"
},
{
"path": "src/store/actionTypes.js",
"chars": 568,
"preview": "//播放或暂停音乐\nexport const CHANGE_MUSIC_STATUS=\"CHANGE_MUSIC_STATUS\";\n//添加音乐到播放列表\nexport const ADD_MUSIC=\"ADD_MUSIC\";\n//更改当前"
},
{
"path": "src/store/actions.js",
"chars": 1543,
"preview": "import * as actionTypes from './actionTypes';\n//添加音乐\nexport const addMusic=(data,callback)=> async (dispatch,getState,{A"
},
{
"path": "src/store/index.js",
"chars": 540,
"preview": "import { createStore,combineReducers, applyMiddleware, compose } from 'redux';\nimport thunk from 'redux-thunk';\nimport {"
},
{
"path": "src/store/reducer.js",
"chars": 3365,
"preview": "import * as actionTypes from './actionTypes';\nconst initialState = {\n currentMusic: {},\n isPlay: false,\n isCurr"
},
{
"path": "src/utils/http.js",
"chars": 902,
"preview": "import axios from 'axios';\nimport {Toast} from 'antd-mobile';\nconst instance=axios.create({\n //超时时间\n timeout:3000,\n /"
},
{
"path": "src/utils/index.js",
"chars": 1162,
"preview": "export default {\n //格式化数字位数\n \"fix\":function(num, length){\n return ('' + num).length < length ? ((new Array("
},
{
"path": "theme.js",
"chars": 46,
"preview": "module.exports={\n \"brand-primary\":\"#31c37c\"\n}"
}
]
About this extraction
This page contains the full source code of the ruichengping/react-mobile-qqMusic GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 58 files (130.1 KB), approximately 31.8k tokens, and a symbol index with 77 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.