├── .npmrc ├── .prettierrc ├── request ├── urls │ ├── reply.js │ ├── category.js │ ├── article.js │ ├── comment.js │ └── user.js ├── api │ ├── reply.js │ ├── category.js │ ├── comment.js │ ├── article.js │ └── user.js ├── request.js └── http.js ├── boblog.png ├── static └── favicon.ico ├── plugins ├── axios-ports.js ├── route.js ├── md.js ├── element-ui.js ├── axios.js └── scrollTo.js ├── .env.development ├── .env.production ├── layouts ├── hutao.vue └── default.vue ├── README.md ├── .editorconfig ├── .eslintrc.js ├── lib ├── auth.js ├── utils.js ├── hutao.js └── progress-indicator.js ├── store ├── category.js └── user.js ├── components ├── common │ ├── Footer.vue │ ├── Header.vue │ └── LoginForm.vue └── article │ └── ArticleComment.vue ├── package.json ├── .gitignore ├── nuxt.config.js ├── pages ├── user.vue ├── usercenter.vue ├── article.vue ├── index.vue └── hutao.vue └── assets └── css └── common.css /.npmrc: -------------------------------------------------------------------------------- 1 | registry = https://registry.npmmirror.com 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /request/urls/reply.js: -------------------------------------------------------------------------------- 1 | export default { 2 | create: '/reply' 3 | } 4 | -------------------------------------------------------------------------------- /boblog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lfb/nuxtjs-blog-web/HEAD/boblog.png -------------------------------------------------------------------------------- /request/urls/category.js: -------------------------------------------------------------------------------- 1 | export default { 2 | list: '/category', 3 | } 4 | -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lfb/nuxtjs-blog-web/HEAD/static/favicon.ico -------------------------------------------------------------------------------- /request/urls/article.js: -------------------------------------------------------------------------------- 1 | export default { 2 | list: '/article', 3 | detail: '/article', 4 | } 5 | -------------------------------------------------------------------------------- /request/urls/comment.js: -------------------------------------------------------------------------------- 1 | 2 | export default { 3 | create: '/comment', 4 | target: '/comment/target/list', 5 | } 6 | -------------------------------------------------------------------------------- /plugins/axios-ports.js: -------------------------------------------------------------------------------- 1 | import { setClient } from '~/request/request' 2 | 3 | export default ({ app, store }) => { 4 | setClient(app.$axios) 5 | } 6 | -------------------------------------------------------------------------------- /request/urls/user.js: -------------------------------------------------------------------------------- 1 | export default { 2 | login: '/user/login', 3 | register: '/user/register', 4 | auth: '/user/auth', 5 | list: '/user/list' 6 | } 7 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | # just a flag 2 | NUXT_APP_ENV = 'development' 3 | 4 | # base api 5 | BASE_URL = 'http://localhost:5000/api/v1' 6 | BOBLOG_TOKEN = 'BOBLOG_TOKEN' 7 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | # just a flag 2 | NUXT_APP_ENV = 'production' 3 | 4 | # base api 5 | BASE_URL = 'https://api.boblog.com/api/v1' 6 | BOBLOG_TOKEN = 'BOBLOG_TOKEN' 7 | 8 | -------------------------------------------------------------------------------- /layouts/hutao.vue: -------------------------------------------------------------------------------- 1 | 6 | 10 | 12 | -------------------------------------------------------------------------------- /request/api/reply.js: -------------------------------------------------------------------------------- 1 | import { POST } from '../http.js' 2 | import reply from '../urls/reply' 3 | 4 | // 创建回复 5 | export function createReply(data) { 6 | return POST({ 7 | url: reply.create, 8 | data 9 | }) 10 | } 11 | -------------------------------------------------------------------------------- /request/api/category.js: -------------------------------------------------------------------------------- 1 | import { GET } from '../http.js' 2 | import category from '../urls/category' 3 | 4 | // 获取分类列表信息 5 | export function getCategory(params) { 6 | return GET({ 7 | url: category.list, 8 | params 9 | }) 10 | } 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 博客网站 2 | 3 | 基于 Nuxt.js SSR 博客项目 4 | 5 | - 技术栈:Vue.js, Nuxt.js, Vuex 6 | - UI 框架:Element-UI 7 | - Node.js 服务端 API 接口项目:[https://github.com/lfb/nodejs-koa-blog](https://github.com/lfb/nodejs-koa-blog) 8 | - 欢迎大家指导~ 9 | 10 | ![image.png](./boblog.png) 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /request/api/comment.js: -------------------------------------------------------------------------------- 1 | import { GET, POST } from '../http.js' 2 | import comment from '../urls/comment' 3 | 4 | export function createComment(data) { 5 | return POST({ 6 | url: comment.create, 7 | data 8 | }) 9 | } 10 | export function getCommentTarget(params) { 11 | return GET({ 12 | url: comment.target, 13 | params 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /layouts/default.vue: -------------------------------------------------------------------------------- 1 | 8 | 18 | 20 | -------------------------------------------------------------------------------- /request/api/article.js: -------------------------------------------------------------------------------- 1 | import { GET } from '../http.js' 2 | import article from '../urls/article' 3 | 4 | // 获取文章详情 5 | export function getArticleDetail(params) { 6 | return GET({ 7 | url: article.detail + '/' + params.id, 8 | params 9 | }) 10 | } 11 | 12 | // 获取文章列表 13 | export function getArticleList(params) { 14 | return GET({ 15 | url: article.list, 16 | params 17 | }) 18 | } 19 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | node: true 6 | }, 7 | parserOptions: { 8 | parser: '@babel/eslint-parser', 9 | requireConfigFile: false 10 | }, 11 | extends: [ 12 | '@nuxtjs', 13 | 'plugin:nuxt/recommended', 14 | 'prettier' 15 | ], 16 | plugins: [ 17 | ], 18 | // add your custom rules here 19 | rules: {} 20 | } 21 | -------------------------------------------------------------------------------- /request/api/user.js: -------------------------------------------------------------------------------- 1 | import { GET, POST } from '../http.js' 2 | import user from '../urls/user' 3 | 4 | // 用户登录 5 | export function login(data) { 6 | return POST({ 7 | url: user.login, 8 | data 9 | }) 10 | } 11 | 12 | // 用户注册 13 | export function register(data) { 14 | return POST({ 15 | url: user.register, 16 | data 17 | }) 18 | } 19 | // 用户信息 20 | export function info(data) { 21 | return GET({ 22 | url: user.auth, 23 | data 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /plugins/route.js: -------------------------------------------------------------------------------- 1 | 2 | export default ({ app, store, env }) => { 3 | app.router.beforeEach(async (to, from, next) => { 4 | const isLoginStatus = store.state.user.isLoginStatus 5 | const token = app.$cookies.get(env.BOBLOG_TOKEN) 6 | 7 | // 存在token,且未登录状态获取用户信息 8 | if (!isLoginStatus && token) { 9 | const [err] = await store.dispatch('user/userInfo') 10 | if (!err) { 11 | next() 12 | } 13 | 14 | next() 15 | } 16 | next() 17 | }) 18 | } 19 | -------------------------------------------------------------------------------- /lib/auth.js: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie' 2 | import { Base64 } from 'js-base64' 3 | const BOBLOG_TOKEN = process.env.BOBLOG_TOKEN 4 | 5 | export function encodeToken() { 6 | const token = getToken() 7 | const base64 = Base64.encode(token + ':') 8 | return 'Basic ' + base64 9 | } 10 | 11 | export function getToken() { 12 | return Cookies.get(BOBLOG_TOKEN) 13 | } 14 | 15 | export function setToken(token) { 16 | return Cookies.set(BOBLOG_TOKEN, token) 17 | } 18 | 19 | export function removeToken() { 20 | return Cookies.remove(BOBLOG_TOKEN) 21 | } 22 | -------------------------------------------------------------------------------- /request/request.js: -------------------------------------------------------------------------------- 1 | let client 2 | 3 | export function setClient(newclient) { 4 | client = newclient 5 | } 6 | 7 | // Request helpers 8 | const reqMethods = [ 9 | 'request', 10 | 'delete', 11 | 'get', 12 | 'head', 13 | 'options', // url, config 14 | 'post', 15 | 'put', 16 | 'patch' // url, data, config 17 | ] 18 | const service = {} 19 | 20 | reqMethods.forEach(method => { 21 | service[method] = function () { 22 | if (!client) throw new Error('apiClient not installed') 23 | return client[method].apply(null, arguments) 24 | } 25 | }) 26 | 27 | export default service 28 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * 验证邮箱 4 | * @param {string} email 5 | * @returns {Boolean} 6 | */ 7 | export function validEmail(email) { 8 | const reg = /^\w+@[a-zA-Z0-9]{2,10}(?:\.[a-z]{2,4}){1,3}$/ 9 | return reg.test(email) 10 | } 11 | 12 | /** 13 | * 验证密码 14 | * @param {string} email 15 | * @returns {Boolean} 16 | */ 17 | export function validPassword(email) { 18 | const reg = /^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]/ 19 | return reg.test(email) 20 | } 21 | 22 | /** 23 | * 判断是否是数组 24 | * @param arr 25 | * @returns {boolean} 26 | */ 27 | export function isArray(arr) { 28 | return Array.isArray(arr) && arr.length > 0 29 | } 30 | -------------------------------------------------------------------------------- /plugins/md.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | const hljs = require('highlight.js') 3 | 4 | const md = require('markdown-it')({ 5 | highlight(str, lang) { 6 | if (lang && hljs.getLanguage(lang)) { 7 | try { 8 | return ( 9 | '
' +
10 |           hljs.highlight(str, {
11 |             language: lang,
12 |             ignoreIllegals: true,
13 |           }).value +
14 |           '
' 15 | ) 16 | } catch (__) { } 17 | } 18 | 19 | return ( 20 | '
' + md.utils.escapeHtml(str) + '
' 21 | ) 22 | }, 23 | }) 24 | 25 | Vue.prototype.$md = md; 26 | export default md; 27 | -------------------------------------------------------------------------------- /store/category.js: -------------------------------------------------------------------------------- 1 | import { getCategory } from '@/request/api/category' 2 | const state = () => ({ 3 | categoryList: [], 4 | }) 5 | 6 | const mutations = { 7 | SET_CATEGORY_LIST(state, data) { 8 | state.categoryList = data 9 | }, 10 | 11 | } 12 | 13 | const actions = { 14 | async getCategoryData({ state, commit }, params = {}) { 15 | if (Array.isArray(state.categoryList) && state.categoryList.length > 0) { 16 | return state.categoryList 17 | } else { 18 | const [err, res] = await getCategory(params) 19 | if (!err) { 20 | const category = res.data.data.data 21 | commit('SET_CATEGORY_LIST', category) 22 | return category 23 | } else { 24 | return err 25 | } 26 | } 27 | }, 28 | } 29 | 30 | export default { 31 | namespace: true, 32 | state, 33 | actions, 34 | mutations 35 | } 36 | -------------------------------------------------------------------------------- /plugins/element-ui.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import { 3 | Icon, 4 | Drawer, 5 | Dialog, 6 | Button, 7 | Avatar, 8 | Dropdown, 9 | DropdownMenu, 10 | DropdownItem, 11 | Pagination, 12 | Input, 13 | Loading, 14 | MessageBox, 15 | Message, 16 | } from 'element-ui' 17 | 18 | import locale from 'element-ui/lib/locale/lang/en' 19 | 20 | const components = [ 21 | Icon, 22 | Button, 23 | Avatar, 24 | Dialog, 25 | Dropdown, 26 | DropdownMenu, 27 | DropdownItem, 28 | Pagination, 29 | Input, 30 | Drawer 31 | ] 32 | const Element = { 33 | install(Vue) { 34 | components.forEach(component => { 35 | Vue.component(component.name, component) 36 | }) 37 | } 38 | } 39 | 40 | Vue.use(Loading.directive); 41 | Vue.prototype.$loading = Loading.service; 42 | Vue.prototype.$msgbox = MessageBox; 43 | Vue.prototype.$alert = MessageBox.alert; 44 | Vue.prototype.$confirm = MessageBox.confirm; 45 | Vue.prototype.$prompt = MessageBox.prompt; 46 | Vue.prototype.$message = Message; 47 | 48 | Vue.use(Element, { locale }) 49 | -------------------------------------------------------------------------------- /plugins/axios.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import { removeToken, encodeToken } from "@/lib/auth"; 3 | 4 | export default ({ $axios, store }) => { 5 | $axios.onRequest(config => { 6 | config.baseURL = process.env.BASE_URL 7 | config.headers.Authorization = encodeToken() 8 | 9 | return config 10 | }) 11 | 12 | $axios.onResponse(res => { 13 | if (res.status === 200 && res.data.code === 200) { 14 | return res 15 | } else { 16 | Vue.prototype.$message.error(res.data.msg || '获取失败') 17 | return Promise.reject(res) 18 | } 19 | }) 20 | 21 | $axios.onError(err => { 22 | const { response } = err || {} 23 | 24 | // 处理token过期无效情况,清除token,初始化store数据 25 | if ([401, 403].includes(response?.status)) { 26 | removeToken() 27 | store.commit('user/SET_LOGIN_STATUS', false) 28 | store.commit('user/SET_USERINFO', null) 29 | } 30 | 31 | const msg = Array.isArray(response?.data.msg) ? response?.data?.msg.join(',') : response?.data?.msg 32 | Vue.prototype.$message.error(msg || '获取失败') 33 | return Promise.reject(err) 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /components/common/Footer.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 24 | 25 | 56 | -------------------------------------------------------------------------------- /request/http.js: -------------------------------------------------------------------------------- 1 | import service from '~/request/request.js' 2 | 3 | export function GET(config) { 4 | const { url = '', data = {}, ...opt } = config 5 | return service 6 | .get(url, { 7 | params: data, 8 | ...opt 9 | }) 10 | .then(res => { 11 | return [null, res] 12 | }) 13 | .catch(err => { 14 | 15 | return [err, null] 16 | }) 17 | } 18 | export function POST(config) { 19 | const { url = '', data = {}, ...opt } = config 20 | return service 21 | .post(url, data, opt) 22 | .then(res => { 23 | return [null, res] 24 | }) 25 | .catch(err => { 26 | return [err, null] 27 | }) 28 | } 29 | 30 | export function UPLOAD(config) { 31 | const { url = '', data = {}, ...opt } = config 32 | return service 33 | .post(url, data, { 34 | headers: { 35 | 'Content-Type': 'multipart/form-data' 36 | }, 37 | ...opt 38 | }) 39 | .then(res => { 40 | return [null, res] 41 | }) 42 | .catch(err => { 43 | return [err, null] 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "1.0.0", 4 | "private": true, 5 | "config": { 6 | "nuxt": { 7 | "host": "localhost", 8 | "port": "3000" 9 | } 10 | }, 11 | "scripts": { 12 | "dev": "cross-env NODE_ENV=development nuxt", 13 | "build": "nuxt build", 14 | "start": "nuxt start", 15 | "generate": "nuxt generate", 16 | "lint:js": "eslint --ext \".js,.vue\" --ignore-path .gitignore .", 17 | "lint": "yarn lint:js" 18 | }, 19 | "dependencies": { 20 | "@nuxtjs/axios": "^5.13.6", 21 | "@xunlei/vue-lazy-component": "^1.1.3", 22 | "babel-plugin-component": "^1.1.1", 23 | "cookie-universal-nuxt": "^2.1.5", 24 | "core-js": "^3.15.1", 25 | "cross-env": "^7.0.3", 26 | "dotenv": "^10.0.0", 27 | "element-ui": "^2.15.2", 28 | "highlight.js": "^11.1.0", 29 | "js-base64": "^3.6.1", 30 | "js-cookie": "^2.2.1", 31 | "markdown-it": "^12.1.0", 32 | "node-sass": "^6.0.1", 33 | "nuxt": "^2.15.7", 34 | "sass-loader": "^10.1.1" 35 | }, 36 | "devDependencies": { 37 | "@babel/eslint-parser": "^7.14.7", 38 | "@nuxtjs/eslint-config": "^6.0.1", 39 | "@nuxtjs/eslint-module": "^3.0.2", 40 | "eslint": "^7.29.0", 41 | "eslint-config-prettier": "^8.3.0", 42 | "eslint-plugin-nuxt": "^2.0.0", 43 | "eslint-plugin-vue": "^7.12.1", 44 | "prettier": "^2.3.2" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/hutao.js: -------------------------------------------------------------------------------- 1 | export const LEVEL_TEXT = { 2 | SSS: { 3 | level: 'SSS', 4 | color: '#ff9900', 5 | star: 5, 6 | text: '胡桃厨天花板级圣遗物,又称遗物级圣遗物,恭喜你,你的有效词条数已经接近满值,增伤点数极限为 5。' 7 | }, 8 | SS: { 9 | level: 'SS', 10 | color: '#19be6b', 11 | star: 4, 12 | text: '极品圣遗物,欧皇专属,很给力的圣遗物,3 的增伤值达标,下一阶段需要 4 的增伤值。' 13 | }, 14 | S: { 15 | level: 'S', 16 | color: '#5cadff', 17 | star: 3, 18 | text: '毕业圣遗物,如果你不是胡桃厨可以休息啦!2 的增伤值达标,下一阶段需要 3 的增伤值。' 19 | }, 20 | C: { 21 | level: 'C', 22 | star: 2, 23 | color: '#808695', 24 | text: '赶快为你的老婆多刷点圣遗物吧,都快穷的吃土了,下一阶段需要 2 的增伤值。' 25 | } 26 | } 27 | 28 | export const ATTRIBUTES_MAP = { 29 | JING_TONG: 23.31, 30 | SHENG_MING_BAI_FEN_BI: 5.83, 31 | XIAO_SHENG_MING: 906, 32 | BAO_SHANG: 7.77, 33 | GONG_JI_BAI_FEN_BI: 5.83, 34 | XIAO_GONG_JI: 41 35 | } 36 | 37 | export const equipArray = [{ 38 | key: 'JING_TONG', 39 | name: '精通', 40 | value: '', 41 | placeholder: '精通' 42 | }, 43 | { 44 | key: 'BAO_SHANG', 45 | name: '爆伤', 46 | value: '', 47 | placeholder: '暴击伤害' 48 | }, 49 | { 50 | key: 'SHENG_MING_BAI_FEN_BI', 51 | name: '大生命', 52 | value: '', 53 | placeholder: '生命百分比' 54 | }, 55 | { 56 | key: 'XIAO_SHENG_MING', 57 | name: '小生命', 58 | value: '', 59 | placeholder: '小生命' 60 | }, 61 | { 62 | key: 'GONG_JI_BAI_FEN_BI', 63 | name: '大攻击', 64 | value: '', 65 | placeholder: '攻击百分比', 66 | isUndivided: true 67 | }, 68 | { 69 | key: 'XIAO_GONG_JI', 70 | name: '小攻击', 71 | value: '', 72 | placeholder: '小攻击', 73 | isUndivided: true 74 | } 75 | ] 76 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | /logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (https://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # TypeScript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | # dotenv environment variables file 60 | .env 61 | 62 | # parcel-bundler cache (https://parceljs.org/) 63 | .cache 64 | 65 | # next.js build output 66 | .next 67 | 68 | # nuxt.js build output 69 | .nuxt 70 | 71 | # Nuxt generate 72 | dist 73 | 74 | # vuepress build output 75 | .vuepress/dist 76 | 77 | # Serverless directories 78 | .serverless 79 | 80 | # IDE / Editor 81 | .idea 82 | 83 | # Service worker 84 | sw.* 85 | 86 | # macOS 87 | .DS_Store 88 | 89 | # Vim swap files 90 | *.swp 91 | -------------------------------------------------------------------------------- /nuxt.config.js: -------------------------------------------------------------------------------- 1 | const dotenv = require('dotenv') 2 | const envConfig = dotenv.config({ path: `.env.${process.env.NODE_ENV}` }).parsed 3 | 4 | export default { 5 | // Global page headers: https://go.nuxtjs.dev/config-head 6 | head: { 7 | title: 'frontend', 8 | htmlAttrs: { 9 | lang: 'en' 10 | }, 11 | meta: [ 12 | { charset: 'utf-8' }, 13 | { name: 'viewport', content: 'width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=0' }, 14 | { hid: 'description', name: 'description', content: '' }, 15 | { name: 'format-detection', content: 'telephone=no' } 16 | ], 17 | link: [ 18 | { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' } 19 | ] 20 | }, 21 | 22 | // Global CSS: https://go.nuxtjs.dev/config-css 23 | css: [ 24 | '@/assets/css/common.css', 25 | 'element-ui/lib/theme-chalk/index.css' 26 | ], 27 | 28 | // Plugins to run before rendering page: https://go.nuxtjs.dev/config-plugins 29 | plugins: [ 30 | '@/plugins/axios', 31 | '@/plugins/axios-ports', 32 | '@/plugins/md', 33 | '@/plugins/route', 34 | { src: '@/plugins/scrollTo', mode: 'client' }, 35 | '@/plugins/element-ui' 36 | ], 37 | 38 | // Auto import components: https://go.nuxtjs.dev/config-components 39 | // components: true, 40 | 41 | // Modules for dev and build (recommended): https://go.nuxtjs.dev/config-modules 42 | buildModules: [ 43 | // https://go.nuxtjs.dev/eslint 44 | // '@nuxtjs/eslint-module', 45 | ], 46 | 47 | // Modules: https://go.nuxtjs.dev/config-modules 48 | modules: [ 49 | '@nuxtjs/axios', 50 | 'cookie-universal-nuxt', 51 | ['cookie-universal-nuxt', { alias: 'cookiz' }], 52 | ], 53 | env: envConfig, 54 | // Build Configuration: https://go.nuxtjs.dev/config-build 55 | build: { 56 | transpile: [/^element-ui/], 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /store/user.js: -------------------------------------------------------------------------------- 1 | import { login, register, info } from '@/request/api/user' 2 | import { setToken } from "@/lib/auth"; 3 | 4 | const state = () => ({ 5 | userInfo: null, 6 | isLoginStatus: false 7 | }) 8 | 9 | const mutations = { 10 | SET_USERINFO(state, data) { 11 | state.userInfo = data 12 | }, 13 | SET_LOGIN_STATUS(state, data) { 14 | state.isLoginStatus = data 15 | } 16 | } 17 | 18 | const actions = { 19 | async userLogin({ state, commit }, params = {}) { 20 | const [err, res] = await login(params) 21 | if (!err) { 22 | const user = res.data.data 23 | commit('SET_USERINFO', { 24 | id: user.id, 25 | username: user.username, 26 | email: user.email 27 | }) 28 | commit('SET_LOGIN_STATUS', true) 29 | setToken(user.token) 30 | return [null, user] 31 | } else { 32 | return [err, null] 33 | } 34 | }, 35 | async userRegister({ state, commit }, params = {}) { 36 | const [err, res] = await register(params) 37 | if (!err) { 38 | const user = res.data.data 39 | commit('SET_USERINFO', { 40 | id: user.id, 41 | username: user.username, 42 | email: user.email 43 | }) 44 | commit('SET_LOGIN_STATUS', true) 45 | setToken(user.token) 46 | return [null, user] 47 | } else { 48 | return [err, null] 49 | } 50 | 51 | }, 52 | async userInfo({ state, commit }, params = {}) { 53 | if (state.isLoginStatus && state.userInfo) { 54 | return state.userInfo 55 | } 56 | 57 | const [err, res] = await info(params) 58 | if (!err) { 59 | const user = res.data.data 60 | commit('SET_USERINFO', { 61 | id: user.id, 62 | username: user.username, 63 | email: user.email 64 | }) 65 | commit('SET_LOGIN_STATUS', true) 66 | return [null, user] 67 | } else { 68 | return [err, null] 69 | } 70 | }, 71 | } 72 | 73 | export default { 74 | namespace: true, 75 | state, 76 | actions, 77 | mutations 78 | } 79 | -------------------------------------------------------------------------------- /plugins/scrollTo.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | Math.easeInOutQuad = function (t, b, c, d) { 4 | // eslint-disable-next-line no-param-reassign 5 | t /= d / 2 6 | if (t < 1) { 7 | return (c / 2) * t * t + b 8 | } 9 | // eslint-disable-next-line no-param-reassign 10 | t-- 11 | return (-c / 2) * (t * (t - 2) - 1) + b 12 | } 13 | 14 | // requestAnimationFrame for Smart Animating http://goo.gl/sx5sts 15 | const requestAnimFrame = (function () { 16 | return ( 17 | window.requestAnimationFrame || 18 | window.webkitRequestAnimationFrame || 19 | window.mozRequestAnimationFrame || 20 | function (callback) { 21 | window.setTimeout(callback, 1000 / 60) 22 | } 23 | ) 24 | })() 25 | 26 | // because it's so fucking difficult to detect the scrolling element, just move them all 27 | function move(amount) { 28 | document.documentElement.scrollTop = amount 29 | document.body.parentNode.scrollTop = amount 30 | document.body.scrollTop = amount 31 | } 32 | 33 | function position() { 34 | return document.documentElement.scrollTop || document.body.parentNode.scrollTop || document.body.scrollTop 35 | } 36 | 37 | function scrollTo(to, duration, callback) { 38 | const start = position() 39 | const change = to - start 40 | const increment = 20 41 | let currentTime = 0 42 | // eslint-disable-next-line no-param-reassign 43 | duration = typeof duration === 'undefined' ? 500 : duration 44 | const animateScroll = function () { 45 | // increment the time 46 | currentTime += increment 47 | // find the value with the quadratic in-out easing function 48 | const val = Math.easeInOutQuad(currentTime, start, change, duration) 49 | // move the document.body 50 | move(val) 51 | // do the animation unless its over 52 | if (currentTime < duration) { 53 | requestAnimFrame(animateScroll) 54 | } else if (callback && typeof callback === 'function') { 55 | // the animation is done so lets callback 56 | callback() 57 | } 58 | } 59 | animateScroll() 60 | } 61 | 62 | 63 | Vue.prototype.$scrollTo = scrollTo 64 | 65 | export default scrollTo 66 | -------------------------------------------------------------------------------- /pages/user.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 106 | 107 | 118 | -------------------------------------------------------------------------------- /pages/usercenter.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 112 | 113 | 124 | -------------------------------------------------------------------------------- /assets/css/common.css: -------------------------------------------------------------------------------- 1 | /*highlight.js*/ 2 | /*Syntax highlighting for the Web*/ 3 | /*@font-face {*/ 4 | /* font-family: "Roboto";*/ 5 | /* src: url("https://cdn.boblog.com/Roboto-Regular.ttf");*/ 6 | /*}*/ 7 | 8 | /*@font-face {*/ 9 | /* font-family: "NotoSansTC";*/ 10 | /* src: url("https://cdn.boblog.com/NotoSansTC-Regular.otf");*/ 11 | /*}*/ 12 | 13 | @font-face { 14 | font-family: "SourceCodePro-Regular"; 15 | src: url("https://cdn.boblog.com/SourceCodePro-Regular.ttf"); 16 | font-weight: normal; 17 | } 18 | /*@font-face {*/ 19 | /* font-family: "SourceCodePro";*/ 20 | /* src: url("https://cdn.boblog.com/SourceCodePro-Bold.ttf");*/ 21 | /* font-weight: bold;*/ 22 | /*}*/ 23 | @font-face { 24 | font-family: "SourceCodePro"; 25 | src: url("https://cdn.boblog.com/SourceCodePro-Medium.ttf"); 26 | font-weight: 600; 27 | } 28 | /*@font-face {*/ 29 | /* font-family: "JetBrainsMono";*/ 30 | /* src: url("https://cdn.boblog.com/JetBrainsMono-VariableFont_wght.ttf");*/ 31 | /*}*/ 32 | 33 | /*html,*/ 34 | /*body,*/ 35 | /*div,*/ 36 | /*h1,*/ 37 | /*h2,*/ 38 | /*h3,*/ 39 | /*h4,*/ 40 | /*h5,*/ 41 | /*h6,*/ 42 | /*a,*/ 43 | /*p,*/ 44 | /*ul,*/ 45 | /*li {*/ 46 | /* padding: 0;*/ 47 | /* margin: 0;*/ 48 | /* box-sizing: border-box;*/ 49 | /*}*/ 50 | 51 | /*a {*/ 52 | /* color: #0164da;*/ 53 | /* text-decoration: none;*/ 54 | /*}*/ 55 | 56 | /*li {*/ 57 | /* list-style: none;*/ 58 | /*}*/ 59 | 60 | /*img {*/ 61 | /* width: 100%;*/ 62 | /*}*/ 63 | 64 | /*pre {*/ 65 | /* padding: 1em;*/ 66 | /*}*/ 67 | /*pre code.hljs {*/ 68 | /* display: block;*/ 69 | /* overflow-x: auto;*/ 70 | /* padding: 1em*/ 71 | /*}*/ 72 | 73 | /*code.hljs {*/ 74 | /* padding: 3px 5px*/ 75 | /*}*/ 76 | html, body { 77 | margin: 0; 78 | padding: 0; 79 | font-family: SourceCodePro-Regular; 80 | } 81 | img { 82 | width: 100%; 83 | } 84 | 85 | .hljs { 86 | box-sizing: border-box; 87 | color: #abb2bf; 88 | font-size: 13px; 89 | background: #282c34; 90 | padding: 16px; 91 | border-radius: 4px; 92 | overflow: hidden; 93 | overflow-x: scroll; 94 | } 95 | .hljs code { 96 | line-height: 1.5; 97 | box-sizing: border-box; 98 | font-family: SourceCodePro; 99 | /*font-family: "JetBrainsMono";*/ 100 | } 101 | 102 | .hljs-comment,.hljs-quote { 103 | color: #5c6370; 104 | font-style: italic 105 | } 106 | 107 | .hljs-doctag,.hljs-formula,.hljs-keyword { 108 | color: #c678dd 109 | } 110 | 111 | .hljs-deletion,.hljs-name,.hljs-section,.hljs-selector-tag,.hljs-subst { 112 | color: #e06c75 113 | } 114 | 115 | .hljs-literal { 116 | color: #56b6c2 117 | } 118 | 119 | .hljs-addition,.hljs-attribute,.hljs-meta .hljs-string,.hljs-regexp,.hljs-string { 120 | color: #98c379 121 | } 122 | 123 | .hljs-attr,.hljs-number,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-pseudo,.hljs-template-variable,.hljs-type,.hljs-variable { 124 | color: #d19a66 125 | } 126 | 127 | .hljs-bullet,.hljs-link,.hljs-meta,.hljs-selector-id,.hljs-symbol,.hljs-title { 128 | color: #61aeee 129 | } 130 | 131 | .hljs-built_in,.hljs-class .hljs-title,.hljs-title.class_ { 132 | color: #e6c07b 133 | } 134 | 135 | .hljs-emphasis { 136 | font-style: italic 137 | } 138 | 139 | .hljs-strong { 140 | font-weight: 700 141 | } 142 | 143 | .hljs-link { 144 | text-decoration: underline 145 | } 146 | 147 | .opacity { 148 | opacity: 0.5; 149 | } 150 | 151 | .pagination { 152 | box-sizing: border-box; 153 | width: 100%; 154 | display: flex; 155 | align-items: center; 156 | justify-content: center; 157 | padding: 15px 0; 158 | } 159 | 160 | .response-wrap { 161 | box-sizing: border-box; 162 | max-width: 61.8%; 163 | padding: 0; 164 | margin: 0 auto; 165 | } 166 | 167 | 168 | .progress-indicator { 169 | position: fixed; 170 | top: 0; 171 | left: 0; 172 | height: 3px; 173 | background-color: #0A74DA; 174 | } 175 | 176 | @media screen and (max-width: 540px) { 177 | .response-wrap { 178 | max-width: 100%; 179 | padding: 0 24px; 180 | } 181 | } 182 | 183 | .page-enter-active, .page-leave-active { 184 | transition: opacity 500ms; 185 | } 186 | 187 | .page-enter, .page-leave-active { 188 | opacity: 0; 189 | } 190 | -------------------------------------------------------------------------------- /pages/article.vue: -------------------------------------------------------------------------------- 1 | 41 | 129 | 130 | 209 | 210 | -------------------------------------------------------------------------------- /components/common/Header.vue: -------------------------------------------------------------------------------- 1 | 51 | 52 | 132 | 133 | 232 | -------------------------------------------------------------------------------- /pages/index.vue: -------------------------------------------------------------------------------- 1 | 41 | 151 | 152 | 247 | -------------------------------------------------------------------------------- /components/common/LoginForm.vue: -------------------------------------------------------------------------------- 1 | 51 | 168 | 260 | -------------------------------------------------------------------------------- /pages/hutao.vue: -------------------------------------------------------------------------------- 1 | 91 | 220 | 313 | -------------------------------------------------------------------------------- /lib/progress-indicator.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | const root = (typeof self === 'object' && self.self === self && self) || 3 | (typeof global === 'object' && global.global === global && global) || 4 | this || {}; 5 | 6 | // 兼容处理 7 | let lastTime = 0; 8 | const vendors = ['webkit', 'moz']; 9 | for (let x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { 10 | window.requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame']; 11 | window.cancelAnimationFrame = window[vendors[x] + 'CancelAnimationFrame'] || // Webkit中此取消方法的名字变了 12 | window[vendors[x] + 'CancelRequestAnimationFrame']; 13 | } 14 | 15 | if (!window.requestAnimationFrame) { 16 | window.requestAnimationFrame = function(callback, element) { 17 | const currTime = new Date().getTime(); 18 | const timeToCall = Math.max(0, 16.7 - (currTime - lastTime)); 19 | const id = window.setTimeout(function() { 20 | // eslint-disable-next-line node/no-callback-literal 21 | callback(currTime + timeToCall); 22 | }, timeToCall); 23 | lastTime = currTime + timeToCall; 24 | return id; 25 | }; 26 | } 27 | if (!window.cancelAnimationFrame) { 28 | window.cancelAnimationFrame = function(id) { 29 | clearTimeout(id); 30 | }; 31 | } 32 | 33 | const util = { 34 | extend(target) { 35 | for (let i = 1, len = arguments.length; i < len; i++) { 36 | for (const prop in arguments[i]) { 37 | if (Object.prototype.hasOwnProperty.call(arguments[i], prop)) { 38 | target[prop] = arguments[i][prop] 39 | } 40 | } 41 | } 42 | 43 | return target 44 | }, 45 | getViewPortSizeHeight() { 46 | const w = window; 47 | if (w.innerWidth != null) return w.innerHeight; 48 | const d = w.document; 49 | // 表明是标准模式 50 | if (document.compatMode === "CSS1Compat") { 51 | return d.documentElement.clientHeight; 52 | } 53 | // 怪异模式 54 | return d.body.clientHeight; 55 | }, 56 | getScrollOffsetsTop() { 57 | const w = window; 58 | if (w.pageXOffset != null) return w.pageYOffset; 59 | const d = w.document; 60 | // 表明是标准模式 61 | if (document.compatMode === "CSS1Compat") { 62 | return d.documentElement.scrollTop; 63 | } 64 | // 怪异模式 65 | return d.body.scrollTop; 66 | }, 67 | addEvent(element, type, fn) { 68 | if (document.addEventListener) { 69 | element.addEventListener(type, fn, false); 70 | return fn; 71 | } else if (document.attachEvent) { 72 | const bound = function() { 73 | return fn.apply(element, arguments) 74 | } 75 | element.attachEvent('on' + type, bound); 76 | return bound; 77 | } 78 | }, 79 | isValidListener(listener) { 80 | if (typeof listener === 'function') { 81 | return true 82 | } else if (listener && typeof listener === 'object') { 83 | return util.isValidListener(listener.listener) 84 | } else { 85 | return false 86 | } 87 | }, 88 | indexOf(array, item) { 89 | if (array.indexOf) { 90 | return array.indexOf(item); 91 | } 92 | else { 93 | let result = -1; 94 | for (let i = 0, len = array.length; i < len; i++) { 95 | if (array[i] === item) { 96 | result = i; 97 | break; 98 | } 99 | } 100 | return result; 101 | } 102 | }, 103 | // 移除事件 104 | removeEvent (element, type, fn) { 105 | if (document.removeEventListener) { 106 | element.removeEventListener(type, fn, false); 107 | return fn; 108 | } else if (document.attachEvent) { 109 | const bound = function () { 110 | return fn.apply(element, arguments) 111 | } 112 | element.detachEvent('on' + type, bound); 113 | return bound; 114 | } 115 | }, 116 | // 是否为id选择器 117 | isIdSelector(selector) { 118 | return selector.includes('#') 119 | }, 120 | // 是否为类选择器 121 | isClassSelector(selector) { 122 | return selector.includes('.') 123 | }, 124 | // 获取元素 125 | getEle(selector) { 126 | const doc = document 127 | let dom = null 128 | 129 | if (doc.querySelector) { 130 | dom = doc.querySelector(selector) 131 | } 132 | 133 | if (!dom && util.isIdSelector(selector)) { 134 | const ids = selector.split('#')[1] 135 | dom = doc.getElementById(ids) 136 | } 137 | 138 | if (!dom && util.isClassSelector(selector)) { 139 | const cls = selector.split('.')[1] 140 | const selectDom = doc.getElementsByClassName(cls) 141 | dom = selectDom ? selectDom[0] : null 142 | } 143 | 144 | return dom 145 | }, 146 | // 移除DOM元素 147 | removeDom (selector) { 148 | if (selector && typeof selector === 'string') { 149 | const dom = util.getEle(selector) 150 | dom && dom.parentNode && dom.parentNode.removeChild(dom) 151 | } 152 | }, 153 | isFunction(fn) { 154 | return typeof fn === 'function' 155 | } 156 | }; 157 | 158 | function EventEmitter() { 159 | this.__events = {} 160 | } 161 | 162 | EventEmitter.prototype.on = function(eventName, listener) { 163 | if (!eventName || !listener) return; 164 | 165 | if (!util.isValidListener(listener)) { 166 | throw new TypeError('listener must be a function'); 167 | } 168 | 169 | const events = this.__events; 170 | const listeners = events[eventName] = events[eventName] || []; 171 | const listenerIsWrapped = typeof listener === 'object'; 172 | 173 | // 不重复添加事件 174 | if (!util.includes(listeners, listener)) { 175 | listeners.push(listenerIsWrapped ? listener : { 176 | listener, 177 | once: false 178 | }); 179 | } 180 | 181 | return this; 182 | }; 183 | EventEmitter.prototype.once = function(eventName, listener) { 184 | return this.on(eventName, { 185 | listener, 186 | once: true 187 | }) 188 | }; 189 | EventEmitter.prototype.off = function(eventName, listener) { 190 | const listeners = this.__events[eventName]; 191 | if (!listeners) return; 192 | 193 | let index; 194 | for (let i = 0, len = listeners.length; i < len; i++) { 195 | if (listeners[i] && listeners[i].listener === listener) { 196 | index = i; 197 | break; 198 | } 199 | } 200 | 201 | if (typeof index !== 'undefined') { 202 | listeners.splice(index, 1, null) 203 | } 204 | 205 | return this; 206 | }; 207 | EventEmitter.prototype.emit = function(eventName, args) { 208 | const listeners = this.__events[eventName]; 209 | if (!listeners) return; 210 | 211 | for (let i = 0; i < listeners.length; i++) { 212 | const listener = listeners[i]; 213 | if (listener) { 214 | listener.listener.apply(this, args || []); 215 | if (listener.once) { 216 | this.off(eventName, listener.listener) 217 | } 218 | } 219 | 220 | } 221 | 222 | return this; 223 | 224 | }; 225 | 226 | function ProgressIndicator(options) { 227 | 228 | this.options = util.extend({}, this.constructor.defaultOptions, options) 229 | 230 | this.handlers = {}; 231 | 232 | this.init(); 233 | } 234 | 235 | ProgressIndicator.VERSION = '1.0.0'; 236 | 237 | ProgressIndicator.defaultOptions = { 238 | color: "#0A74DA" 239 | } 240 | 241 | const proto = ProgressIndicator.prototype = new EventEmitter(); 242 | 243 | proto.constructor = ProgressIndicator; 244 | 245 | proto.init = function() { 246 | this.createIndicator(); 247 | const width = this.calculateWidthPrecent(); 248 | this.setWidth(width); 249 | this.bindScrollEvent(); 250 | 251 | } 252 | 253 | proto.createIndicator = function() { 254 | const div = document.createElement("div") 255 | 256 | div.id = "progress-indicator"; 257 | div.className = "progress-indicator"; 258 | 259 | // div.style.position = "fixed" 260 | // div.style.top = 0; 261 | // div.style.left = 0; 262 | // div.style.height = '3px'; 263 | // div.style.backgroundColor = this.options.color; 264 | 265 | this.element = div; 266 | 267 | document.body.appendChild(div); 268 | } 269 | 270 | proto.calculateWidthPrecent = function() { 271 | // 文档高度 272 | this.docHeight = Math.max(document.documentElement.scrollHeight, document.documentElement.clientHeight) 273 | 274 | // 视口高度 275 | this.viewPortHeight = util.getViewPortSizeHeight(); 276 | // 差值 277 | this.sHeight = Math.max(this.docHeight - this.viewPortHeight, 0); 278 | // 滚动条垂直偏移量 279 | const scrollTop = util.getScrollOffsetsTop(); 280 | 281 | return this.sHeight ? scrollTop / this.sHeight : 0; 282 | } 283 | 284 | proto.setWidth = function(perc) { 285 | this.element.style.width = perc * 100 + "%"; 286 | } 287 | 288 | proto.bindScrollEvent = function() { 289 | const self = this; 290 | let prev; 291 | 292 | this.scrollHandler = function () { 293 | window.requestAnimationFrame(function () { 294 | const perc = Math.min(util.getScrollOffsetsTop() / self.sHeight, 1); 295 | // 火狐中有可能连续计算为 1,导致 end 事件被触发两次 296 | if (perc === prev) return; 297 | // 在达到 100% 的时候,刷新页面不 emit end 事件 298 | if (prev && perc === 1) { 299 | self.emit("end") 300 | } 301 | 302 | prev = perc; 303 | self.setWidth(perc); 304 | }); 305 | } 306 | 307 | util.addEvent(window, "scroll", this.scrollHandler) 308 | } 309 | 310 | // 移除进度条 311 | proto.removeProgress = function (fn) { 312 | if (util.isFunction(this.scrollHandler)) { 313 | util.removeEvent(window, "scroll", this.scrollHandler) 314 | util.removeDom("#progress-indicator") 315 | 316 | // 回调函数 317 | util.isFunction(fn) && fn() 318 | } 319 | } 320 | 321 | if (typeof exports !== 'undefined' && !exports.nodeType) { 322 | if (typeof module !== 'undefined' && !module.nodeType && module.exports) { 323 | exports = module.exports = ProgressIndicator; 324 | } 325 | exports.ProgressIndicator = ProgressIndicator; 326 | } else { 327 | root.ProgressIndicator = ProgressIndicator; 328 | } 329 | 330 | }()); 331 | -------------------------------------------------------------------------------- /components/article/ArticleComment.vue: -------------------------------------------------------------------------------- 1 |