├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .postcssrc.js ├── README.md ├── build ├── build.js ├── check-versions.js ├── logo.png ├── utils.js ├── vue-loader.conf.js ├── webpack.base.conf.js ├── webpack.dev.conf.js └── webpack.prod.conf.js ├── config ├── dev.env.js ├── index.js └── prod.env.js ├── cursor.cur ├── favicon.ico ├── favicon.png ├── index.html ├── package-lock.json ├── package.json ├── src ├── App.vue ├── api │ ├── search.js │ └── song.js ├── assets │ ├── bg.jpg │ ├── bgvideo480.mp4 │ ├── cursor.cur │ ├── head.jpg │ ├── siyecao1.jpg │ ├── siyecao2.jpg │ ├── siyecao3.jpg │ └── userbg.jpg ├── base │ ├── blog-box │ │ └── blog-box.vue │ ├── board │ │ └── board.vue │ ├── player │ │ └── player.vue │ ├── progress-bar │ │ └── progress-bar.vue │ └── tags │ │ └── tags.vue ├── blogs │ ├── blog.md │ ├── imgs │ │ ├── image-20200818160039299.png │ │ ├── image-20200818163913139-1598429442173.png │ │ ├── image-20200818163913139.png │ │ ├── image-20200819143501237.png │ │ ├── image-20200820222252811.png │ │ ├── image-20200822165552890-1598429509454.png │ │ ├── image-20200822165552890.png │ │ ├── image-20200822172328629.png │ │ ├── image-20200822172553110.png │ │ ├── image-20200822174649258.png │ │ ├── image-20200824161718758.png │ │ ├── image-20200826154211160.png │ │ ├── image-20200826154240558.png │ │ ├── image-20200826213007574.png │ │ ├── image-20200826213021445.png │ │ ├── image-20200826213037228.png │ │ └── image-20200826213524137.png │ ├── test.md │ └── 博客主页开发日志.md ├── common │ ├── css │ │ └── index.css │ └── js │ │ ├── cache.js │ │ ├── config.js │ │ ├── lyric.js │ │ ├── song.js │ │ └── util.js ├── components │ ├── bg │ │ └── bg.vue │ ├── bottom │ │ └── bottom.vue │ ├── center-content │ │ └── center-content.vue │ ├── center │ │ └── center.vue │ ├── left-content │ │ └── left-content.vue │ ├── m-header │ │ └── m-header.vue │ ├── main-content │ │ └── main-content.vue │ ├── md-view │ │ └── md-view.vue │ ├── music-box │ │ └── music-box.vue │ ├── right-content │ │ └── right-content.vue │ ├── search-box │ │ └── search-box.vue │ ├── tab │ │ └── tab.vue │ └── user-info │ │ └── user-info.vue ├── main.js └── router │ └── index.js └── static └── .gitkeep /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "modules": false, 5 | "targets": { 6 | "browsers": ["> 1%", "last 2 versions", "not ie <= 8"] 7 | } 8 | }], 9 | "stage-2" 10 | ], 11 | "plugins": ["transform-vue-jsx", "transform-runtime"] 12 | } 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | /config/ 3 | /dist/ 4 | /*.js 5 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // https://eslint.org/docs/user-guide/configuring 2 | 3 | module.exports = { 4 | root: true, 5 | parserOptions: { 6 | parser: 'babel-eslint' 7 | }, 8 | env: { 9 | browser: true, 10 | }, 11 | extends: [ 12 | // https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention 13 | // consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules. 14 | 'plugin:vue/essential', 15 | // https://github.com/standard/standard/blob/master/docs/RULES-en.md 16 | 'standard' 17 | ], 18 | // required to lint *.vue files 19 | plugins: [ 20 | 'vue' 21 | ], 22 | // add your custom rules here 23 | rules: { 24 | // allow async-await 25 | 'generator-star-spacing': 'off', 26 | // allow debugger during development 27 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off' 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | /dist/ 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Editor directories and files 9 | .idea 10 | .vscode 11 | *.suo 12 | *.ntvs* 13 | *.njsproj 14 | *.sln 15 | -------------------------------------------------------------------------------- /.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | "plugins": { 5 | "postcss-import": {}, 6 | "postcss-url": {}, 7 | // to edit target browsers: use "browserslist" field in package.json 8 | "autoprefixer": {} 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-blog 2 | 一个基于Vue的个人博客,为了初学Vue和记录笔记而打造,后端还在学习开发中。 3 | 4 | ## 博客地址 5 | 四叶草的博客:www.luckyclover.top 6 | 7 | 觉得不错的点个star吧 8 | 9 | ## 小白教程 10 | 11 | 0. 安装[Node.js](https://nodejs.org/en/) 12 | 13 | 1. 下载代码,vscode打开根目录,终端执行 14 | 15 | ``` 16 | npm install //下载项目依赖 17 | ``` 18 | 19 | 2. 本地调试(退出 Ctrl + C) 20 | 21 | ``` 22 | npm start 23 | ``` 24 | 25 | 3. 打包 26 | 27 | 28 | 终端运行 29 | 30 | ``` 31 | npm run build 32 | ``` 33 | 34 | 4. 部署 35 | 36 | 打开生成的dist目录,复制里面的内容到你的网站上即可 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /build/build.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | require('./check-versions')() 3 | 4 | process.env.NODE_ENV = 'production' 5 | 6 | const ora = require('ora') 7 | const rm = require('rimraf') 8 | const path = require('path') 9 | const chalk = require('chalk') 10 | const webpack = require('webpack') 11 | const config = require('../config') 12 | const webpackConfig = require('./webpack.prod.conf') 13 | 14 | const spinner = ora('building for production...') 15 | spinner.start() 16 | 17 | rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => { 18 | if (err) throw err 19 | webpack(webpackConfig, (err, stats) => { 20 | spinner.stop() 21 | if (err) throw err 22 | process.stdout.write(stats.toString({ 23 | colors: true, 24 | modules: false, 25 | children: false, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build. 26 | chunks: false, 27 | chunkModules: false 28 | }) + '\n\n') 29 | 30 | if (stats.hasErrors()) { 31 | console.log(chalk.red(' Build failed with errors.\n')) 32 | process.exit(1) 33 | } 34 | 35 | console.log(chalk.cyan(' Build complete.\n')) 36 | console.log(chalk.yellow( 37 | ' Tip: built files are meant to be served over an HTTP server.\n' + 38 | ' Opening index.html over file:// won\'t work.\n' 39 | )) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /build/check-versions.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const chalk = require('chalk') 3 | const semver = require('semver') 4 | const packageConfig = require('../package.json') 5 | const shell = require('shelljs') 6 | 7 | function exec (cmd) { 8 | return require('child_process').execSync(cmd).toString().trim() 9 | } 10 | 11 | const versionRequirements = [ 12 | { 13 | name: 'node', 14 | currentVersion: semver.clean(process.version), 15 | versionRequirement: packageConfig.engines.node 16 | } 17 | ] 18 | 19 | if (shell.which('npm')) { 20 | versionRequirements.push({ 21 | name: 'npm', 22 | currentVersion: exec('npm --version'), 23 | versionRequirement: packageConfig.engines.npm 24 | }) 25 | } 26 | 27 | module.exports = function () { 28 | const warnings = [] 29 | 30 | for (let i = 0; i < versionRequirements.length; i++) { 31 | const mod = versionRequirements[i] 32 | 33 | if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { 34 | warnings.push(mod.name + ': ' + 35 | chalk.red(mod.currentVersion) + ' should be ' + 36 | chalk.green(mod.versionRequirement) 37 | ) 38 | } 39 | } 40 | 41 | if (warnings.length) { 42 | console.log('') 43 | console.log(chalk.yellow('To use this template, you must update following to modules:')) 44 | console.log() 45 | 46 | for (let i = 0; i < warnings.length; i++) { 47 | const warning = warnings[i] 48 | console.log(' ' + warning) 49 | } 50 | 51 | console.log() 52 | process.exit(1) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /build/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iCodek/vue-blog/27998e1559ee7813c9ffc6560dc41f4f76b4c953/build/logo.png -------------------------------------------------------------------------------- /build/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const config = require('../config') 4 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 5 | const packageConfig = require('../package.json') 6 | 7 | exports.assetsPath = function (_path) { 8 | const assetsSubDirectory = process.env.NODE_ENV === 'production' 9 | ? config.build.assetsSubDirectory 10 | : config.dev.assetsSubDirectory 11 | 12 | return path.posix.join(assetsSubDirectory, _path) 13 | } 14 | 15 | exports.cssLoaders = function (options) { 16 | options = options || {} 17 | 18 | const cssLoader = { 19 | loader: 'css-loader', 20 | options: { 21 | sourceMap: options.sourceMap 22 | } 23 | } 24 | 25 | const postcssLoader = { 26 | loader: 'postcss-loader', 27 | options: { 28 | sourceMap: options.sourceMap 29 | } 30 | } 31 | 32 | // generate loader string to be used with extract text plugin 33 | function generateLoaders (loader, loaderOptions) { 34 | const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader] 35 | 36 | if (loader) { 37 | loaders.push({ 38 | loader: loader + '-loader', 39 | options: Object.assign({}, loaderOptions, { 40 | sourceMap: options.sourceMap 41 | }) 42 | }) 43 | } 44 | 45 | // Extract CSS when that option is specified 46 | // (which is the case during production build) 47 | if (options.extract) { 48 | return ExtractTextPlugin.extract({ 49 | use: loaders, 50 | publicPath: '../../', 51 | fallback: 'vue-style-loader' 52 | }) 53 | } else { 54 | return ['vue-style-loader'].concat(loaders) 55 | } 56 | } 57 | 58 | // https://vue-loader.vuejs.org/en/configurations/extract-css.html 59 | return { 60 | css: generateLoaders(), 61 | postcss: generateLoaders(), 62 | less: generateLoaders('less'), 63 | sass: generateLoaders('sass', { indentedSyntax: true }), 64 | scss: generateLoaders('sass'), 65 | stylus: generateLoaders('stylus'), 66 | styl: generateLoaders('stylus') 67 | } 68 | } 69 | 70 | // Generate loaders for standalone style files (outside of .vue) 71 | exports.styleLoaders = function (options) { 72 | const output = [] 73 | const loaders = exports.cssLoaders(options) 74 | 75 | for (const extension in loaders) { 76 | const loader = loaders[extension] 77 | output.push({ 78 | test: new RegExp('\\.' + extension + '$'), 79 | use: loader 80 | }) 81 | } 82 | 83 | return output 84 | } 85 | 86 | exports.createNotifierCallback = () => { 87 | const notifier = require('node-notifier') 88 | 89 | return (severity, errors) => { 90 | if (severity !== 'error') return 91 | 92 | const error = errors[0] 93 | const filename = error.file && error.file.split('!').pop() 94 | 95 | notifier.notify({ 96 | title: packageConfig.name, 97 | message: severity + ': ' + error.name, 98 | subtitle: filename || '', 99 | icon: path.join(__dirname, 'logo.png') 100 | }) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /build/vue-loader.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const utils = require('./utils') 3 | const config = require('../config') 4 | const isProduction = process.env.NODE_ENV === 'production' 5 | const sourceMapEnabled = isProduction 6 | ? config.build.productionSourceMap 7 | : config.dev.cssSourceMap 8 | 9 | module.exports = { 10 | loaders: utils.cssLoaders({ 11 | sourceMap: sourceMapEnabled, 12 | extract: isProduction 13 | }), 14 | cssSourceMap: sourceMapEnabled, 15 | cacheBusting: config.dev.cacheBusting, 16 | transformToRequire: { 17 | video: ['src', 'poster'], 18 | source: 'src', 19 | img: 'src', 20 | image: 'xlink:href' 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /build/webpack.base.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const utils = require('./utils') 4 | const config = require('../config') 5 | const vueLoaderConfig = require('./vue-loader.conf') 6 | 7 | function resolve (dir) { 8 | return path.join(__dirname, '..', dir) 9 | } 10 | 11 | const createLintingRule = () => ({ 12 | test: /\.(js|vue)$/, 13 | loader: 'eslint-loader', 14 | enforce: 'pre', 15 | include: [resolve('src'), resolve('test')], 16 | options: { 17 | formatter: require('eslint-friendly-formatter'), 18 | emitWarning: !config.dev.showEslintErrorsInOverlay 19 | } 20 | }) 21 | 22 | module.exports = { 23 | context: path.resolve(__dirname, '../'), 24 | entry: { 25 | app: './src/main.js' 26 | }, 27 | output: { 28 | path: config.build.assetsRoot, 29 | filename: '[name].js', 30 | publicPath: process.env.NODE_ENV === 'production' 31 | ? config.build.assetsPublicPath 32 | : config.dev.assetsPublicPath 33 | }, 34 | resolve: { 35 | extensions: ['.js', '.vue', '.json'], 36 | alias: { 37 | 'vue$': 'vue/dist/vue.esm.js', 38 | '@': resolve('src'), 39 | 'common': resolve('src/common'), 40 | 'cpnts': resolve('src/components'), 41 | 'base': resolve('src/base'), 42 | 'api': resolve('src/api'), 43 | 'blogs': resolve('src/blogs') 44 | } 45 | }, 46 | module: { 47 | rules: [ 48 | ...(config.dev.useEslint ? [createLintingRule()] : []), 49 | { 50 | test: /\.vue$/, 51 | loader: 'vue-loader', 52 | options: vueLoaderConfig 53 | }, 54 | { 55 | test: /\.js$/, 56 | loader: 'babel-loader', 57 | include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')] 58 | }, 59 | { 60 | test: /\.(png|jpe?g|gif|svg|cur)(\?.*)?$/, 61 | loader: 'url-loader', 62 | options: { 63 | limit: 10000, 64 | name: utils.assetsPath('img/[name].[hash:7].[ext]') 65 | } 66 | }, 67 | { 68 | test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, 69 | loader: 'url-loader', 70 | options: { 71 | limit: 10000, 72 | name: utils.assetsPath('media/[name].[hash:7].[ext]') 73 | } 74 | }, 75 | { 76 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 77 | loader: 'url-loader', 78 | options: { 79 | limit: 10000, 80 | name: utils.assetsPath('fonts/[name].[hash:7].[ext]') 81 | } 82 | }, 83 | { 84 | test: /\.md$/, 85 | use: [ 86 | { 87 | loader: "html-loader" 88 | }, 89 | { 90 | loader: "markdown-loader", 91 | options: {} 92 | } 93 | ] 94 | } 95 | ] 96 | }, 97 | node: { 98 | // prevent webpack from injecting useless setImmediate polyfill because Vue 99 | // source contains it (although only uses it if it's native). 100 | setImmediate: false, 101 | // prevent webpack from injecting mocks to Node native modules 102 | // that does not make sense for the client 103 | dgram: 'empty', 104 | fs: 'empty', 105 | net: 'empty', 106 | tls: 'empty', 107 | child_process: 'empty' 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /build/webpack.dev.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const utils = require('./utils') 3 | const webpack = require('webpack') 4 | const config = require('../config') 5 | const merge = require('webpack-merge') 6 | const path = require('path') 7 | const baseWebpackConfig = require('./webpack.base.conf') 8 | const CopyWebpackPlugin = require('copy-webpack-plugin') 9 | const HtmlWebpackPlugin = require('html-webpack-plugin') 10 | const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') 11 | const portfinder = require('portfinder') 12 | 13 | const HOST = process.env.HOST 14 | const PORT = process.env.PORT && Number(process.env.PORT) 15 | 16 | const devWebpackConfig = merge(baseWebpackConfig, { 17 | module: { 18 | rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true }) 19 | }, 20 | // cheap-module-eval-source-map is faster for development 21 | devtool: config.dev.devtool, 22 | 23 | // these devServer options should be customized in /config/index.js 24 | devServer: { 25 | clientLogLevel: 'warning', 26 | historyApiFallback: { 27 | rewrites: [ 28 | { from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') }, 29 | ], 30 | }, 31 | hot: true, 32 | contentBase: false, // since we use CopyWebpackPlugin. 33 | compress: true, 34 | host: HOST || config.dev.host, 35 | port: PORT || config.dev.port, 36 | open: config.dev.autoOpenBrowser, 37 | overlay: config.dev.errorOverlay 38 | ? { warnings: false, errors: true } 39 | : false, 40 | publicPath: config.dev.assetsPublicPath, 41 | proxy: config.dev.proxyTable, 42 | quiet: true, // necessary for FriendlyErrorsPlugin 43 | watchOptions: { 44 | poll: config.dev.poll, 45 | } 46 | }, 47 | plugins: [ 48 | new webpack.DefinePlugin({ 49 | 'process.env': require('../config/dev.env') 50 | }), 51 | new webpack.HotModuleReplacementPlugin(), 52 | new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update. 53 | new webpack.NoEmitOnErrorsPlugin(), 54 | // https://github.com/ampedandwired/html-webpack-plugin 55 | new HtmlWebpackPlugin({ 56 | filename: 'index.html', 57 | template: 'index.html', 58 | inject: true, 59 | favicon: path.resolve('favicon.png') 60 | }), 61 | // copy custom static assets 62 | new CopyWebpackPlugin([ 63 | { 64 | from: path.resolve(__dirname, '../static'), 65 | to: config.dev.assetsSubDirectory, 66 | ignore: ['.*'] 67 | } 68 | ]) 69 | ] 70 | }) 71 | 72 | module.exports = new Promise((resolve, reject) => { 73 | portfinder.basePort = process.env.PORT || config.dev.port 74 | portfinder.getPort((err, port) => { 75 | if (err) { 76 | reject(err) 77 | } else { 78 | // publish the new Port, necessary for e2e tests 79 | process.env.PORT = port 80 | // add port to devServer config 81 | devWebpackConfig.devServer.port = port 82 | 83 | // Add FriendlyErrorsPlugin 84 | devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({ 85 | compilationSuccessInfo: { 86 | messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`], 87 | }, 88 | onErrors: config.dev.notifyOnErrors 89 | ? utils.createNotifierCallback() 90 | : undefined 91 | })) 92 | 93 | resolve(devWebpackConfig) 94 | } 95 | }) 96 | }) 97 | -------------------------------------------------------------------------------- /build/webpack.prod.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const utils = require('./utils') 4 | const webpack = require('webpack') 5 | const config = require('../config') 6 | const merge = require('webpack-merge') 7 | const baseWebpackConfig = require('./webpack.base.conf') 8 | const CopyWebpackPlugin = require('copy-webpack-plugin') 9 | const HtmlWebpackPlugin = require('html-webpack-plugin') 10 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 11 | const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin') 12 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin') 13 | 14 | const env = require('../config/prod.env') 15 | 16 | const webpackConfig = merge(baseWebpackConfig, { 17 | module: { 18 | rules: utils.styleLoaders({ 19 | sourceMap: config.build.productionSourceMap, 20 | extract: true, 21 | usePostCSS: true 22 | }) 23 | }, 24 | devtool: config.build.productionSourceMap ? config.build.devtool : false, 25 | output: { 26 | path: config.build.assetsRoot, 27 | filename: utils.assetsPath('js/[name].[chunkhash].js'), 28 | chunkFilename: utils.assetsPath('js/[id].[chunkhash].js') 29 | }, 30 | plugins: [ 31 | // http://vuejs.github.io/vue-loader/en/workflow/production.html 32 | new webpack.DefinePlugin({ 33 | 'process.env': env 34 | }), 35 | new UglifyJsPlugin({ 36 | uglifyOptions: { 37 | compress: { 38 | warnings: false 39 | } 40 | }, 41 | sourceMap: config.build.productionSourceMap, 42 | parallel: true 43 | }), 44 | // extract css into its own file 45 | new ExtractTextPlugin({ 46 | filename: utils.assetsPath('css/[name].[contenthash].css'), 47 | // Setting the following option to `false` will not extract CSS from codesplit chunks. 48 | // Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack. 49 | // It's currently set to `true` because we are seeing that sourcemaps are included in the codesplit bundle as well when it's `false`, 50 | // increasing file size: https://github.com/vuejs-templates/webpack/issues/1110 51 | allChunks: true, 52 | }), 53 | // Compress extracted CSS. We are using this plugin so that possible 54 | // duplicated CSS from different components can be deduped. 55 | new OptimizeCSSPlugin({ 56 | cssProcessorOptions: config.build.productionSourceMap 57 | ? { safe: true, map: { inline: false } } 58 | : { safe: true } 59 | }), 60 | // generate dist index.html with correct asset hash for caching. 61 | // you can customize output by editing /index.html 62 | // see https://github.com/ampedandwired/html-webpack-plugin 63 | new HtmlWebpackPlugin({ 64 | filename: config.build.index, 65 | template: 'index.html', 66 | inject: true, 67 | favicon: path.resolve('favicon.png'), 68 | minify: { 69 | removeComments: true, 70 | collapseWhitespace: true, 71 | removeAttributeQuotes: true 72 | // more options: 73 | // https://github.com/kangax/html-minifier#options-quick-reference 74 | }, 75 | // necessary to consistently work with multiple chunks via CommonsChunkPlugin 76 | chunksSortMode: 'dependency' 77 | }), 78 | // keep module.id stable when vendor modules does not change 79 | new webpack.HashedModuleIdsPlugin(), 80 | // enable scope hoisting 81 | new webpack.optimize.ModuleConcatenationPlugin(), 82 | // split vendor js into its own file 83 | new webpack.optimize.CommonsChunkPlugin({ 84 | name: 'vendor', 85 | minChunks (module) { 86 | // any required modules inside node_modules are extracted to vendor 87 | return ( 88 | module.resource && 89 | /\.js$/.test(module.resource) && 90 | module.resource.indexOf( 91 | path.join(__dirname, '../node_modules') 92 | ) === 0 93 | ) 94 | } 95 | }), 96 | // extract webpack runtime and module manifest to its own file in order to 97 | // prevent vendor hash from being updated whenever app bundle is updated 98 | new webpack.optimize.CommonsChunkPlugin({ 99 | name: 'manifest', 100 | minChunks: Infinity 101 | }), 102 | // This instance extracts shared chunks from code splitted chunks and bundles them 103 | // in a separate chunk, similar to the vendor chunk 104 | // see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk 105 | new webpack.optimize.CommonsChunkPlugin({ 106 | name: 'app', 107 | async: 'vendor-async', 108 | children: true, 109 | minChunks: 3 110 | }), 111 | 112 | // copy custom static assets 113 | new CopyWebpackPlugin([ 114 | { 115 | from: path.resolve(__dirname, '../static'), 116 | to: config.build.assetsSubDirectory, 117 | ignore: ['.*'] 118 | } 119 | ]) 120 | ] 121 | }) 122 | 123 | if (config.build.productionGzip) { 124 | const CompressionWebpackPlugin = require('compression-webpack-plugin') 125 | 126 | webpackConfig.plugins.push( 127 | new CompressionWebpackPlugin({ 128 | asset: '[path].gz[query]', 129 | algorithm: 'gzip', 130 | test: new RegExp( 131 | '\\.(' + 132 | config.build.productionGzipExtensions.join('|') + 133 | ')$' 134 | ), 135 | threshold: 10240, 136 | minRatio: 0.8 137 | }) 138 | ) 139 | } 140 | 141 | if (config.build.bundleAnalyzerReport) { 142 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin 143 | webpackConfig.plugins.push(new BundleAnalyzerPlugin()) 144 | } 145 | 146 | module.exports = webpackConfig 147 | -------------------------------------------------------------------------------- /config/dev.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const merge = require('webpack-merge') 3 | const prodEnv = require('./prod.env') 4 | 5 | module.exports = merge(prodEnv, { 6 | NODE_ENV: '"development"' 7 | }) 8 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | // Template version: 1.3.1 3 | // see http://vuejs-templates.github.io/webpack for documentation. 4 | 5 | const path = require('path') 6 | 7 | module.exports = { 8 | dev: { 9 | 10 | // Paths 11 | assetsSubDirectory: 'static', 12 | assetsPublicPath: '/', 13 | proxyTable: {}, 14 | 15 | // Various Dev Server settings 16 | host: 'localhost', // can be overwritten by process.env.HOST 17 | port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined 18 | autoOpenBrowser: false, 19 | errorOverlay: true, 20 | notifyOnErrors: true, 21 | poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions- 22 | 23 | // Use Eslint Loader? 24 | // If true, your code will be linted during bundling and 25 | // linting errors and warnings will be shown in the console. 26 | useEslint: true, 27 | // If true, eslint errors and warnings will also be shown in the error overlay 28 | // in the browser. 29 | showEslintErrorsInOverlay: false, 30 | 31 | /** 32 | * Source Maps 33 | */ 34 | 35 | // https://webpack.js.org/configuration/devtool/#development 36 | devtool: 'cheap-module-eval-source-map', 37 | 38 | // If you have problems debugging vue-files in devtools, 39 | // set this to false - it *may* help 40 | // https://vue-loader.vuejs.org/en/options.html#cachebusting 41 | cacheBusting: true, 42 | 43 | cssSourceMap: true 44 | }, 45 | 46 | build: { 47 | // Template for index.html 48 | index: path.resolve(__dirname, '../dist/index.html'), 49 | 50 | // Paths 51 | assetsRoot: path.resolve(__dirname, '../dist'), 52 | assetsSubDirectory: 'static', 53 | assetsPublicPath: '/', 54 | 55 | /** 56 | * Source Maps 57 | */ 58 | 59 | productionSourceMap: true, 60 | // https://webpack.js.org/configuration/devtool/#production 61 | devtool: '#source-map', 62 | 63 | // Gzip off by default as many popular static hosts such as 64 | // Surge or Netlify already gzip all static assets for you. 65 | // Before setting to `true`, make sure to: 66 | // npm install --save-dev compression-webpack-plugin 67 | productionGzip: false, 68 | productionGzipExtensions: ['js', 'css'], 69 | 70 | // Run the build command with an extra argument to 71 | // View the bundle analyzer report after build finishes: 72 | // `npm run build --report` 73 | // Set to `true` or `false` to always turn it on or off 74 | bundleAnalyzerReport: process.env.npm_config_report 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /config/prod.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | NODE_ENV: '"production"' 4 | } 5 | -------------------------------------------------------------------------------- /cursor.cur: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iCodek/vue-blog/27998e1559ee7813c9ffc6560dc41f4f76b4c953/cursor.cur -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iCodek/vue-blog/27998e1559ee7813c9ffc6560dc41f4f76b4c953/favicon.ico -------------------------------------------------------------------------------- /favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iCodek/vue-blog/27998e1559ee7813c9ffc6560dc41f4f76b4c953/favicon.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 四叶草的博客 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-blog", 3 | "version": "1.0.0", 4 | "description": "第一个博客主页", 5 | "author": "iCodek <790477428@qq.com>", 6 | "private": true, 7 | "scripts": { 8 | "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js", 9 | "start": "npm run dev", 10 | "lint": "eslint --ext .js,.vue src", 11 | "build": "node build/build.js" 12 | }, 13 | "dependencies": { 14 | "@better-scroll/core": "^2.0.0-alpha.4", 15 | "axios": "^0.18.0", 16 | "babel-runtime": "^6.26.0", 17 | "better-scroll": "^2.0.0-beta.10", 18 | "create-keyframe-animation": "0.1.0", 19 | "fastclick": "^1.0.6", 20 | "good-storage": "^1.0.1", 21 | "lyric-parser": "^1.0.1", 22 | "pinyin": "^2.8.3", 23 | "vue": "^2.5.2", 24 | "vue-lazyload": "^1.2.3", 25 | "vue-router": "^3.0.1", 26 | "vuex": "^3.0.1", 27 | "vuescroll": "4.16.1", 28 | "mavon-editor": "2.9.0", 29 | "markdown-loader": "5.1.0", 30 | "html-loader": "1.3.0", 31 | "highlight.js": "10.1.2" 32 | }, 33 | "devDependencies": { 34 | "autoprefixer": "^7.1.2", 35 | "babel-core": "^6.22.1", 36 | "babel-eslint": "^8.2.1", 37 | "babel-helper-vue-jsx-merge-props": "^2.0.3", 38 | "babel-loader": "^7.1.1", 39 | "babel-plugin-syntax-jsx": "^6.18.0", 40 | "babel-plugin-transform-runtime": "^6.22.0", 41 | "babel-plugin-transform-vue-jsx": "^3.5.0", 42 | "babel-preset-env": "^1.3.2", 43 | "babel-preset-stage-2": "^6.22.0", 44 | "chalk": "^2.0.1", 45 | "copy-webpack-plugin": "^4.0.1", 46 | "css-loader": "^0.28.0", 47 | "eslint": "^4.15.0", 48 | "eslint-config-standard": "^10.2.1", 49 | "eslint-friendly-formatter": "^3.0.0", 50 | "eslint-loader": "^1.7.1", 51 | "eslint-plugin-import": "^2.7.0", 52 | "eslint-plugin-node": "^5.2.0", 53 | "eslint-plugin-promise": "^3.4.0", 54 | "eslint-plugin-standard": "^3.0.1", 55 | "eslint-plugin-vue": "^4.0.0", 56 | "extract-text-webpack-plugin": "^3.0.0", 57 | "file-loader": "^1.1.4", 58 | "friendly-errors-webpack-plugin": "^1.6.1", 59 | "html-webpack-plugin": "^2.30.1", 60 | "node-notifier": "^5.1.2", 61 | "node-sass": "^4.9.0", 62 | "optimize-css-assets-webpack-plugin": "^3.2.0", 63 | "ora": "^1.2.0", 64 | "portfinder": "^1.0.13", 65 | "postcss-import": "^11.0.0", 66 | "postcss-loader": "^2.0.8", 67 | "postcss-url": "^7.2.1", 68 | "rimraf": "^2.6.0", 69 | "semver": "^5.3.0", 70 | "shelljs": "^0.7.6", 71 | "sass-loader": "^7.0.1", 72 | "uglifyjs-webpack-plugin": "^1.1.1", 73 | "url-loader": "^0.5.8", 74 | "vue-loader": "^13.3.0", 75 | "vue-style-loader": "^3.0.1", 76 | "vue-template-compiler": "^2.5.2", 77 | "webpack": "^3.6.0", 78 | "webpack-bundle-analyzer": "^2.9.0", 79 | "webpack-dev-server": "^2.9.1", 80 | "webpack-merge": "^4.1.0" 81 | }, 82 | "engines": { 83 | "node": ">= 6.0.0", 84 | "npm": ">= 3.0.0" 85 | }, 86 | "browserslist": [ 87 | "> 1%", 88 | "last 2 versions", 89 | "not ie <= 8" 90 | ] 91 | } 92 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 24 | 25 | 35 | -------------------------------------------------------------------------------- /src/api/search.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import {HOST} from 'common/js/config' 3 | 4 | export function getSearchSinger (name) { 5 | const url = HOST + `/search?keywords=${name}&type=100` 6 | 7 | return axios.get(url) 8 | } 9 | 10 | export function getSearchSongs (name, page) { 11 | const url = HOST + `/search?keywords=${name}&offset=${page}` 12 | 13 | return axios.get(url) 14 | } 15 | 16 | export function getSearchSuggest (name) { 17 | const url = HOST + `/search/suggest?keywords=${name}` 18 | 19 | return axios.get(url) 20 | } 21 | 22 | export function getSongDetail (id) { 23 | const url = HOST + `/song/detail?ids=${id}` 24 | 25 | return axios.get(url) 26 | } 27 | 28 | export function getSearchHot (id) { 29 | const url = HOST + `/search/hot` 30 | 31 | return axios.get(url) 32 | } 33 | -------------------------------------------------------------------------------- /src/api/song.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import {HOST} from 'common/js/config' 3 | 4 | export function getSong (id) { 5 | const url = HOST + `/song/url?id=${id}` 6 | 7 | return axios.get(url) 8 | } 9 | 10 | export function getLyric (id) { 11 | const url = HOST + `/lyric?id=${id}` 12 | 13 | return axios.get(url) 14 | } 15 | 16 | export function getSongDetail (id) { 17 | const url = HOST + `/song/detail?ids=${id}` 18 | 19 | return axios.get(url) 20 | } 21 | -------------------------------------------------------------------------------- /src/assets/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iCodek/vue-blog/27998e1559ee7813c9ffc6560dc41f4f76b4c953/src/assets/bg.jpg -------------------------------------------------------------------------------- /src/assets/bgvideo480.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iCodek/vue-blog/27998e1559ee7813c9ffc6560dc41f4f76b4c953/src/assets/bgvideo480.mp4 -------------------------------------------------------------------------------- /src/assets/cursor.cur: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iCodek/vue-blog/27998e1559ee7813c9ffc6560dc41f4f76b4c953/src/assets/cursor.cur -------------------------------------------------------------------------------- /src/assets/head.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iCodek/vue-blog/27998e1559ee7813c9ffc6560dc41f4f76b4c953/src/assets/head.jpg -------------------------------------------------------------------------------- /src/assets/siyecao1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iCodek/vue-blog/27998e1559ee7813c9ffc6560dc41f4f76b4c953/src/assets/siyecao1.jpg -------------------------------------------------------------------------------- /src/assets/siyecao2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iCodek/vue-blog/27998e1559ee7813c9ffc6560dc41f4f76b4c953/src/assets/siyecao2.jpg -------------------------------------------------------------------------------- /src/assets/siyecao3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iCodek/vue-blog/27998e1559ee7813c9ffc6560dc41f4f76b4c953/src/assets/siyecao3.jpg -------------------------------------------------------------------------------- /src/assets/userbg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iCodek/vue-blog/27998e1559ee7813c9ffc6560dc41f4f76b4c953/src/assets/userbg.jpg -------------------------------------------------------------------------------- /src/base/blog-box/blog-box.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 73 | 213 | -------------------------------------------------------------------------------- /src/base/board/board.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 31 | 71 | -------------------------------------------------------------------------------- /src/base/player/player.vue: -------------------------------------------------------------------------------- 1 | 80 | 81 | 362 | 597 | -------------------------------------------------------------------------------- /src/base/progress-bar/progress-bar.vue: -------------------------------------------------------------------------------- 1 | 14 | 116 | 194 | -------------------------------------------------------------------------------- /src/base/tags/tags.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 47 | 48 | 78 | -------------------------------------------------------------------------------- /src/blogs/blog.md: -------------------------------------------------------------------------------- 1 | # 博客主页开发日志 2 | 3 | >## 0. 开篇 4 | > 5 | >今天818,帮弟弟买手机又被狗东坑了一手,真是气啊。言归正传,这是第一篇markdown博客,主要目的呢,是为了熟悉下markdown语法和记录下博客的开发日志,也是第一次边学边做笔记,当初没用现成的wordpress和hexo,就是为了自己实现一下动态博客。开始吧~ 6 | > 7 | >GitHub地址在此:https://github.com/iCodek/vue-blog 8 | 9 | # 1.网站大体样式 10 | 11 | ## 1.1 先创建Vue项目 12 | 13 | ``` 14 | vue init webpack vue-blog 15 | ``` 16 | 17 | 改下结构,加了下面三个目录 18 | 19 | ![image-20200826154211160](imgs/image-20200826154211160.png) 20 | 21 | - api 前后端交互 22 | - base 基本组件 23 | - common 用到的css,js方法 24 | 25 | 26 | 27 | ## 1.2 编写样式 28 | 29 | 主要样子呢 打算照着[小游的博客主页](https://xiaoyou66.com/)画瓢 30 | 31 | ![](imgs/image-20200826154240558.png) 32 | 33 | 观察了下,路由的位置都在中间的绿色区域,那就先把其他的组件位置固定下。 34 | 35 | # 2.各种组件 36 | 37 | ## 2.1 m-header组件 38 | 39 | 创建好了脚手架,突然发现body默认是*margin:* *8px;*晕,在index.html修改下style 40 | 41 | search-box组件用到了icon-font 使用方式是在style中导入(全局可用)忽略了scope 42 | 43 | ```js 44 | @import url("../../common/css/iconfont/iconfont.css"); 45 | ``` 46 | 47 | 分类标题用的弹性布局,也是第一次搞懂了,只要设置父元素display: flex; 在设置子元素的flex大小,是成比例缩放的。其他参数可以[参见](https://www.runoob.com/w3cnote/flex-grammar.html)。 48 | 49 | 还有首页的居中,设置父元素没有高度,然后子元素设置margin即可。 50 | 51 | ------ 52 | 53 | 分割线表示第二天,做了挺多的,主要是遇到了好多坑,来总结一下 54 | 55 | 上面的icon导入方式是黑白的图标,换了一种svg的方式,首先把iconfont的图标加入自己的项目,然后生成自己的代码,例如 56 | 57 | ``` 58 | //at.alicdn.com/t/font_2014472_qa5ruvao5kb.js 59 | ``` 60 | 61 | 然后在index.html引入 62 | 63 | ```html 64 | 65 | ``` 66 | 67 | 然后就不用像上面import了,直接 68 | 69 | ```html 70 | 73 | ``` 74 | 75 | 就能用了,#后面是图标的代码(自己项目里的) 76 | 77 | ## 2.2 content组件 78 | 79 | 中间主要有三列,用了弹性布局 80 | 81 | 父级元素 82 | 83 | ```css 84 | 85 | 86 | 87 | ``` 88 | 89 | 样式 90 | 91 | ```css 92 | display: flex; 93 | ``` 94 | 95 | 再把中间的元素设置样式 96 | 97 | ```css 98 | flex: 1; 99 | ``` 100 | 101 | 另外 为了适配移动端,左右两边当宽度小于500不显示,响应式css代码 102 | 103 | ```css 104 | @media screen and (max-width:500px){ 105 | display: none; 106 | } 107 | ``` 108 | 109 | 默认左右两边的容器直接撑到底下了,查了下父元素要设置**align-items** 110 | 111 | image-20200818160039299 112 | 113 | 大致框架为 114 | 115 | image-20200818163913139 116 | 117 | ## 2.3 user-info组件 118 | 119 | 这里的布局都是用的flex,熟能生巧了。 120 | 121 | 主要难(坑)点 122 | 123 | - 头像要设置hover时变大,那只能是absolute定位,所以在头像div外面套了一个relative定位的div,在把里面的头像div弄成absolute。这样变大也不会影像下面的元素位置。绝对定位想要居中得用left top 50%和transform: translate(-50%, -50%) 124 | 125 | - 头像hover旋转要注意transition里是all,居中定位的transform: translate(-50%, -50%)要保留,所以hover里是transform: translate(-50%, -50%) rotate(360deg); 注意这里还有顺序要求,不然就不转了(不知道为什么) 126 | 127 | ```css 128 | .userheadwrap { 129 | position: relative; 130 | .userhead { 131 | left: 50%; 132 | position: absolute; 133 | width: 90px; 134 | height: 90px; 135 | border-radius: 100%; 136 | border: 4px solid hsla(0,0%,100%,.4); 137 | background-color: red; 138 | background: url('../../assets/siyecao1.jpg') center center no-repeat; 139 | background-size: cover; 140 | transition: all 0.4s; 141 | transform: translate(-50%, -50%); 142 | &:hover { 143 | width: 120px; 144 | height: 120px; 145 | transform: translate(-50%, -50%) rotate(360deg); 146 | } 147 | } 148 | } 149 | ``` 150 | 151 | - 下面的联系方式图标想要一个hover显示提示框的效果![image-20200819143501237](imgs/image-20200819143501237.png)这里要用三个伪类,首先在after伪类里面弄好圆角框和动画时间,然后隐藏。接下来在:hover::after里面显示就可以了,下面的三角形其实是一个css画的before 152 | 153 | ```css 154 | ::after { 155 | content: attr(data-title); //取到data-title属性的值 156 | background:#000; 157 | width: 80px; 158 | height: 35px; 159 | line-height: 35px; 160 | border-radius: 10px; 161 | font-size: 16px; 162 | color: #fff; 163 | position: absolute; 164 | bottom: 58px; 165 | left: 0px; 166 | transition: all 0.4s; 167 | opacity: 0; 168 | } 169 | ``` 170 | 171 | ```css 172 | :hover::after { 173 | opacity: 1; 174 | } 175 | ``` 176 | 177 | ```css 178 | :before { //三角形 179 | content: ""; 180 | position: absolute; 181 | bottom: 50px; 182 | left: 34px; 183 | width: 0; 184 | height: 0; 185 | border-top: 8px solid #000; 186 | border-left: 6px solid transparent; 187 | border-right: 6px solid transparent; 188 | transition: all 0.4s; 189 | opacity: 0; 190 | } 191 | :hover::before{ 192 | opacity: 1; 193 | } 194 | ``` 195 | 196 | ## 2.4 music-box组件 197 | 198 | 这绝对称得上博客页面最难的地方,用上了keyframes动画,height是auto的动画,vuescroll插件,自己画svg,父子组件通信,Promise.all的使用,li里的span不换行,一个一个讲。 199 | 200 | ![](imgs/image-20200820222252811.png) 201 | 202 | - keyframes 实现旋转动画,首先定义rotate 203 | 204 | ```css 205 | @keyframes rotate { 206 | 0% { 207 | transform: rotate(0); 208 | } 209 | 100% { 210 | transform: rotate(360deg); 211 | } 212 | } 213 | ``` 214 | 215 | 再在css里定义animation 216 | 217 | ```css 218 | &.play { 219 | animation: rotate 10s linear infinite; 220 | } 221 | &.pause { 222 | animation-play-state: paused; 223 | } 224 | ``` 225 | 226 | 即可实现封面无限旋转 227 | 228 | - height是auto不固定的div无法实现height动画,可以利用max-height 229 | 230 | ```css 231 | .scrollboxhide { 232 | max-height: 0 !important; 233 | margin-bottom: 0px !important; 234 | } 235 | .scrollbox { 236 | max-height: 250px; 237 | margin-bottom: 8px; 238 | transition: all 0.5s; 239 | } 240 | ``` 241 | 242 | - vuescroll插件使用,现在main.js里 243 | 244 | ```js 245 | import vuescroll from 'vuescroll' 246 | Vue.use(vuescroll) 247 | 248 | Vue.prototype.$vuescrollConfig = { 249 | vuescroll: { 250 | mode: 'native', // 选择一个模式, native 或者 slide(pc&app) 251 | sizeStrategy: 'percent', // 如果父容器不是固定高度,请设置为 number , 否则保持默认的percent即可 252 | detectResize: true // 是否检测内容尺寸发生变化 253 | }, 254 | scrollPanel: { 255 | initialScrollY: false, // 只要组件mounted之后自动滚动的距离。 例如 100 or 10% 256 | initialScrollX: false, 257 | scrollingX: false, // 是否启用 x 或者 y 方向上的滚动 258 | scrollingY: true, 259 | speed: 100, // 多长时间内完成一次滚动。 数值越小滚动的速度越快 260 | easing: undefined, // 滚动动画 参数通animation 261 | verticalNativeBarPos: 'right'// 原生滚动条的位置 262 | }, 263 | rail: { // 轨道 264 | background: '#c3c3c3', // 轨道的背景色 265 | opacity: 0, 266 | size: '6px', 267 | specifyBorderRadius: false, // 是否指定轨道的 borderRadius, 如果不那么将会自动设置 268 | gutterOfEnds: null, 269 | gutterOfSide: '0px', // 轨道距 x 和 y 轴两端的距离 270 | keepShow: false // 是否即使 bar 不存在的情况下也保持显示 271 | }, 272 | bar: { 273 | showDelay: 1000, // 在鼠标离开容器后多长时间隐藏滚动条 274 | onlyShowBarOnScroll: false, // 当页面滚动时显示 275 | keepShow: false, // 是否一直显示 276 | background: '#c3c3c3', 277 | opacity: 1, 278 | hoverStyle: false, 279 | specifyBorderRadius: false, 280 | minSize: false, 281 | size: '6px', 282 | disable: false // 是否禁用滚动条 283 | }, // 在这里设置全局默认配置 284 | name: 'vuescroll' // 在这里自定义组件名字,默认是vueScroll 285 | } 286 | ``` 287 | 288 | 然后使用 289 | 290 | ```js 291 |
292 | 293 |
294 |
    295 |
  • 296 | 299 | 302 | 303 | {{index + 1}} 304 | {{song.name}} 305 | 306 |
    307 |
  • 308 |
309 |
310 |
311 |
312 | ``` 313 | 314 | - svg可以在iconfont自己下个差不多的,再上svg在线编辑网站改下就好了,覆盖掉原来自己项目里的svg 315 | 316 | - 父子组件通信 317 | 318 | 父调用子: 319 | 320 | 父中组件 321 | 322 | ```js 323 | 324 | ``` 325 | 326 | 调用 327 | 328 | ```js 329 | toPlay (item, e) { 330 | this.$refs.player.setPlay(item) 331 | } 332 | ``` 333 | 334 | 子调用父: 335 | 336 | 子中调用 337 | 338 | ```js 339 | shouqi (e) { 340 | this.spread = false 341 | this.$emit('callshouqi') 342 | } 343 | ``` 344 | 345 | 父亲中用到的组件用@callshouqi="shouqilist" 346 | 347 | ```js 348 | 349 | ``` 350 | 351 | 父中函数 352 | 353 | ```js 354 | shouqilist () { 355 | this.spread = false 356 | this.scrollClass = 'scrollbox scrollboxhide' 357 | } 358 | ``` 359 | 360 | - Promise.all 361 | 362 | 在处理for中的异步方法中,每个异步方法都有then,相当于两个异步,必须等所有异步执行完再赋值songlist 363 | 364 | ```js 365 | _getSongList () { 366 | const songlist = [] 367 | const pros = [] 368 | this.songs.forEach((id, index) => { 369 | pros.push(getSongDetail(id).then((res) => { 370 | if (res.status === ERR_OK) { 371 | songlist.push({'id': res.data.songs[0].id, 'name': res.data.songs[0].name, 'pic': res.data.songs[0].al.picUrl, 'index': index}) 372 | } 373 | })) 374 | }) 375 | Promise.all(pros).then(() => { 376 | songlist.sort((a, b) => a.index - b.index) 377 | this.songlist = songlist 378 | }) 379 | } 380 | ``` 381 | 382 | Promise.all([p1,p2,...])接受Promise列表的传入参数,所以先创建一个for训练里面的异步列表,再传入Promise.all 383 | 384 | - li里的span不换行 385 | 386 | ```html 387 |
  • 388 | 389 | {{index + 1}} 390 | {{song.name}} 391 | 392 |
  • 393 | ``` 394 | 395 | 外面再套一个span,设置 white-space: nowrap; 和设置 li overflow: hidden; 396 | 397 | 播放组件做了做了一天半,效果如图 398 | 399 | ![](imgs/image-20200822165552890-1598429509454.png) 400 | 401 | 真是苦难重重啊。 402 | 403 | - 布局 404 | 405 | 绿色整块display:flex; 图像大小固定,图片右边整块flex:1,然后也是flex布局,列向排列,主要是为了随着底部部分变化,歌词都可以撑满蓝色的区域,底部(进度条+图标)也是flex布局,进度条flex:1,可以随着屏幕变化而变。 406 | 407 | - 播放进度条(注意点:document的事件绑定document.onmousemove = function() {} 可以用docume.onmousemove =null**清空绑定** 408 | 409 | 但是使用document.addEventListener('mousemove',f)绑定的要用document.removeEventListener('mousemove',f)**解除该绑定** 410 | 411 | - 点击事件 412 | 413 | ```js 414 | progressClick (e) { 415 | let rect = this.$refs.progressBar.getBoundingClientRect() 416 | let offsetWidth = this._isMobile() ? e.touches[0].clientX - rect.left : e.clientX - rect.left //手机和电脑clientX不一样 417 | let barWidth = rect.right - rect.left 418 | let percent = offsetWidth / barWidth 419 | this.currentTime = this.duration * percent 420 | this.$refs.audio.currentTime = this.duration * percent 421 | } 422 | ``` 423 | 424 | - pc端+移动端拖动事件 425 | 426 | ```js 427 | progressTouchStart (e) { 428 | this.touch.move = true 429 | let rect = this.$refs.progressBar.getBoundingClientRect() 430 | let barWidth = rect.right - rect.left 431 | let offsetWidth = this._isMobile() ? e.touches[0].clientX - rect.left : e.clientX - rect.left 432 | let percent = offsetWidth / barWidth 433 | if (this._isMobile()) { //手机端触碰直接跳到进度 434 | this.currentTime = this.duration * percent 435 | this.$refs.audio.currentTime = this.duration * percent 436 | } 437 | this.touch.rect = rect //PC端保存以下dom偏离位置 438 | let $this = this 439 | if (!this._isMobile()) { //PC端和手机端的鼠标移动事件和鼠标按起事件不一样 440 | document.onmousemove = function (e) { 441 | $this.progressTouchMove(e) 442 | } 443 | document.onmouseup = function (e) { 444 | document.onmousemove = null 445 | document.onmouseup = null 446 | $this.progressTouchEnd(e) 447 | } 448 | } else { 449 | document.ontouchmove = function (e) { 450 | $this.progressTouchMove(e) 451 | } 452 | document.ontouchend = function (e) { 453 | document.ontouchmove = null 454 | document.ontouchend = null 455 | $this.progressTouchEnd(e) 456 | } 457 | } 458 | }, 459 | progressTouchMove (e) { 460 | if (!this.touch.move) { 461 | return 462 | } 463 | let endX = this._isMobile() ? e.touches[0].clientX : e.clientX 464 | let percent = (endX - this.touch.rect.left) / (this.touch.rect.right - this.touch.rect.left) 465 | this.currentTime = this.duration * Math.min(Math.max(0, percent), 1) //移一次设置一次进度条但是不改变事件 466 | }, 467 | progressTouchEnd (e) { 468 | this.touch.move = false 469 | this.$refs.audio.currentTime = this.currentTime //移完了改变播放时间 470 | } 471 | ``` 472 | 473 | - 歌词移动 474 | 475 | 歌词没有点击事件,比上面简单一点 476 | 477 | ```js 478 | lyricTouchStart (e) { 479 | this.songLyric.move = true 480 | this.songLyric.startY = this._isMobile() ? e.touches[0].clientY : e.clientY 481 | this.songLyric.marginTop = this.$refs.ul.style.marginTop 482 | let dom = document.querySelector('ul') 483 | let height = window.getComputedStyle(dom).height 484 | this.songLyric.height = height 485 | let $this = this 486 | if (!this._isMobile()) { 487 | document.onmousemove = function (e) { 488 | $this.lyricTouchMove(e) 489 | } 490 | document.onmouseup = function (e) { 491 | document.onmousemove = null 492 | document.onmouseup = null 493 | $this.lyricTouchEnd() 494 | } 495 | } else { 496 | document.ontouchmove = function (e) { 497 | $this.lyricTouchMove(e) 498 | } 499 | document.ontouchend = function (e) { 500 | document.ontouchmove = null 501 | document.ontouchend = null 502 | $this.lyricTouchEnd() 503 | } 504 | } 505 | }, 506 | lyricTouchMove (e) { 507 | if (!this.songLyric.move) return 508 | let endY = this._isMobile() ? e.touches[0].clientY : e.clientY 509 | this.transitionOn = false 510 | let top = Math.min(parseInt(this.songLyric.marginTop) + endY - this.songLyric.startY, 25) 511 | let bottom = Math.max(-parseInt(this.songLyric.height) + 45, top) 512 | this.$refs.ul.style.marginTop = bottom + 'px' 513 | }, 514 | lyricTouchEnd (e) { 515 | this.songLyric.move = false 516 | this.transitionOn = true 517 | } 518 | ``` 519 | 520 | - 音量悬浮显示音量bar![image-20200822172328629](imgs/image-20200822172328629.png) 521 | 522 | - 注意点:悬浮显示需要在鼠标悬浮在![](imgs/image-20200822172553110.png)和进度条部分都要显示,所以干脆把这两个放在一个div里,初始父div位置就在图标上,hover时,把子div就是bar的height高度还原正常,这样鼠标移动到bar就可以继续hover显示 523 | 524 | ```css 525 | .volume { 526 | position: relative; 527 | display: inline-block; 528 | height: 30px; 529 | &:hover > .volume_wrap { 530 | height: 54px; 531 | } 532 | } 533 | ``` 534 | 535 | 音量调整条 比上面两个简单 536 | 537 | ```js 538 | volumeClick (e) { 539 | let rect = this.$refs.vlmpgs.getBoundingClientRect() 540 | let offsetHeight = rect.bottom - e.clientY 541 | let barHeight = rect.bottom - rect.top 542 | this.volume = Math.min(Math.max(offsetHeight / barHeight, 0), 1) 543 | }, 544 | volumeMoveStart (e) { 545 | let rect = this.$refs.vlmpgs.getBoundingClientRect() 546 | let $this = this 547 | document.onmousemove = function (e) { 548 | let offsetHeight = rect.bottom - e.clientY 549 | let barHeight = rect.bottom - rect.top 550 | 551 | $this.volume = Math.min(Math.max(offsetHeight / barHeight, 0), 1) 552 | } 553 | document.onmouseup = function (e) { 554 | document.onmousemove = null 555 | document.onmouseup = null 556 | } 557 | } 558 | ``` 559 | 560 | 561 | 562 | - 歌词高亮 563 | 564 | 使用了background-clip,思路绝对定位(ul会动)一个div,ul放在这个div里面,设置div css 565 | 566 | ```css 567 | .mask { 568 | position: absolute; 569 | width: 100%; 570 | height: 100%; 571 | color: transparent; 572 | -webkit-background-clip: text; 573 | background-image: linear-gradient(0deg, rgba(121, 121, 121, 0.8) 33%, white 45%, white 64%, rgba(121, 121, 121, 0.8) 72%);; 574 | } 575 | ``` 576 | 577 | 相当于歌词透明,背景是![](imgs/image-20200822174649258.png),背景透过透明的字就是-webkit-background-clip: text;的效果 578 | 579 | - 获取内联样式 580 | 581 | ```js 582 | this.$refs.ul.style.marginTop 583 | ``` 584 | 585 | - 获取即时样式 586 | 587 | ```js 588 | let dom = document.querySelector('ul') 589 | let height = window.getComputedStyle(dom).height 590 | ``` 591 | 592 | - 事件e的位置 593 | 594 | #### 一、clientX、clientY 595 | 596 | 点击位置距离当前body可视区域的x,y坐标 597 | 598 | #### 二、pageX、pageY 599 | 600 | 对于整个页面来说,包括了被卷去的body部分的长度 601 | 602 | #### 三、screenX、screenY 603 | 604 | 点击位置距离当前电脑屏幕的x,y坐标 605 | 606 | #### 四、offsetX、offsetY 607 | 608 | 相对于带有定位的父盒子的x,y坐标 609 | 610 | #### 五、x、y 611 | 612 | 和screenX、screenY一样 613 | 614 | 音乐盒又细化了两天 现在来继续总结下 615 | 616 | 加了搜索功能,花功夫主要在一些逻辑bug和动画上 617 | 618 | - 输入框停止输入300毫秒后搜索,用到了截流函数 619 | 620 | ```js 621 | // 截流函数 622 | export function debounce (func, delay) { 623 | let timer 624 | return function (...args) { 625 | if (timer) { 626 | clearTimeout(timer) 627 | } 628 | timer = setTimeout(() => { 629 | func.apply(this, args) 630 | }, delay) 631 | } 632 | } 633 | ``` 634 | 635 | 然后在mounted方法中绑定watch调用的函数 636 | 637 | ```js 638 | mounted () { 639 | this.$watch('query', debounce((newQuery) => { 640 | if (newQuery) { 641 | let searchResults = [] 642 | getSearchSongs(newQuery, 0).then((res) => { 643 | if (res.status === ERR_OK) { 644 | let songs = res.data.result.songs 645 | if (songs.length > 0) { 646 | songs.forEach((song, index) => { 647 | searchResults.push({'id': song.id, 'name': song.name, 'pic': undefined, 'index': index, 'duration': song.duration, 'singerName': this.singerName(song.artists), 'source': 'Search', 'islike': false}) 648 | }) 649 | this.searchResults = searchResults 650 | } 651 | } 652 | }) 653 | } 654 | }, 300)) 655 | } 656 | ``` 657 | 658 | - 注意下.className:hover(无空格)和.className :hover(:前面有空格)的区别 659 | 660 | - 列表布局![ ](imgs/image-20200824161718758.png) 661 | 662 | 注意歌名过长会隐藏掉,然后歌手名字长度是不固定的,局部方式 663 | 664 | ```css 665 | .wrap { 666 | // white-space: nowrap; 667 | width: 300px; 668 | overflow: hidden; 669 | display: flex; 670 | height: 36px; 671 | .index { 672 | text-align: center; 673 | width: 26px; 674 | } 675 | .name { 676 | flex: 1; 677 | } 678 | .singer { 679 | float: right; 680 | font-size: 16px; 681 | opacity: 0.6; 682 | line-height: 36px; 683 | margin: 1px; 684 | } 685 | .duration { 686 | float: right; 687 | margin: 2px; 688 | width: 28px; 689 | font-size: 14px; 690 | opacity: 0.3; 691 | } 692 | ``` 693 | 694 | - 播放器组件用了很多vue动画,[参考](https://cn.vuejs.org/v2/guide/transitions.html#CSS-%E5%8A%A8%E7%94%BB) 695 | 696 | - svg的title 697 | 698 | ```html 699 | 703 | ``` 704 | 705 | - 注册全局filter(main.js) 706 | 707 | ```js 708 | Vue.filter('format', (interval) => { 709 | interval = interval | 0 710 | let minute = interval / 60 | 0 711 | let second = interval % 60 712 | if (second < 10) { 713 | second = '0' + second 714 | } 715 | return minute + ':' + second 716 | }) 717 | ``` 718 | 719 | - 递归深拷贝对象 720 | 721 | ```js 722 | export function copySong (obj) { 723 | if (typeof obj !== 'object') return obj 724 | let newobj = {} 725 | for (var attr in obj) { 726 | newobj[attr] = copySong(obj[attr]) 727 | } 728 | return newobj 729 | } 730 | ``` 731 | 732 | - 图片src没有的时候,解决出现的边框 733 | 734 | ```css 735 | img[src=""],img:not([src]){ 736 | opacity:0; 737 | } 738 | ``` 739 | 740 | - input里面的placeholder字体 741 | 742 | ```css 743 | input::-webkit-input-placeholder { 744 | color: #ccc; 745 | } 746 | ``` 747 | 748 | ## 2.5 blog-box组件 749 | 750 | - 背景图片API 751 | 752 | ```html 753 |
    754 | ``` 755 | 756 | index是父组件传入的props,为了图片不重复(设置cookie达到不重复,第一次登录需要刷新) 757 | 758 | # 3.骚操作 759 | 760 | ## 3.1 主题透明度 761 | 762 | 为了调节全局的透明的,使用拖拽进度条调节css的var() 763 | 764 | - 进度条组件 765 | 766 | ```css 767 | 780 | 849 | 930 | 931 | ``` 932 | 933 | 使用 934 | 935 | ```html 936 | 937 | ``` 938 | 939 | - 全局透明度,index.html定义 940 | 941 | ```css 942 | :root { 943 | --color: rgba(255, 255, 255, 1); 944 | --opacity: 1; 945 | } 946 | ``` 947 | 948 | 在需要改变的类样式中 949 | 950 | ```css 951 | background-color: var(--color); 952 | opacity: var(--opacity); 953 | ``` 954 | 955 | 观察进度条变化变化,调节透明度 956 | 957 | ```js 958 | watch: { 959 | transparent (newVal) { 960 | if (newVal >= 0 && newVal <= 100) { 961 | let root = document.querySelector(':root') 962 | let value = '--color: ' + 'rgba(255, 255, 255, ' + (1 - newVal / 100) + ');' 963 | let opacity = '--opacity: ' + (1 - newVal / 100) 964 | root.setAttribute('style', value + opacity) 965 | } 966 | } 967 | } 968 | ``` 969 | 970 | ------ 971 | 972 | ## 3.2 打乱动画 973 | 974 | 主要在![image-20200826213007574](imgs/image-20200826213007574.png)![image-20200826213021445](imgs/image-20200826213021445.png)这两部分有shuffle动画,参考了Vue官网。 975 | 976 | - 引入shuffle函数(本来自己写的,结果动画不支持自己写的) 977 | 978 | ```js 979 | import _ from 'lodash/lodash' 980 | ``` 981 | 982 | - 创建彩虹色函数和打乱函数 983 | 984 | ```js 985 | methods: { 986 | rainBow () { 987 | this.color = rainbowColor(this.tags.length, 15, 255) 988 | }, 989 | shuffle () { 990 | this.tags = _.shuffle(this.tags) 991 | } 992 | }, 993 | ``` 994 | 995 | - 初始化颜色 996 | 997 | ```js 998 | mounted () { 999 | this.rainBow() 1000 | } 1001 | ``` 1002 | 1003 | - 使用transition-group组件和绑定shuffle函数 1004 | 1005 | ```html 1006 |
    1007 | 1008 |
    1009 | {{tag.name+' ('+tag.number+')'}} 1010 |
    1011 |
    1012 |
    1013 | ``` 1014 | 1015 | - 添加css 1016 | 1017 | ```css 1018 | .cell-move { 1019 | transition: transform 1s; 1020 | } 1021 | ``` 1022 | 1023 | ## 3.3 board组件 1024 | 1025 | ![image-20200826213524137](imgs/image-20200826213524137.png) 1026 | 1027 | 这是一个基础组件,预留了slot 1028 | 1029 | ```html 1030 | 1040 | ``` 1041 | 1042 | 注意点: 1043 | 1044 | ```html 1045 |

    原本是

    {{content}}

    , 1046 | ``` 1047 | 1048 | 但是这样不支持content里面有换行,模板字符串也不行,只能用v-html,传入的 1049 | 1050 | content需要使用 1051 | 1052 | ``` 1053 | 空格:  1054 | 换行:
    1055 | ``` 1056 | 1057 | 但是这样content不受组件内部css影响,需要在index.html的css设置 1058 | 1059 | # 4.markdown显示 1060 | 1061 | ## 4.1 md-view组件 1062 | 1063 | - package.json添加依赖dependencies 1064 | 1065 | ```json 1066 | "mavon-editor": "2.9.0", 1067 | "markdown-loader": "5.1.0", 1068 | "html-loader": "1.3.0" 1069 | ``` 1070 | 1071 | - main.js全局注册 1072 | 1073 | ```js 1074 | import mavonEditor from 'mavon-editor' 1075 | import 'mavon-editor/dist/css/index.css' 1076 | Vue.use(mavonEditor) 1077 | ``` 1078 | 1079 | - 引入博客的md文件,并绑定给组件 1080 | 1081 | ```js 1082 | import Blog from 'blogs/博客主页开发日志.md' 1083 | export default { 1084 | components: {}, 1085 | props: {}, 1086 | data () { 1087 | return { 1088 | value: Blog 1089 | } 1090 | }, 1091 | watch: {}, 1092 | computed: { 1093 | _isMobile () { 1094 | let flag = navigator.userAgent.match(/(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i) 1095 | return flag 1096 | } 1097 | }, 1098 | methods: {}, 1099 | created () {}, 1100 | mounted () { 1101 | this.$refs.md.$nextTick(() => { 1102 | setTimeout(() => { 1103 | let blocks = this.$el.querySelectorAll('pre code') 1104 | blocks.forEach((block) => hljs.highlightBlock(block)) 1105 | }, 1000) 1106 | }) 1107 | } 1108 | } 1109 | ``` 1110 | 1111 | - 使用组件 1112 | 1113 | ```html 1114 | 1115 | ``` 1116 | 1117 | # 5.保存喜欢的音乐 1118 | 1119 | - 新建js/cache.js 1120 | 1121 | ```js 1122 | import storage from 'good-storage' 1123 | 1124 | const FAVORITE_KEY = '__favorite__' 1125 | const FAVORITE_MAX_LENGTH = 200 1126 | 1127 | function insertArray (arr, val, compare, maxLen) { 1128 | const index = arr.findIndex(compare) 1129 | if (index === 0) { 1130 | return 1131 | } 1132 | if (index > 0) { 1133 | arr.splice(index, 1) 1134 | } 1135 | arr.unshift(val) 1136 | if (maxLen && arr.length > maxLen) { 1137 | arr.pop() 1138 | } 1139 | } 1140 | 1141 | function deleteFromArray (arr, compare) { 1142 | const index = arr.findIndex(compare) 1143 | if (index > -1) { 1144 | arr.splice(index, 1) 1145 | } 1146 | } 1147 | 1148 | export function saveFavorite (song) { 1149 | let songs = storage.get(FAVORITE_KEY, []) 1150 | insertArray(songs, song, (item) => { 1151 | return song === item 1152 | }, FAVORITE_MAX_LENGTH) 1153 | storage.set(FAVORITE_KEY, songs) 1154 | return songs 1155 | } 1156 | 1157 | export function deleteFavorite (song) { 1158 | let songs = storage.get(FAVORITE_KEY, []) 1159 | deleteFromArray(songs, (item) => { 1160 | return song.id === item.id 1161 | }) 1162 | storage.set(FAVORITE_KEY, songs) 1163 | return songs 1164 | } 1165 | 1166 | export function loadFavorite () { 1167 | return storage.get(FAVORITE_KEY, []) 1168 | } 1169 | ``` 1170 | 1171 | - 在music-box组件引入 1172 | 1173 | ```js 1174 | import {saveFavorite, deleteFavorite, loadFavorite} from 'common/js/cache' 1175 | ``` 1176 | 1177 | - 使用 1178 | 1179 | ```js 1180 | like (item) { 1181 | if (this.songs.includes(item.id)) return 1182 | let copy = copySong(item) 1183 | copy.source = 'Normal' 1184 | copy.index = this.songs.length 1185 | this.songs.push(copy.id) 1186 | this.songlist.push(copy) 1187 | this.playindex = copy.index 1188 | saveFavorite(copy.id) 1189 | }, 1190 | unlike (item) { 1191 | let id = this.songs.indexOf(item.id) 1192 | if (id !== -1) { 1193 | this.songlist.splice(id, 1) 1194 | this.songs.splice(id, 1) 1195 | } 1196 | this.playindex = -1 1197 | this.songlist.forEach((item, index) => { 1198 | item.index = index 1199 | }) 1200 | deleteFavorite(item.id) 1201 | }, 1202 | ``` 1203 | 1204 | # 6.代码高亮 1205 | 1206 | - 添加依赖 1207 | 1208 | ```json 1209 | "highlight.js": "10.1.2" 1210 | ``` 1211 | 1212 | - md-view组件中引入 1213 | 1214 | ```js 1215 | import hljs from 'highlight.js' 1216 | ``` 1217 | 1218 | 设置props,(组件自带的高亮有点问题,强制关掉,但是codeStyle='atom-one-dark'是为了引入hljs的css样式 1219 | 1220 | ```js 1221 | 1222 | ``` 1223 | 1224 | - 加载后设置延时引入,在md-view的mounted方法添加 1225 | 1226 | ```js 1227 | mounted () { 1228 | this.$refs.md.$nextTick(() => { 1229 | setTimeout(() => { 1230 | let blocks = this.$el.querySelectorAll('pre code') 1231 | blocks.forEach((block) => hljs.highlightBlock(block)) 1232 | }, 1000) 1233 | }) 1234 | } 1235 | ``` 1236 | 1237 | this.$refs.md.$nextTick是在md组件渲染完成后执行,设置了延时是为了打开博客时不卡,因为是循环替换样式 1238 | 1239 | 1240 | 注意 1241 | 1242 | ```css 1243 | 'rgba(0, 0, 0, calc(var(--opacity)*2))' 1244 | ``` 1245 | 1246 | 是calc与var结合的写法 1247 | 1248 | - 修改代码背景色和默认色 1249 | 1250 | ```css 1251 | .markdown-body .highlight pre, 1252 | .markdown-body pre { 1253 | background-color: rgba(0, 0, 0, calc(var(--opacity)*2)) !important; 1254 | color: #fff; 1255 | } 1256 | .v-note-wrapper, .v-note-op, .v-show-content{ 1257 | background-color: var(--color) !important; 1258 | } 1259 | .v-note-navigation-wrapper { 1260 | right: 12px !important; 1261 | bottom: auto !important; 1262 | height: auto !important; 1263 | border: none !important; 1264 | width: 225px !important; 1265 | background-color: rgba(255,255,255,0.4) !important; 1266 | opacity: var(--opacity); 1267 | } 1268 | .v-show-content { 1269 | height: 1200px !important; 1270 | } 1271 | .v-note-wrapper .v-note-panel .v-note-show .v-show-content.scroll-style-border-radius::-webkit-scrollbar, .v-note-wrapper .v-note-panel .v-note-show .v-show-content-html.scroll-style-border-radius::-webkit-scrollbar { 1272 | width: 12px !important; 1273 | background-color: var(--color) !important; 1274 | } 1275 | ``` 1276 | 1277 | -------------------------------------------------------------------------------- /src/blogs/imgs/image-20200818160039299.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iCodek/vue-blog/27998e1559ee7813c9ffc6560dc41f4f76b4c953/src/blogs/imgs/image-20200818160039299.png -------------------------------------------------------------------------------- /src/blogs/imgs/image-20200818163913139-1598429442173.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iCodek/vue-blog/27998e1559ee7813c9ffc6560dc41f4f76b4c953/src/blogs/imgs/image-20200818163913139-1598429442173.png -------------------------------------------------------------------------------- /src/blogs/imgs/image-20200818163913139.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iCodek/vue-blog/27998e1559ee7813c9ffc6560dc41f4f76b4c953/src/blogs/imgs/image-20200818163913139.png -------------------------------------------------------------------------------- /src/blogs/imgs/image-20200819143501237.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iCodek/vue-blog/27998e1559ee7813c9ffc6560dc41f4f76b4c953/src/blogs/imgs/image-20200819143501237.png -------------------------------------------------------------------------------- /src/blogs/imgs/image-20200820222252811.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iCodek/vue-blog/27998e1559ee7813c9ffc6560dc41f4f76b4c953/src/blogs/imgs/image-20200820222252811.png -------------------------------------------------------------------------------- /src/blogs/imgs/image-20200822165552890-1598429509454.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iCodek/vue-blog/27998e1559ee7813c9ffc6560dc41f4f76b4c953/src/blogs/imgs/image-20200822165552890-1598429509454.png -------------------------------------------------------------------------------- /src/blogs/imgs/image-20200822165552890.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iCodek/vue-blog/27998e1559ee7813c9ffc6560dc41f4f76b4c953/src/blogs/imgs/image-20200822165552890.png -------------------------------------------------------------------------------- /src/blogs/imgs/image-20200822172328629.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iCodek/vue-blog/27998e1559ee7813c9ffc6560dc41f4f76b4c953/src/blogs/imgs/image-20200822172328629.png -------------------------------------------------------------------------------- /src/blogs/imgs/image-20200822172553110.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iCodek/vue-blog/27998e1559ee7813c9ffc6560dc41f4f76b4c953/src/blogs/imgs/image-20200822172553110.png -------------------------------------------------------------------------------- /src/blogs/imgs/image-20200822174649258.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iCodek/vue-blog/27998e1559ee7813c9ffc6560dc41f4f76b4c953/src/blogs/imgs/image-20200822174649258.png -------------------------------------------------------------------------------- /src/blogs/imgs/image-20200824161718758.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iCodek/vue-blog/27998e1559ee7813c9ffc6560dc41f4f76b4c953/src/blogs/imgs/image-20200824161718758.png -------------------------------------------------------------------------------- /src/blogs/imgs/image-20200826154211160.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iCodek/vue-blog/27998e1559ee7813c9ffc6560dc41f4f76b4c953/src/blogs/imgs/image-20200826154211160.png -------------------------------------------------------------------------------- /src/blogs/imgs/image-20200826154240558.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iCodek/vue-blog/27998e1559ee7813c9ffc6560dc41f4f76b4c953/src/blogs/imgs/image-20200826154240558.png -------------------------------------------------------------------------------- /src/blogs/imgs/image-20200826213007574.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iCodek/vue-blog/27998e1559ee7813c9ffc6560dc41f4f76b4c953/src/blogs/imgs/image-20200826213007574.png -------------------------------------------------------------------------------- /src/blogs/imgs/image-20200826213021445.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iCodek/vue-blog/27998e1559ee7813c9ffc6560dc41f4f76b4c953/src/blogs/imgs/image-20200826213021445.png -------------------------------------------------------------------------------- /src/blogs/imgs/image-20200826213037228.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iCodek/vue-blog/27998e1559ee7813c9ffc6560dc41f4f76b4c953/src/blogs/imgs/image-20200826213037228.png -------------------------------------------------------------------------------- /src/blogs/imgs/image-20200826213524137.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iCodek/vue-blog/27998e1559ee7813c9ffc6560dc41f4f76b4c953/src/blogs/imgs/image-20200826213524137.png -------------------------------------------------------------------------------- /src/blogs/test.md: -------------------------------------------------------------------------------- 1 | # 1 标题1 2 | 3 | # 2 标题2 4 | 5 | ## 2.1 二级 6 | 7 | ## 2.2 二级 8 | 9 | 对方对方的 10 | 11 | ```js 12 | like (item) { 13 | if (this.songs.includes(item.id)) return 14 | let copy = copySong(item) 15 | copy.source = 'Normal' 16 | copy.index = this.songs.length 17 | this.songs.push(copy.id) 18 | this.songlist.push(copy) 19 | this.playindex = copy.index 20 | saveFavorite(copy.id) 21 | }, 22 | unlike (item) { 23 | let id = this.songs.indexOf(item.id) 24 | if (id !== -1) { 25 | this.songlist.splice(id, 1) 26 | this.songs.splice(id, 1) 27 | } 28 | this.playindex = -1 29 | this.songlist.forEach((item, index) => { 30 | item.index = index 31 | }) 32 | deleteFavorite(item.id) 33 | }, 34 | ``` 35 | 36 | 37 | 38 | # 3 标题3 39 | 40 | # 4 标题4 41 | 42 | # 5 标题5 43 | 44 | # 7 标题5 45 | 46 | # 8 标题5 47 | 48 | # 9 标题5 49 | 50 | # 10 标题5 51 | 52 | # 11 标题6 53 | 54 | # 12 标题6 55 | 56 | # 13 标题6 57 | 58 | # 14 标题6 59 | 60 | # 15 标题6 -------------------------------------------------------------------------------- /src/blogs/博客主页开发日志.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iCodek/vue-blog/27998e1559ee7813c9ffc6560dc41f4f76b4c953/src/blogs/博客主页开发日志.md -------------------------------------------------------------------------------- /src/common/css/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | cursor: url('../../assets/cursor.cur'), default; 3 | } 4 | .icon { 5 | width: 1em; height: 1em; 6 | vertical-align: -0.15em; 7 | fill: currentColor; 8 | overflow: hidden; 9 | font-size: 40px; 10 | } 11 | * { 12 | margin: 0; 13 | padding: 0; 14 | } 15 | :root { 16 | --color: rgba(255, 255, 255, 1); 17 | --opacity: 1; 18 | --margin: 6; 19 | } 20 | a { 21 | text-decoration: none; 22 | color: rgb(60, 136, 236); 23 | } 24 | -------------------------------------------------------------------------------- /src/common/js/cache.js: -------------------------------------------------------------------------------- 1 | import storage from 'good-storage' 2 | 3 | const FAVORITE_KEY = '__favorite__' 4 | const FAVORITE_MAX_LENGTH = 200 5 | 6 | function insertArray (arr, val, compare, maxLen) { 7 | const index = arr.findIndex(compare) 8 | if (index === 0) { 9 | return 10 | } 11 | if (index > 0) { 12 | arr.splice(index, 1) 13 | } 14 | arr.unshift(val) 15 | if (maxLen && arr.length > maxLen) { 16 | arr.pop() 17 | } 18 | } 19 | 20 | function deleteFromArray (arr, compare) { 21 | const index = arr.findIndex(compare) 22 | if (index > -1) { 23 | arr.splice(index, 1) 24 | } 25 | } 26 | 27 | export function saveFavorite (song) { 28 | let songs = storage.get(FAVORITE_KEY, []) 29 | insertArray(songs, song, (item) => { 30 | return song === item 31 | }, FAVORITE_MAX_LENGTH) 32 | storage.set(FAVORITE_KEY, songs) 33 | return songs 34 | } 35 | 36 | export function deleteFavorite (song) { 37 | let songs = storage.get(FAVORITE_KEY, []) 38 | deleteFromArray(songs, (item) => { 39 | return song.id === item.id 40 | }) 41 | storage.set(FAVORITE_KEY, songs) 42 | return songs 43 | } 44 | 45 | export function loadFavorite () { 46 | return storage.get(FAVORITE_KEY, []) 47 | } 48 | -------------------------------------------------------------------------------- /src/common/js/config.js: -------------------------------------------------------------------------------- 1 | export const HOST = 'https://www.luckyclover.top/api' 2 | export const ERR_OK = 200 3 | 4 | export const playMode = { 5 | sequence: 0, 6 | loop: 1, 7 | random: 2 8 | } 9 | -------------------------------------------------------------------------------- /src/common/js/lyric.js: -------------------------------------------------------------------------------- 1 | export function parseLyric (data) { 2 | const lyricList = [] 3 | const lyricTime = [] 4 | let array = data.split('\n') 5 | let timeReg = /\[(\d*:\d*\.\d*)\]/ 6 | for (let element of array) { 7 | if (element.indexOf(']') === -1) continue 8 | let lyr = element.split(']')[1] 9 | let res = timeReg.exec(element) 10 | if (res == null || lyr.length <= 1) continue 11 | let timeStr = res[1] 12 | let min = parseInt(timeStr.split(':')[0]) * 60 13 | let sec = parseFloat(timeStr.split(':')[1]) 14 | let time = +Number(min + sec).toFixed(2) 15 | lyricTime.push(time) 16 | lyricList.push(lyr) 17 | } 18 | // console.log([lyricList, lyricTime]) 19 | return [lyricList, lyricTime] 20 | } 21 | 22 | export function curLyricIndex (lyricTime, curTime) { 23 | let id = 0 24 | let i = 0 25 | for (let element of lyricTime) { 26 | id = i 27 | if (element > (curTime + 0.2)) { 28 | // console.log(element, curTime, id - 1) 29 | break 30 | } 31 | id += 1 32 | i++ 33 | } 34 | if (id <= 0) return 0 35 | return id - 1 36 | } 37 | -------------------------------------------------------------------------------- /src/common/js/song.js: -------------------------------------------------------------------------------- 1 | export function copySong (obj) { 2 | if (typeof obj !== 'object') return obj 3 | let newobj = {} 4 | for (var attr in obj) { 5 | newobj[attr] = copySong(obj[attr]) 6 | } 7 | return newobj 8 | } 9 | -------------------------------------------------------------------------------- /src/common/js/util.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash/lodash' 2 | // 截流函数 3 | export function debounce (func, delay) { 4 | let timer 5 | return function (...args) { 6 | if (timer) { 7 | clearTimeout(timer) 8 | } 9 | timer = setTimeout(() => { 10 | func.apply(this, args) 11 | }, delay) 12 | } 13 | } 14 | // 排序数组 15 | export function sortList (n) { 16 | let foo = [] 17 | for (let i = 0; i < n; i++) { 18 | foo.push(i) 19 | } 20 | return foo 21 | } 22 | 23 | // 彩虹色 24 | export function rainbowColor (len, min, max) { 25 | if (min < 0 || max > 255) return 26 | let arr = sortList(len) 27 | let bgc = new Array(len).fill('rgba(') 28 | // let color = arr.map((a) => Math.floor(255 * (a + 1) / len)) 29 | let color = arr.map((a) => Math.floor((max - min) * a / (len - 1)) + min) 30 | for (let i = 0; i < 3; i++) { 31 | let shu = _.shuffle(arr) 32 | for (let j = 0; j < shu.length; j++) { 33 | bgc[j] += color[shu[j]] + ',' 34 | if (i === 2) { 35 | bgc[j] += '0.8)' 36 | } 37 | } 38 | } 39 | return bgc 40 | } 41 | -------------------------------------------------------------------------------- /src/components/bg/bg.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 28 | 42 | -------------------------------------------------------------------------------- /src/components/bottom/bottom.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 48 | 61 | -------------------------------------------------------------------------------- /src/components/center-content/center-content.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 30 | 38 | -------------------------------------------------------------------------------- /src/components/center/center.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 34 | 47 | -------------------------------------------------------------------------------- /src/components/left-content/left-content.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 28 | 37 | -------------------------------------------------------------------------------- /src/components/m-header/m-header.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 41 | 60 | -------------------------------------------------------------------------------- /src/components/main-content/main-content.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 55 | 61 | -------------------------------------------------------------------------------- /src/components/md-view/md-view.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 37 | 38 | 71 | -------------------------------------------------------------------------------- /src/components/music-box/music-box.vue: -------------------------------------------------------------------------------- 1 | 83 | 84 | 249 | 498 | -------------------------------------------------------------------------------- /src/components/right-content/right-content.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 32 | 41 | -------------------------------------------------------------------------------- /src/components/search-box/search-box.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 43 | 90 | -------------------------------------------------------------------------------- /src/components/tab/tab.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 63 | 140 | -------------------------------------------------------------------------------- /src/components/user-info/user-info.vue: -------------------------------------------------------------------------------- 1 | 57 | 58 | 101 | 208 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | // The Vue build version to load with the `import` command 2 | // (runtime-only or standalone) has been set in webpack.base.conf with an alias. 3 | import Vue from 'vue' 4 | import App from './App' 5 | import router from './router' 6 | import vuescroll from 'vuescroll' 7 | import mavonEditor from 'mavon-editor' 8 | import 'mavon-editor/dist/css/index.css' 9 | 10 | Vue.use(mavonEditor) 11 | Vue.use(vuescroll) 12 | 13 | Vue.prototype.$vuescrollConfig = { 14 | vuescroll: { 15 | mode: 'native', // 选择一个模式, native 或者 slide(pc&app) 16 | sizeStrategy: 'percent', // 如果父容器不是固定高度,请设置为 number , 否则保持默认的percent即可 17 | detectResize: true // 是否检测内容尺寸发生变化 18 | }, 19 | scrollPanel: { 20 | initialScrollY: false, // 只要组件mounted之后自动滚动的距离。 例如 100 or 10% 21 | initialScrollX: false, 22 | scrollingX: false, // 是否启用 x 或者 y 方向上的滚动 23 | scrollingY: true, 24 | speed: 100, // 多长时间内完成一次滚动。 数值越小滚动的速度越快 25 | easing: undefined, // 滚动动画 参数通animation 26 | verticalNativeBarPos: 'right'// 原生滚动条的位置 27 | }, 28 | rail: { // 轨道 29 | background: '#c3c3c3', // 轨道的背景色 30 | opacity: 0, 31 | size: '6px', 32 | specifyBorderRadius: false, // 是否指定轨道的 borderRadius, 如果不那么将会自动设置 33 | gutterOfEnds: null, 34 | gutterOfSide: '0px', // 轨道距 x 和 y 轴两端的距离 35 | keepShow: false // 是否即使 bar 不存在的情况下也保持显示 36 | }, 37 | bar: { 38 | showDelay: 1000, // 在鼠标离开容器后多长时间隐藏滚动条 39 | onlyShowBarOnScroll: false, // 当页面滚动时显示 40 | keepShow: false, // 是否一直显示 41 | background: '#c3c3c3', 42 | opacity: 1, 43 | hoverStyle: false, 44 | specifyBorderRadius: false, 45 | minSize: false, 46 | size: '6px', 47 | disable: false // 是否禁用滚动条 48 | }, // 在这里设置全局默认配置 49 | name: 'vuescroll' // 在这里自定义组件名字,默认是vueScroll 50 | } 51 | 52 | Vue.config.productionTip = false 53 | 54 | Vue.filter('format', (interval) => { 55 | interval = interval | 0 56 | let minute = interval / 60 | 0 57 | let second = interval % 60 58 | if (second < 10) { 59 | second = '0' + second 60 | } 61 | return minute + ':' + second 62 | }) 63 | /* eslint-disable no-new */ 64 | new Vue({ 65 | el: '#app', 66 | router, 67 | components: { App }, 68 | template: '' 69 | }) 70 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | 4 | const MainContent = (resolve) => { 5 | import('cpnts/main-content/main-content').then((module) => { 6 | resolve(module) 7 | }) 8 | } 9 | const MdView = (resolve) => { 10 | import('cpnts/md-view/md-view').then((module) => { 11 | resolve(module) 12 | }) 13 | } 14 | Vue.use(Router) 15 | 16 | export default new Router({ 17 | mode: 'history', 18 | routes: [ 19 | // { 20 | // path: '/', 21 | // name: 'HelloWorld', 22 | // component: HelloWorld 23 | // } 24 | { 25 | path: '/', 26 | name: 'MainContent', 27 | component: MainContent 28 | }, 29 | { 30 | path: '/', 31 | name: 'MdView', 32 | component: MdView 33 | } 34 | ] 35 | }) 36 | -------------------------------------------------------------------------------- /static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iCodek/vue-blog/27998e1559ee7813c9ffc6560dc41f4f76b4c953/static/.gitkeep --------------------------------------------------------------------------------