├── .editorconfig ├── .env.development ├── .env.production ├── .env.staging ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .travis.yml ├── LICENSE ├── README-zh.md ├── README.md ├── babel.config.js ├── build └── index.js ├── images ├── api-doc.png ├── archives.png ├── article-1.png ├── article-2.png ├── article-manage.png ├── category.png ├── collect.png ├── edit.png ├── friend-link.png ├── index-1.png ├── index-2.png ├── login.png ├── message-1.png ├── message-2.png ├── mobile │ ├── archives.png │ ├── article-1.png │ ├── category.png │ ├── collect.png │ ├── friend-link.png │ ├── index.png │ ├── message.png │ └── user.png ├── rebind-mobile.png ├── reset-password.png ├── tag.png ├── user-manage.png └── user.png ├── jest.config.js ├── jsconfig.json ├── mock ├── index.js ├── mock-server.js ├── table.js └── user.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── favicon.ico └── index.html ├── src ├── App.vue ├── api │ ├── archives.js │ ├── article.js │ ├── category.js │ ├── client.js │ ├── code.js │ ├── collect.js │ ├── comment.js │ ├── file.js │ ├── friend-link.js │ ├── like.js │ ├── message.js │ ├── tag.js │ └── user.js ├── assets │ ├── 404_images │ │ ├── 404.png │ │ └── 404_cloud.png │ ├── audio │ │ ├── index.css │ │ └── index.js │ ├── js │ │ ├── 1e7b63404cd2fb8e6525b2fd4ee4d286.png │ │ ├── live2d-message.js │ │ ├── quill-emoji.css │ │ └── quill-emoji.js │ └── quill-emoji │ │ └── quill-emoji.js ├── components │ ├── ArticleList │ │ └── index.vue │ ├── Breadcrumb │ │ └── index.vue │ ├── DynamicInput │ │ └── index.vue │ ├── Hamburger │ │ └── index.vue │ ├── Header │ │ ├── LoginDialog.vue │ │ ├── RegisterDialog.vue │ │ └── index.vue │ ├── Live2d │ │ ├── dynamicLoadScript.js │ │ └── index.vue │ ├── SvgIcon │ │ └── index.vue │ └── mixin │ │ └── ResizeHandler.js ├── config │ ├── editor.js │ └── sections.js ├── element-ui.js ├── icons │ ├── index.js │ ├── svg │ │ ├── archives-menu.svg │ │ ├── art-edit.svg │ │ ├── art-list.svg │ │ ├── art-manage.svg │ │ ├── category-manage.svg │ │ ├── client-manage.svg │ │ ├── collect-manage.svg │ │ ├── collect.svg │ │ ├── copy.svg │ │ ├── dashboard.svg │ │ ├── example.svg │ │ ├── eye-open.svg │ │ ├── eye.svg │ │ ├── file-manage.svg │ │ ├── friend-chain-manage.svg │ │ ├── gitee-login.svg │ │ ├── github-login.svg │ │ ├── like.svg │ │ ├── line.svg │ │ ├── link.svg │ │ ├── logo.svg │ │ ├── menu.svg │ │ ├── nested.svg │ │ ├── password.svg │ │ ├── qq-login.svg │ │ ├── qq.svg │ │ ├── recommend-manage.svg │ │ ├── table.svg │ │ ├── tag-manage.svg │ │ ├── tag.svg │ │ ├── tree.svg │ │ ├── user-manage.svg │ │ ├── user.svg │ │ └── wx.svg │ └── svgo.yml ├── layout │ ├── components │ │ ├── AppMain.vue │ │ ├── Navbar.vue │ │ ├── Sidebar │ │ │ ├── FixiOSBug.js │ │ │ ├── Item.vue │ │ │ ├── Link.vue │ │ │ ├── Logo.vue │ │ │ ├── SidebarItem.vue │ │ │ └── index.vue │ │ └── index.js │ ├── index.vue │ └── mixin │ │ └── ResizeHandler.js ├── main.js ├── permission.js ├── router │ └── index.js ├── settings.js ├── store │ ├── getters.js │ ├── index.js │ └── modules │ │ ├── app.js │ │ ├── login.js │ │ ├── permission.js │ │ ├── settings.js │ │ └── user.js ├── styles │ ├── element-ui.scss │ ├── heilingt.css │ ├── index.scss │ ├── mixin.scss │ ├── sidebar.scss │ ├── transition.scss │ └── variables.scss ├── utils │ ├── auth.js │ ├── get-page-title.js │ ├── index.js │ ├── request.js │ └── validate.js └── views │ ├── 404.vue │ ├── about │ └── index.vue │ ├── archives │ └── index.vue │ ├── article-manage │ ├── components │ │ ├── ArticlePreview.vue │ │ ├── DynamicTags.vue │ │ └── ImgUpload.vue │ ├── edit.vue │ ├── edit_bank.vue │ └── list.vue │ ├── article │ ├── components │ │ ├── ArtTags.vue │ │ ├── CommentList.vue │ │ ├── CopyRight.vue │ │ ├── InterrelatedList.vue │ │ ├── SupportAuthor.vue │ │ └── SuspendedPanel.vue │ └── index.vue │ ├── bind-mobile │ └── index.vue │ ├── category-manage │ └── index.vue │ ├── category │ └── index.vue │ ├── client-manage │ └── index.vue │ ├── collect-manage │ └── index.vue │ ├── email-validate │ └── index.vue │ ├── file-manage │ ├── components │ │ └── ImgUpload.vue │ └── index.vue │ ├── friend-link-manage │ └── index.vue │ ├── friend-link │ └── index.vue │ ├── index │ ├── components │ │ ├── RightSideAbout.vue │ │ ├── RightSideComment.vue │ │ ├── RightSideRecommend.vue │ │ ├── RightSideSection.vue │ │ └── RightSideTags.vue │ └── index.vue │ ├── message │ └── index.vue │ ├── oauth │ └── index.vue │ ├── privacy │ └── index.vue │ ├── rebind-mobile │ └── index.vue │ ├── recommend-manage │ ├── components │ │ └── ArticlePreview.vue │ └── index.vue │ ├── reset-password │ └── index.vue │ ├── search │ └── index.vue │ ├── tag-manage │ └── index.vue │ ├── tag │ └── index.vue │ ├── terms │ └── index.vue │ ├── user-manage │ └── index.vue │ └── user │ └── index.vue ├── tests └── unit │ ├── .eslintrc.js │ ├── components │ ├── Breadcrumb.spec.js │ ├── Hamburger.spec.js │ └── SvgIcon.spec.js │ └── utils │ ├── formatTime.spec.js │ ├── parseTime.spec.js │ └── validate.spec.js └── vue.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | insert_final_newline = false 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | # just a flag 2 | ENV = 'development' 3 | 4 | # base api 5 | VUE_APP_BASE_API = 'https://www.poile.cn/api' 6 | 7 | # vue-cli uses the VUE_CLI_BABEL_TRANSPILE_MODULES environment variable, 8 | # to control whether the babel-plugin-dynamic-import-node plugin is enabled. 9 | # It only does one thing by converting all import() to require(). 10 | # This configuration can significantly increase the speed of hot updates, 11 | # when you have a large number of pages. 12 | # Detail: https://github.com/vuejs/vue-cli/blob/dev/packages/@vue/babel-preset-app/index.js 13 | 14 | VUE_CLI_BABEL_TRANSPILE_MODULES = true 15 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | # just a flag 2 | ENV = 'production' 3 | 4 | # base api 5 | VUE_APP_BASE_API = 'https://www.poile.cn/api' 6 | 7 | -------------------------------------------------------------------------------- /.env.staging: -------------------------------------------------------------------------------- 1 | NODE_ENV = production 2 | 3 | # just a flag 4 | ENV = 'staging' 5 | 6 | # base api 7 | VUE_APP_BASE_API = '/stage-api' 8 | 9 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/*.js 2 | src/assets 3 | public 4 | dist 5 | src/views/about/* 6 | src/views/terms/* 7 | src/views/privacy/* 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | package-lock.json 8 | tests/**/coverage/ 9 | 10 | # Editor directories and files 11 | .idea 12 | .vscode 13 | *.suo 14 | *.ntvs* 15 | *.njsproj 16 | *.sln 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 10 3 | script: npm run test 4 | notifications: 5 | email: false 6 | -------------------------------------------------------------------------------- /README-zh.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### 简介 2 | 3 | 本项目技术栈基于[vue](https://cn.vuejs.org/index.html)、[vuex](https://vuex.vuejs.org/zh-cn/)、[vue-router](https://router.vuejs.org/zh-cn/) 、[vue-cli](https://github.com/vuejs/vue-cli) 、[axios](https://github.com/axios/axios) 和 [element-ui](https://github.com/ElemeFE/element),是一个vue全家桶 + element-ui编写的博客前端项目,简约风格,功能完善,具备完备的后台管理和前端数据展示,同时项目通过媒体查询等技术适配移动端,是一个响应式的前端项目。 4 | 国内Gitee:[https://gitee.com/copoile/blog-web.git](https://gitee.com/copoile/blog-web) 5 | 国外Github:[https://github.com/copoile/blog-web](https://github.com/copoile/blog-web.git) 6 | 7 | > 数据通过接口Api方式获取,因此前端项目运行需启动后端项目,后端项目地址:https://github.com/copoile/blog-api.git 8 | 9 | **在线接口文档** 10 | 11 | 国外: [https://copoile.github.io](https://copoile.github.io) 12 | 国内: [http://copoile.gitee.io/blog-doc](http://copoile.gitee.io/blog-doc) 13 | 14 | > Api文档使用GitBook编写,文档源码地址: [https://github.com/copoile/blog-api-doc.git](https://github.com/copoile/blog-api-doc.git) 15 | 16 | ![](./images/api-doc.png) 17 | 18 | 19 | 20 | ### 功能 21 | 22 | ``` 23 | - 账号注册 24 | - 账号登录 / 手机号登录 25 | - 第三方登录 / QQ / Github / Gitee 26 | - 个人信息 27 | - 绑定邮箱 / 绑定手机号 28 | - 修改密码 / 重置密码 29 | - 文章编辑(MarkDown编辑器) 30 | - 文章管理 31 | - 文章推荐 32 | - 用户管理 33 | - 分类管理 34 | - 标签管理 35 | - 收藏管理 36 | - 博客友链 37 | - 客户端管理 38 | - 首页 39 | - 分类 40 | - 归档 41 | - 标签 42 | - 友链 43 | - 留言 44 | - 文章 / 收藏 / 点赞 / 评论 / 回复 / 相关推荐 45 | - Live2d 46 | - 其他 47 | ``` 48 | 49 | 50 | 51 | ### 开发 52 | 53 | ``` 54 | # 克隆项目 55 | git clone https://github.com/copoile/blog-web.git 56 | 57 | # 进入项目目录 58 | cd blog-web 59 | 60 | # 安装依赖 61 | npm install (node版本v14.18.0) 62 | 63 | # 建议不要直接使用 cnpm 安装依赖,会有各种诡异的 bug。可以通过如下操作解决 npm 下载速度慢的问题 64 | npm install --registry=https://registry.npm.taobao.org 65 | 66 | # 启动服务 67 | npm run dev 68 | 69 | # 打包项目 70 | npm run build:prod 71 | ``` 72 | 73 | ### 线上 74 | 目前网站已正式上线,如需查看完整效果可查看网站: 75 | [https://www.poile.cn](https://www.poile.cn) 76 | 77 | > 觉得不错的话,多支持下low逼弟弟哦~ 78 | 79 | 80 | ### 项目展示 81 | 82 | ![](./images/index-1.png) 83 | 84 | 85 | 86 | 87 | 88 | ![](./images/index-2.png) 89 | 90 | 91 | 92 | ![](./images/article-1.png) 93 | 94 | 95 | 96 | ![](./images/category.png) 97 | 98 | ![](./images/archives.png) 99 | 100 | 101 | 102 | ![](./images/friend-link.png) 103 | 104 | 105 | 106 | ![](./images/tag.png) 107 | 108 | 109 | 110 | ![](./images/message-1.png) 111 | 112 | 113 | 114 | ![](./images/user.png) 115 | 116 | 117 | 118 | ![](./images/article-manage.png) 119 | 120 | 121 | 122 | ![](./images/edit.png) 123 | 124 | 125 | 126 | ![](./images/mobile/index.png) 127 | 128 | 129 | 130 | 131 | 132 | ![](./images/mobile/category.png) 133 | 134 | 135 | 136 | 137 | 138 | ![](./images/mobile/article-1.png) 139 | 140 | 141 | 142 | 143 | 144 | ![](./images/mobile/user.png) 145 | 146 | 147 | 148 | ### License 149 | 150 | [Apache License 2.0](http://apache.org/licenses/LICENSE-2.0.html) 151 | 152 | Copyright (c) 2020-present Yaohw 153 | 154 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/app', 4 | ["@babel/preset-env", { "modules": false }] 5 | ], 6 | plugins: [ 7 | [ 8 | "component", 9 | { 10 | "libraryName": "element-ui", 11 | "styleLibraryName": "theme-chalk" 12 | } 13 | ] 14 | ] 15 | } -------------------------------------------------------------------------------- /build/index.js: -------------------------------------------------------------------------------- 1 | const { run } = require('runjs') 2 | const chalk = require('chalk') 3 | const config = require('../vue.config.js') 4 | const rawArgv = process.argv.slice(2) 5 | const args = rawArgv.join(' ') 6 | 7 | if (process.env.npm_config_preview || rawArgv.includes('--preview')) { 8 | const report = rawArgv.includes('--report') 9 | 10 | run(`vue-cli-service build ${args}`) 11 | 12 | const port = 9526 13 | const publicPath = config.publicPath 14 | 15 | var connect = require('connect') 16 | var serveStatic = require('serve-static') 17 | const app = connect() 18 | 19 | app.use( 20 | publicPath, 21 | serveStatic('./dist', { 22 | index: ['index.html', '/'] 23 | }) 24 | ) 25 | 26 | app.listen(port, function () { 27 | console.log(chalk.green(`> Preview at http://localhost:${port}${publicPath}`)) 28 | if (report) { 29 | console.log(chalk.green(`> Report at http://localhost:${port}${publicPath}report.html`)) 30 | } 31 | 32 | }) 33 | } else { 34 | run(`vue-cli-service build ${args}`) 35 | } 36 | -------------------------------------------------------------------------------- /images/api-doc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/copoile/blog-web/caa3b904cee9a316136b6008f367b1b9b85e413f/images/api-doc.png -------------------------------------------------------------------------------- /images/archives.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/copoile/blog-web/caa3b904cee9a316136b6008f367b1b9b85e413f/images/archives.png -------------------------------------------------------------------------------- /images/article-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/copoile/blog-web/caa3b904cee9a316136b6008f367b1b9b85e413f/images/article-1.png -------------------------------------------------------------------------------- /images/article-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/copoile/blog-web/caa3b904cee9a316136b6008f367b1b9b85e413f/images/article-2.png -------------------------------------------------------------------------------- /images/article-manage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/copoile/blog-web/caa3b904cee9a316136b6008f367b1b9b85e413f/images/article-manage.png -------------------------------------------------------------------------------- /images/category.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/copoile/blog-web/caa3b904cee9a316136b6008f367b1b9b85e413f/images/category.png -------------------------------------------------------------------------------- /images/collect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/copoile/blog-web/caa3b904cee9a316136b6008f367b1b9b85e413f/images/collect.png -------------------------------------------------------------------------------- /images/edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/copoile/blog-web/caa3b904cee9a316136b6008f367b1b9b85e413f/images/edit.png -------------------------------------------------------------------------------- /images/friend-link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/copoile/blog-web/caa3b904cee9a316136b6008f367b1b9b85e413f/images/friend-link.png -------------------------------------------------------------------------------- /images/index-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/copoile/blog-web/caa3b904cee9a316136b6008f367b1b9b85e413f/images/index-1.png -------------------------------------------------------------------------------- /images/index-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/copoile/blog-web/caa3b904cee9a316136b6008f367b1b9b85e413f/images/index-2.png -------------------------------------------------------------------------------- /images/login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/copoile/blog-web/caa3b904cee9a316136b6008f367b1b9b85e413f/images/login.png -------------------------------------------------------------------------------- /images/message-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/copoile/blog-web/caa3b904cee9a316136b6008f367b1b9b85e413f/images/message-1.png -------------------------------------------------------------------------------- /images/message-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/copoile/blog-web/caa3b904cee9a316136b6008f367b1b9b85e413f/images/message-2.png -------------------------------------------------------------------------------- /images/mobile/archives.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/copoile/blog-web/caa3b904cee9a316136b6008f367b1b9b85e413f/images/mobile/archives.png -------------------------------------------------------------------------------- /images/mobile/article-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/copoile/blog-web/caa3b904cee9a316136b6008f367b1b9b85e413f/images/mobile/article-1.png -------------------------------------------------------------------------------- /images/mobile/category.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/copoile/blog-web/caa3b904cee9a316136b6008f367b1b9b85e413f/images/mobile/category.png -------------------------------------------------------------------------------- /images/mobile/collect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/copoile/blog-web/caa3b904cee9a316136b6008f367b1b9b85e413f/images/mobile/collect.png -------------------------------------------------------------------------------- /images/mobile/friend-link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/copoile/blog-web/caa3b904cee9a316136b6008f367b1b9b85e413f/images/mobile/friend-link.png -------------------------------------------------------------------------------- /images/mobile/index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/copoile/blog-web/caa3b904cee9a316136b6008f367b1b9b85e413f/images/mobile/index.png -------------------------------------------------------------------------------- /images/mobile/message.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/copoile/blog-web/caa3b904cee9a316136b6008f367b1b9b85e413f/images/mobile/message.png -------------------------------------------------------------------------------- /images/mobile/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/copoile/blog-web/caa3b904cee9a316136b6008f367b1b9b85e413f/images/mobile/user.png -------------------------------------------------------------------------------- /images/rebind-mobile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/copoile/blog-web/caa3b904cee9a316136b6008f367b1b9b85e413f/images/rebind-mobile.png -------------------------------------------------------------------------------- /images/reset-password.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/copoile/blog-web/caa3b904cee9a316136b6008f367b1b9b85e413f/images/reset-password.png -------------------------------------------------------------------------------- /images/tag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/copoile/blog-web/caa3b904cee9a316136b6008f367b1b9b85e413f/images/tag.png -------------------------------------------------------------------------------- /images/user-manage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/copoile/blog-web/caa3b904cee9a316136b6008f367b1b9b85e413f/images/user-manage.png -------------------------------------------------------------------------------- /images/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/copoile/blog-web/caa3b904cee9a316136b6008f367b1b9b85e413f/images/user.png -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: ['js', 'jsx', 'json', 'vue'], 3 | transform: { 4 | '^.+\\.vue$': 'vue-jest', 5 | '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 6 | 'jest-transform-stub', 7 | '^.+\\.jsx?$': 'babel-jest' 8 | }, 9 | moduleNameMapper: { 10 | '^@/(.*)$': '/src/$1' 11 | }, 12 | snapshotSerializers: ['jest-serializer-vue'], 13 | testMatch: [ 14 | '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)' 15 | ], 16 | collectCoverageFrom: ['src/utils/**/*.{js,vue}', '!src/utils/auth.js', '!src/utils/request.js', 'src/components/**/*.{js,vue}'], 17 | coverageDirectory: '/tests/unit/coverage', 18 | // 'collectCoverage': true, 19 | 'coverageReporters': [ 20 | 'lcov', 21 | 'text-summary' 22 | ], 23 | testURL: 'http://localhost/' 24 | } 25 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "paths": { 5 | "@/*": ["src/*"] 6 | } 7 | }, 8 | "exclude": ["node_modules", "dist"] 9 | } 10 | -------------------------------------------------------------------------------- /mock/index.js: -------------------------------------------------------------------------------- 1 | import Mock from 'mockjs' 2 | import { param2Obj } from '../src/utils' 3 | 4 | import user from './user' 5 | import table from './table' 6 | 7 | const mocks = [ 8 | ...user, 9 | ...table 10 | ] 11 | 12 | // for front mock 13 | // please use it cautiously, it will redefine XMLHttpRequest, 14 | // which will cause many of your third-party libraries to be invalidated(like progress event). 15 | export function mockXHR() { 16 | // mock patch 17 | // https://github.com/nuysoft/Mock/issues/300 18 | Mock.XHR.prototype.proxy_send = Mock.XHR.prototype.send 19 | Mock.XHR.prototype.send = function() { 20 | if (this.custom.xhr) { 21 | this.custom.xhr.withCredentials = this.withCredentials || false 22 | 23 | if (this.responseType) { 24 | this.custom.xhr.responseType = this.responseType 25 | } 26 | } 27 | this.proxy_send(...arguments) 28 | } 29 | 30 | function XHR2ExpressReqWrap(respond) { 31 | return function(options) { 32 | let result = null 33 | if (respond instanceof Function) { 34 | const { body, type, url } = options 35 | // https://expressjs.com/en/4x/api.html#req 36 | result = respond({ 37 | method: type, 38 | body: JSON.parse(body), 39 | query: param2Obj(url) 40 | }) 41 | } else { 42 | result = respond 43 | } 44 | return Mock.mock(result) 45 | } 46 | } 47 | 48 | for (const i of mocks) { 49 | Mock.mock(new RegExp(i.url), i.type || 'get', XHR2ExpressReqWrap(i.response)) 50 | } 51 | } 52 | 53 | // for mock server 54 | const responseFake = (url, type, respond) => { 55 | return { 56 | url: new RegExp(`/mock${url}`), 57 | type: type || 'get', 58 | response(req, res) { 59 | res.json(Mock.mock(respond instanceof Function ? respond(req, res) : respond)) 60 | } 61 | } 62 | } 63 | 64 | export default mocks.map(route => { 65 | return responseFake(route.url, route.type, route.response) 66 | }) 67 | -------------------------------------------------------------------------------- /mock/mock-server.js: -------------------------------------------------------------------------------- 1 | const chokidar = require('chokidar') 2 | const bodyParser = require('body-parser') 3 | const chalk = require('chalk') 4 | const path = require('path') 5 | 6 | const mockDir = path.join(process.cwd(), 'mock') 7 | 8 | function registerRoutes(app) { 9 | let mockLastIndex 10 | const { default: mocks } = require('./index.js') 11 | for (const mock of mocks) { 12 | app[mock.type](mock.url, mock.response) 13 | mockLastIndex = app._router.stack.length 14 | } 15 | const mockRoutesLength = Object.keys(mocks).length 16 | return { 17 | mockRoutesLength: mockRoutesLength, 18 | mockStartIndex: mockLastIndex - mockRoutesLength 19 | } 20 | } 21 | 22 | function unregisterRoutes() { 23 | Object.keys(require.cache).forEach(i => { 24 | if (i.includes(mockDir)) { 25 | delete require.cache[require.resolve(i)] 26 | } 27 | }) 28 | } 29 | 30 | module.exports = app => { 31 | // es6 polyfill 32 | require('@babel/register') 33 | 34 | // parse app.body 35 | // https://expressjs.com/en/4x/api.html#req.body 36 | app.use(bodyParser.json()) 37 | app.use(bodyParser.urlencoded({ 38 | extended: true 39 | })) 40 | 41 | const mockRoutes = registerRoutes(app) 42 | var mockRoutesLength = mockRoutes.mockRoutesLength 43 | var mockStartIndex = mockRoutes.mockStartIndex 44 | 45 | // watch files, hot reload mock server 46 | chokidar.watch(mockDir, { 47 | ignored: /mock-server/, 48 | ignoreInitial: true 49 | }).on('all', (event, path) => { 50 | if (event === 'change' || event === 'add') { 51 | try { 52 | // remove mock routes stack 53 | app._router.stack.splice(mockStartIndex, mockRoutesLength) 54 | 55 | // clear routes cache 56 | unregisterRoutes() 57 | 58 | const mockRoutes = registerRoutes(app) 59 | mockRoutesLength = mockRoutes.mockRoutesLength 60 | mockStartIndex = mockRoutes.mockStartIndex 61 | 62 | console.log(chalk.magentaBright(`\n > Mock Server hot reload success! changed ${path}`)) 63 | } catch (error) { 64 | console.log(chalk.redBright(error)) 65 | } 66 | } 67 | }) 68 | } 69 | -------------------------------------------------------------------------------- /mock/table.js: -------------------------------------------------------------------------------- 1 | import Mock from 'mockjs' 2 | 3 | const data = Mock.mock({ 4 | 'items|30': [{ 5 | id: '@id', 6 | title: '@sentence(10, 20)', 7 | 'status|1': ['published', 'draft', 'deleted'], 8 | author: 'name', 9 | display_time: '@datetime', 10 | pageviews: '@integer(300, 5000)' 11 | }] 12 | }) 13 | 14 | export default [ 15 | { 16 | url: '/table/list', 17 | type: 'get', 18 | response: config => { 19 | const items = data.items 20 | return { 21 | code: 20000, 22 | data: { 23 | total: items.length, 24 | items: items 25 | } 26 | } 27 | } 28 | } 29 | ] 30 | -------------------------------------------------------------------------------- /mock/user.js: -------------------------------------------------------------------------------- 1 | 2 | const tokens = { 3 | admin: { 4 | token: 'admin-token' 5 | }, 6 | editor: { 7 | token: 'editor-token' 8 | } 9 | } 10 | 11 | const users = { 12 | 'admin-token': { 13 | roles: ['admin'], 14 | introduction: 'I am a super administrator', 15 | avatar: 'https://poile-img.nos-eastchina1.126.net/1568536623338.png', 16 | name: 'Super Admin' 17 | }, 18 | 'editor-token': { 19 | roles: ['editor'], 20 | introduction: 'I am an editor', 21 | avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif', 22 | name: 'Normal Editor' 23 | } 24 | } 25 | 26 | export default [ 27 | // user login 28 | { 29 | url: '/user/login', 30 | type: 'post', 31 | response: config => { 32 | const { username } = config.body 33 | const token = tokens[username] 34 | 35 | // mock error 36 | if (!token) { 37 | return { 38 | code: 60204, 39 | message: 'Account and password are incorrect.' 40 | } 41 | } 42 | 43 | return { 44 | code: 20000, 45 | data: token 46 | } 47 | } 48 | }, 49 | 50 | // get user info 51 | { 52 | url: '/user/info\.*', 53 | type: 'get', 54 | response: config => { 55 | const { token } = config.query 56 | const info = users[token] 57 | 58 | // mock error 59 | if (!info) { 60 | return { 61 | code: 50008, 62 | message: 'Login failed, unable to get user details.' 63 | } 64 | } 65 | 66 | return { 67 | code: 20000, 68 | data: info 69 | } 70 | } 71 | }, 72 | 73 | // user logout 74 | { 75 | url: '/user/logout', 76 | type: 'post', 77 | response: _ => { 78 | return { 79 | code: 20000, 80 | data: 'success' 81 | } 82 | } 83 | } 84 | ] 85 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blog-web", 3 | "version": "4.2.1", 4 | "description": "a blog web project", 5 | "author": "yaohw", 6 | "license": "Apache", 7 | "scripts": { 8 | "dev": "vue-cli-service serve", 9 | "build:prod": "vue-cli-service build", 10 | "build:stage": "vue-cli-service build --mode staging", 11 | "preview": "node build/index.js --preview", 12 | "lint": "eslint --ext .js,.vue src", 13 | "test:unit": "jest --clearCache && vue-cli-service test:unit", 14 | "test:ci": "npm run lint && npm run test:unit", 15 | "svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml" 16 | }, 17 | "dependencies": { 18 | "animate.css": "^3.7.2", 19 | "axios": "0.18.1", 20 | "babel-preset-env": "^1.7.0", 21 | "element-ui": "^2.13.0", 22 | "js-base64": "^2.5.2", 23 | "js-cookie": "^2.2.1", 24 | "mavon-editor": "^2.7.7", 25 | "normalize.css": "7.0.0", 26 | "nprogress": "0.2.0", 27 | "path-to-regexp": "2.4.0", 28 | "vue": "2.6.10", 29 | "vue-clipboard2": "^0.3.0", 30 | "vue-quill-editor": "^3.0.6", 31 | "vue-router": "3.0.6", 32 | "vuex": "3.1.0" 33 | }, 34 | "devDependencies": { 35 | "@babel/core": "7.0.0", 36 | "@babel/register": "7.0.0", 37 | "@vue/cli-plugin-babel": "3.6.0", 38 | "@vue/cli-plugin-eslint": "^3.9.1", 39 | "@vue/cli-plugin-unit-jest": "3.6.3", 40 | "@vue/cli-service": "3.6.0", 41 | "@vue/test-utils": "1.0.0-beta.29", 42 | "autoprefixer": "^9.5.1", 43 | "babel-core": "7.0.0-bridge.0", 44 | "babel-eslint": "10.0.1", 45 | "babel-jest": "23.6.0", 46 | "babel-plugin-component": "^1.1.1", 47 | "chalk": "2.4.2", 48 | "connect": "3.6.6", 49 | "eslint": "5.15.3", 50 | "eslint-plugin-vue": "5.2.2", 51 | "html-webpack-plugin": "3.2.0", 52 | "mockjs": "1.0.1-beta3", 53 | "node-sass": "^4.14.1", 54 | "runjs": "^4.3.2", 55 | "sass-loader": "^7.3.1", 56 | "script-ext-html-webpack-plugin": "2.1.3", 57 | "script-loader": "0.7.2", 58 | "serve-static": "^1.13.2", 59 | "svg-sprite-loader": "4.1.3", 60 | "svgo": "1.2.2", 61 | "vue-template-compiler": "2.6.10" 62 | }, 63 | "engines": { 64 | "node": ">=8.9", 65 | "npm": ">= 3.0.0" 66 | }, 67 | "browserslist": [ 68 | "> 1%", 69 | "last 2 versions" 70 | ] 71 | } 72 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | 'plugins': { 5 | // to edit target browsers: use "browserslist" field in package.json 6 | 'autoprefixer': {} 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/copoile/blog-web/caa3b904cee9a316136b6008f367b1b9b85e413f/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | <%= webpackConfig.name %> 17 | 18 | 19 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 25 | -------------------------------------------------------------------------------- /src/api/archives.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | /** 4 | * 分页获取归档 5 | * @data {Object} data 6 | */ 7 | export function pageArchives(params) { 8 | return request.get('/article/archives/page', { params }) 9 | } -------------------------------------------------------------------------------- /src/api/article.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | /** 4 | * 保存文章 5 | * @data {Object} data 6 | */ 7 | export function saveArticle(data) { 8 | return request.post('/article/save', data) 9 | } 10 | 11 | /** 12 | * 文章详情(后台) 13 | * @param {Object} id 14 | */ 15 | export function articleDetail(id) { 16 | return request.get(`/article/detail/${id}`) 17 | } 18 | 19 | /** 20 | * 分页获取文章(后台) 21 | * @param {Object} params 22 | */ 23 | export function pageArticle(params) { 24 | return request.get('/article/page', { params }) 25 | } 26 | 27 | /** 28 | * 状态修改 29 | * @param {Object} params 30 | */ 31 | export function updateStatus(params) { 32 | return request({ 33 | url: '/article/status/update', 34 | method: 'post', 35 | params: params 36 | }) 37 | } 38 | 39 | /** 40 | * 分页获取已发布文章(前台) 41 | * @param {Object} params 42 | */ 43 | export function pagePublishedArticle(params) { 44 | return request.get('/article/published/page', { params }) 45 | } 46 | 47 | /** 48 | * 删除文章 49 | * @param {Number} id 50 | */ 51 | export function deleteArticle(id) { 52 | return request.delete(`/article/delete/${id}`) 53 | } 54 | 55 | /** 56 | * 添加或更新推荐 57 | * @param {Object} params 58 | */ 59 | export function addRecommend(params) { 60 | return request({ 61 | url: '/article/recommend/save', 62 | method: 'post', 63 | params: params 64 | }) 65 | } 66 | 67 | /** 68 | * @description 推荐列表 69 | */ 70 | export function recommendList() { 71 | return request.get('/article/recommend/list') 72 | } 73 | 74 | /** 75 | * 移除推荐 76 | * @param {Number} id 77 | */ 78 | export function deleteRecommend(id) { 79 | return request.delete('/article/recommend/delete/' + id) 80 | } 81 | 82 | /** 83 | * @description 浏览文章 84 | * @param {Object} id 85 | */ 86 | export function viewArtilce(id) { 87 | return request.get('/article/view/' + id) 88 | } 89 | 90 | /** 91 | * 自增浏览次数 92 | * @param {Object} id 93 | */ 94 | export function incrementView(id) { 95 | return request.put('/article/increment_view/' + id) 96 | } 97 | 98 | /** 99 | * @description 相关文章 100 | * @param {Object} params 101 | */ 102 | export function interrelated(params) { 103 | return request.get('/article/interrelated/list', { params } ) 104 | } 105 | -------------------------------------------------------------------------------- /src/api/category.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | /** 4 | * 新增分类 5 | * @data {Object} data 6 | */ 7 | export function addCategory(data) { 8 | return request.post('/category/add', data) 9 | } 10 | 11 | /** 12 | * 分页获取分类列表 13 | * @param {Object} params 14 | */ 15 | export function pageCategory(params) { 16 | return request.get('/category/page', { params }) 17 | } 18 | /** 19 | * 获取分类列表 20 | */ 21 | export function categoryList() { 22 | return request.get('/category/list') 23 | } 24 | 25 | /** 26 | * 更新分类 27 | * @param {Object} params 28 | */ 29 | export function updateCategory(params) { 30 | return request({ 31 | url: '/category/update', 32 | method: 'post', 33 | params: params 34 | }) 35 | } 36 | 37 | /** 38 | * 删除分类 39 | * @param {Object} id 40 | */ 41 | export function deleteCategory(id) { 42 | return request.delete('/category/delete/' + id) 43 | } 44 | -------------------------------------------------------------------------------- /src/api/client.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | /** 4 | * 5 | * @data {Object} data 6 | */ 7 | export function pageClient(params) { 8 | return request.get('/client/page', { params }) 9 | } 10 | 11 | /** 12 | * 保存客户端 13 | * @param {Object} data 14 | */ 15 | export function saveClient(data) { 16 | return request.post('/client/save', data) 17 | } 18 | 19 | /** 20 | * 删除客户端 21 | * @param {Object} id 22 | */ 23 | export function deleteClient(id) { 24 | return request.delete('/client/delete/' + id) 25 | } -------------------------------------------------------------------------------- /src/api/code.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | 4 | /** 5 | * 发送验证码 6 | * @param {Object} params 7 | */ 8 | export function sendCode(params) { 9 | return request({ 10 | url: '/sms/send', 11 | method: 'post', 12 | params: params 13 | }) 14 | } -------------------------------------------------------------------------------- /src/api/collect.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | 4 | /** 5 | * @description 是否已收藏 6 | * @param {Object} articleId 7 | */ 8 | export function isCollected(articleId) { 9 | return request.get(`/article/collect/collected/${articleId}`) 10 | } 11 | 12 | /** 13 | * @description 新增收藏 14 | * @param {Object} params 15 | */ 16 | export function addCollect(params){ 17 | return request({ 18 | url: '/article/collect/add', 19 | method: 'post', 20 | params: params 21 | }) 22 | } 23 | 24 | /** 25 | * @description 取消收藏 26 | * @param {Object} params 27 | */ 28 | export function cancelCollected(params) { 29 | return request.delete('/article/collect/delete', { params }) 30 | } 31 | 32 | /** 33 | * @description 分页获取收藏 34 | * @param {Object} params 35 | */ 36 | export function pageCollect(params) { 37 | return request.get('/article/collect/page', { params }) 38 | } 39 | -------------------------------------------------------------------------------- /src/api/comment.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | /** 4 | * @param {Object} params 5 | * @description 分页获取评论回复列表 6 | */ 7 | export function pageComment(params) { 8 | return request.get('/article/comment/page', { params }) 9 | } 10 | 11 | /** 12 | * @description 新增评论 13 | * @param {Object} params 14 | */ 15 | export function addComment(params) { 16 | return request({ 17 | url: '/article/comment/add', 18 | method: 'post', 19 | params: params 20 | }) 21 | } 22 | 23 | /** 24 | * @description 新增回复 25 | * @param {Object} params 26 | */ 27 | export function addReply(params) { 28 | return request({ 29 | url: '/article/reply/add', 30 | method: 'post', 31 | params: params 32 | }) 33 | } 34 | 35 | /** 36 | * @description 删除评论 37 | * @param {Object} params 38 | */ 39 | export function deleteComment(params) { 40 | return request.delete('/article/comment/delete', { params }) 41 | } 42 | 43 | /** 44 | * @description 删除回复 45 | * @param {Object} params 46 | */ 47 | export function deleteReply(params) { 48 | return request.delete('/article/reply/delete', { params }) 49 | } 50 | 51 | /** 52 | * @description 最新评论 53 | * @param {Object} params 54 | */ 55 | export function latestCommentList(params) { 56 | return request.get('/article/comment/latest', { params }) 57 | } 58 | -------------------------------------------------------------------------------- /src/api/file.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | /** 4 | * @param {Object} params 5 | * @description 删除文件 6 | */ 7 | export function deleteFile(params) { 8 | return request.delete('/file/delete', { params }) 9 | } 10 | 11 | /** 12 | * 分页获取文件 13 | * @param {Object} params 14 | */ 15 | export function pageFile(params) { 16 | return request.get('/file/page', { params }) 17 | } -------------------------------------------------------------------------------- /src/api/friend-link.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | /** 4 | * 保存友链 5 | * @data {Object} data 6 | */ 7 | export function saveFriendLink(data) { 8 | return request.post('/friend/link/save', data) 9 | } 10 | 11 | /** 12 | * 分页获取友链 13 | * @param {Object} params 14 | */ 15 | export function pageFriendLink(params) { 16 | return request.get('/friend/link/page', { params }) 17 | } 18 | 19 | /** 20 | * 友链列表 21 | */ 22 | export function listFriendLink() { 23 | return request.get('/friend/link/list') 24 | } 25 | 26 | /** 27 | * 删除友链 28 | * @param {Number} id 29 | */ 30 | export function deleteFriendLink(id) { 31 | return request.delete(`/friend/link/delete/${id}`) 32 | } 33 | -------------------------------------------------------------------------------- /src/api/like.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | 4 | /** 5 | * @description 是否已点赞 6 | * @param {Object} articleId 7 | */ 8 | export function isLiked(articleId) { 9 | return request.get(`/article/like/liked/${articleId}`) 10 | } 11 | 12 | /** 13 | * @description 新增点赞 14 | * @param {Object} params 15 | */ 16 | export function addLike(params){ 17 | return request({ 18 | url: '/article/like/add', 19 | method: 'post', 20 | params: params 21 | }) 22 | } 23 | 24 | /** 25 | * @description 取消点赞 26 | * @param {Object} params 27 | */ 28 | export function cancelLiked(params) { 29 | return request.delete('/article/like/cancel', { params }) 30 | } 31 | 32 | -------------------------------------------------------------------------------- /src/api/message.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | /** 4 | * @param {Object} params 5 | * @description 分页获取留言 6 | */ 7 | export function pageMessage(params) { 8 | return request.get('/leave/message/page', { params }) 9 | } 10 | 11 | /** 12 | * @description 新增留言 13 | * @param {Object} params 14 | */ 15 | export function addMessage(params) { 16 | return request({ 17 | url: '/leave/message/add', 18 | method: 'post', 19 | params: params 20 | }) 21 | } 22 | 23 | /** 24 | * @description 新增回复 25 | * @param {Object} params 26 | */ 27 | export function addReply(params) { 28 | return request({ 29 | url: '/leave/message/reply', 30 | method: 'post', 31 | params: params 32 | }) 33 | } 34 | 35 | /** 36 | * @description 删除留言或回复 37 | * @param {Object} id 38 | */ 39 | export function deleteO(id) { 40 | return request.delete('/leave/message/delete/' + id) 41 | } 42 | -------------------------------------------------------------------------------- /src/api/tag.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | /** 4 | * 新增标签 5 | * @params {Object} params 6 | */ 7 | export function addTag(params) { 8 | return request({ 9 | url: '/tag/add', 10 | method: 'post', 11 | params: params 12 | }) 13 | } 14 | 15 | /** 16 | * 分页获取标签列表 17 | * @param {Object} params 18 | */ 19 | export function pageTag(params) { 20 | return request.get('/tag/page', { params }) 21 | } 22 | 23 | /** 24 | * 获取标签列表 25 | */ 26 | export function tagList() { 27 | return request.get('/tag/list') 28 | } 29 | 30 | /** 31 | * 修改标签 32 | * @param {Object} params 33 | */ 34 | export function updateTag(params) { 35 | return request({ 36 | url: '/tag/update', 37 | method: 'post', 38 | params: params 39 | }) 40 | } 41 | 42 | /** 43 | * 删除标签 44 | * @param {Object} id 45 | */ 46 | export function deleteTag(id) { 47 | return request.delete('/tag/delete/' + id) 48 | } 49 | -------------------------------------------------------------------------------- /src/api/user.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | /** 4 | * 账号登录 5 | * @param {Object} params 6 | */ 7 | export function accountLogin(params) { 8 | return request({ 9 | url: '/account/login', 10 | headers: { 11 | 'Authorization': 'Basic cGM6MTIzNDU2' 12 | }, 13 | method: 'post', 14 | params: params 15 | }) 16 | } 17 | 18 | /** 19 | * 第三方登录 20 | * @param {Object} params 21 | */ 22 | export function thirdLogin(params) { 23 | return request({ 24 | url: '/oauth', 25 | headers: { 26 | 'Authorization': 'Basic cGM6MTIzNDU2' 27 | }, 28 | method: 'post', 29 | params: params 30 | }) 31 | } 32 | 33 | /** 34 | * 手机号验证码登录 35 | * @param {Object} params 36 | */ 37 | export function codeLogin(params) { 38 | return request({ 39 | url: '/mobile/login', 40 | headers: { 41 | 'Authorization': 'Basic cGM6MTIzNDU2' 42 | }, 43 | method: 'post', 44 | params: params 45 | }) 46 | } 47 | 48 | /** 49 | * @description 获取用户信息 50 | * @param {String} token 51 | */ 52 | export function getUserInfo(token) { 53 | return request({ 54 | url: '/user/info', 55 | method: 'get' 56 | }) 57 | } 58 | 59 | /** 60 | * @description 退出 61 | * @param {Object} params 62 | */ 63 | export function logout(params) { 64 | return request({ 65 | url: '/logout', 66 | headers: { 67 | 'Authorization': 'Basic cGM6MTIzNDU2' 68 | }, 69 | method: 'delete', 70 | params: params 71 | }) 72 | } 73 | 74 | /** 75 | * 用户注册 76 | * @param {Object} data 77 | */ 78 | export function register(data) { 79 | return request.post('/user/register', data) 80 | } 81 | 82 | /** 83 | * 更新用户(非空项更新) 84 | * @param {Object} data 85 | */ 86 | export function updateUser(data) { 87 | return request.post('/user/update', data) 88 | } 89 | 90 | /** 91 | * 更新密码 92 | * @param {Object} params 93 | */ 94 | export function updatePassword(params) { 95 | return request({ 96 | url: '/user/password/update', 97 | method: 'post', 98 | params: params 99 | }) 100 | } 101 | 102 | /** 103 | * 重置密码 104 | * @param {Object} params 105 | */ 106 | export function resetPassword(params) { 107 | return request({ 108 | url: '/user/password/reset', 109 | method: 'post', 110 | params: params 111 | }) 112 | } 113 | 114 | /** 115 | * 验证邮箱(发送验证链接) 116 | * @param {Object} params 117 | */ 118 | export function validateEmail(params) { 119 | return request({ 120 | url: '/user/email/validate', 121 | method: 'post', 122 | params: params 123 | }) 124 | } 125 | 126 | /** 127 | * code绑定邮箱 128 | * @param {Object} params 129 | */ 130 | export function bindEmail(params) { 131 | return request({ 132 | url: '/user/email/bind', 133 | method: 'post', 134 | params: params 135 | }) 136 | } 137 | 138 | /** 139 | * 验证原手机号 140 | * @param {Object} params 141 | */ 142 | export function validateMobile(params) { 143 | return request({ 144 | url: '/user/mobile/validate', 145 | method: 'post', 146 | params: params 147 | }) 148 | } 149 | 150 | /** 151 | * 绑定新手机号 152 | * @param {Object} params 153 | */ 154 | export function rebindMobile(params) { 155 | return request({ 156 | url: '/user/mobile/rebind', 157 | method: 'post', 158 | params: params 159 | }) 160 | } 161 | 162 | /** 163 | * 分页获取用户 164 | * @param {Object} params 165 | */ 166 | export function pageUser(params) { 167 | return request.get('/user/page', { params }) 168 | } 169 | 170 | /** 171 | * 修改用户状态 172 | * @param {Object} params 173 | */ 174 | export function updateStatus(params) { 175 | return request({ 176 | url: '/user/status/update', 177 | method: 'post', 178 | params: params 179 | }) 180 | } 181 | 182 | /** 183 | * 绑定用户名 184 | * @param {Object} params 185 | */ 186 | export function bindUsername(params) { 187 | return request({ 188 | url: '/user/username/bind', 189 | method: 'post', 190 | params: params 191 | }) 192 | } 193 | 194 | /** 195 | * 绑定手机号 196 | * @param {Object} params 197 | */ 198 | export function bindMobile(params) { 199 | return request({ 200 | url: '/user/mobile/bind', 201 | method: 'post', 202 | params: params 203 | }) 204 | } 205 | -------------------------------------------------------------------------------- /src/assets/404_images/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/copoile/blog-web/caa3b904cee9a316136b6008f367b1b9b85e413f/src/assets/404_images/404.png -------------------------------------------------------------------------------- /src/assets/404_images/404_cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/copoile/blog-web/caa3b904cee9a316136b6008f367b1b9b85e413f/src/assets/404_images/404_cloud.png -------------------------------------------------------------------------------- /src/assets/audio/index.css: -------------------------------------------------------------------------------- 1 | 2 | .audio-wrapper { 3 | background-color: #fcfcfc; 4 | margin: 10px auto; 5 | max-width: 400px; 6 | height: 40px; 7 | border: 1px solid #f0f0f0; 8 | color: #3e3e3e; 9 | display: flex; 10 | align-items: center; 11 | border-radius: 2px; 12 | } 13 | 14 | .audio-left { 15 | float: left; 16 | text-align: center; 17 | width: 50px; 18 | height: 100%; 19 | margin-right: 5px; 20 | position: relative; 21 | } 22 | 23 | .audio-left img { 24 | display: block; 25 | width: 22px; 26 | position: absolute; 27 | top: 50%; 28 | left: 50%; 29 | transform: translate(-50%, -50%); 30 | display: initial; /* 解除与app的样式冲突 */ 31 | cursor: pointer; 32 | background-color: inherit; 33 | } 34 | 35 | .audio-right { 36 | margin-right: 2%; 37 | flex: 1; 38 | height: 100%; 39 | } 40 | 41 | .audio-right p { 42 | font-size: 12px; 43 | padding: 0; 44 | margin-bottom: 3px; 45 | 46 | /* 歌曲名称只显示在一行,超出部分显示为省略号 */ 47 | overflow: hidden; 48 | white-space: nowrap; 49 | text-overflow: ellipsis; 50 | max-width: 243px; /* 要适配小屏幕手机,所以最大宽度先设小一点,后面js根据屏幕大小重新设置 */ 51 | } 52 | 53 | .progress-bar-bg { 54 | background-color: #d9d9d9; 55 | position: relative; 56 | height: 1px; 57 | cursor: pointer; 58 | margin: 2px 0; 59 | } 60 | 61 | .progress-bar { 62 | background-color: #649fec; 63 | width: 0; 64 | height: 2px; 65 | opacity: .8; 66 | } 67 | 68 | .progress-bar-bg span { 69 | content: " "; 70 | width: 5px; 71 | height: 5px; 72 | border-radius: 50%; 73 | -moz-border-radius: 50%; 74 | -webkit-border-radius: 50%; 75 | background-color: #3e87e8; 76 | position: absolute; 77 | left: 0; 78 | top: 50%; 79 | margin-top: -2px; 80 | margin-left: -5px; 81 | cursor: pointer; 82 | padding: 1px; 83 | } 84 | -------------------------------------------------------------------------------- /src/assets/js/1e7b63404cd2fb8e6525b2fd4ee4d286.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/copoile/blog-web/caa3b904cee9a316136b6008f367b1b9b85e413f/src/assets/js/1e7b63404cd2fb8e6525b2fd4ee4d286.png -------------------------------------------------------------------------------- /src/assets/js/live2d-message.js: -------------------------------------------------------------------------------- 1 | function renderTip(template, context) { 2 | var tokenReg = /(\\)?\{([^\{\}\\]+)(\\)?\}/g 3 | return template.replace(tokenReg, function (word, slash1, token, slash2) { 4 | if (slash1 || slash2) { 5 | return word.replace('\\', '') 6 | } 7 | var variables = token.replace(/\s/g, '').split('.') 8 | var currentObject = context 9 | var i, length, variable 10 | for (i = 0, length = variables.length; i < length; ++i) { 11 | variable = variables[i] 12 | currentObject = currentObject[variable] 13 | if (currentObject === undefined || currentObject === null) return '' 14 | } 15 | return currentObject; 16 | }) 17 | } 18 | 19 | String.prototype.renderTip = function (context) { 20 | return renderTip(this, context) 21 | } 22 | 23 | 24 | function initTips() { 25 | $.ajax({ 26 | cache: true, 27 | url: 'live2d-message.json', 28 | dataType: "json", 29 | success: function (result) { 30 | $.each(result.mouseover, function (index, tips) { 31 | $(tips.selector).mouseover(function () { 32 | var text = tips.text 33 | if (Array.isArray(tips.text)) text = tips.text[Math.floor(Math.random() * tips.text.length + 1) - 1] 34 | text = text.renderTip({ text: $(this).text() }) 35 | showMessage(text, 5000) 36 | }) 37 | }) 38 | $.each(result.click, function (index, tips) { 39 | $(tips.selector).click(function () { 40 | var text = tips.text 41 | if (Array.isArray(tips.text)) text = tips.text[Math.floor(Math.random() * tips.text.length + 1) - 1] 42 | text = text.renderTip({ text: $(this).text() }) 43 | showMessage(text, 5000) 44 | }) 45 | }) 46 | } 47 | }) 48 | } 49 | 50 | setTimeout(function () { 51 | var text = '欢迎使用个人悦读分享!' 52 | showMessage(text, 7000) 53 | }, 1000) 54 | 55 | initTips() 56 | 57 | // 时间消息提醒 58 | function showDateTip() { 59 | var text; 60 | var now = (new Date()).getHours() 61 | if (now > 23 || now <= 5) { 62 | text = '你是夜猫子呀?这么晚还不睡觉,明天起的来嘛?' 63 | } else if (now > 5 && now <= 7) { 64 | text = '早上好!一日之计在于晨,美好的一天就要开始了!' 65 | } else if (now > 7 && now <= 11) { 66 | text = '上午好!工作顺利嘛,不要久坐,多起来走动走动哦!' 67 | } else if (now > 11 && now <= 14) { 68 | text = '中午了,工作了一个上午,现在是午餐时间!' 69 | } else if (now > 14 && now <= 17) { 70 | text = '午后很容易犯困呢,今天的运动目标完成了吗?' 71 | } else if (now > 17 && now <= 19) { 72 | text = '傍晚了!窗外夕阳的景色很美丽呢,最美不过夕阳红~~' 73 | } else if (now > 19 && now <= 21) { 74 | text = '晚上好,今天过得怎么样?' 75 | } else if (now > 21 && now <= 23) { 76 | text = '已经这么晚了呀,早点休息吧,晚安~~' 77 | } else { 78 | text = '嗨~ 快来逗我玩吧!' 79 | } 80 | showMessage(text, 5000) 81 | } 82 | // 每两小时 83 | window.setInterval(showDateTip, 7200000) 84 | // 每20秒 85 | window.setInterval(showHitokoto, 20000) 86 | 87 | function showHitokoto() { 88 | $.getJSON('https://v1.hitokoto.cn/', function (result) { 89 | showMessage(result.hitokoto, 5000) 90 | }); 91 | } 92 | // 显示消息 93 | function showMessage(text, timeout) { 94 | if (Array.isArray(text)) { 95 | text = text[Math.floor(Math.random() * text.length + 1) - 1] 96 | } 97 | if (text.length <= 45) { 98 | var msg = $('.live2d-msg') 99 | var messageBox = $('.message-box') 100 | messageBox.stop(); 101 | msg.html(text).fadeTo(200, 1) 102 | if (timeout === null) timeout = 5000 103 | hideMessage(timeout) 104 | } 105 | } 106 | // 隐藏消息 107 | function hideMessage(timeout) { 108 | var messageBox = $('.message-box') 109 | messageBox.stop().css('opacity', 1) 110 | if (timeout === null) timeout = 5000 111 | messageBox.delay(timeout).fadeTo(200, 0) 112 | } 113 | -------------------------------------------------------------------------------- /src/components/Breadcrumb/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 54 | 55 | 68 | -------------------------------------------------------------------------------- /src/components/DynamicInput/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 30 | 31 | 65 | 66 | 78 | -------------------------------------------------------------------------------- /src/components/Hamburger/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 16 | 17 | 33 | 34 | 46 | -------------------------------------------------------------------------------- /src/components/Live2d/dynamicLoadScript.js: -------------------------------------------------------------------------------- 1 | let callbacks = [] 2 | 3 | const dynamicLoadScript = (src, callback) => { 4 | const existingScript = document.getElementById(src) 5 | const cb = callback || function() {} 6 | 7 | if (!existingScript) { 8 | const script = document.createElement('script') 9 | script.src = src // src url for the third-party library being loaded. 10 | script.id = src 11 | document.body.appendChild(script) 12 | callbacks.push(cb) 13 | const onEnd = 'onload' in script ? stdOnEnd : ieOnEnd 14 | onEnd(script) 15 | } 16 | 17 | if (existingScript && cb) { 18 | callbacks.push(cb) 19 | } 20 | 21 | function stdOnEnd(script) { 22 | script.onload = function() { 23 | // this.onload = null here is necessary 24 | // because even IE9 works not like others 25 | this.onerror = this.onload = null 26 | for (const cb of callbacks) { 27 | cb(null, script) 28 | } 29 | callbacks = null 30 | } 31 | script.onerror = function() { 32 | this.onerror = this.onload = null 33 | cb(new Error('Failed to load ' + src), script) 34 | } 35 | } 36 | 37 | function ieOnEnd(script) { 38 | script.onreadystatechange = function() { 39 | if (this.readyState !== 'complete' && this.readyState !== 'loaded') return 40 | this.onreadystatechange = null 41 | for (const cb of callbacks) { 42 | cb(null, script) // there is no way to catch loading errors in IE8 43 | } 44 | callbacks = null 45 | } 46 | } 47 | } 48 | 49 | export default dynamicLoadScript 50 | -------------------------------------------------------------------------------- /src/components/Live2d/index.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 65 | 66 | 123 | -------------------------------------------------------------------------------- /src/components/SvgIcon/index.vue: -------------------------------------------------------------------------------- 1 | 8 | 14 | 15 | 54 | 55 | 70 | -------------------------------------------------------------------------------- /src/components/mixin/ResizeHandler.js: -------------------------------------------------------------------------------- 1 | import store from '@/store' 2 | 3 | const { body } = document 4 | const WIDTH = 992 5 | 6 | export default { 7 | beforeMount() { 8 | window.addEventListener('resize', this.$_resizeHandler) 9 | }, 10 | beforeDestroy() { 11 | window.removeEventListener('resize', this.$_resizeHandler) 12 | }, 13 | mounted() { 14 | const isMobile = this.$_isMobile() 15 | if (isMobile) { 16 | store.dispatch('app/toggleDevice', 'mobile') 17 | } 18 | }, 19 | methods: { 20 | $_isMobile() { 21 | const rect = body.getBoundingClientRect() 22 | return rect.width - 1 < WIDTH 23 | }, 24 | $_resizeHandler() { 25 | if (!document.hidden) { 26 | const isMobile = this.$_isMobile() 27 | store.dispatch('app/toggleDevice', isMobile ? 'mobile' : 'desktop') 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/config/editor.js: -------------------------------------------------------------------------------- 1 | import Quill from 'quill' 2 | 3 | // 编辑器图片处理插件配置, 如果不配置图片在编辑器改动的样式将不会保存到htmlContent中 4 | var BaseImageFormat = Quill.import('formats/image'); 5 | const ImageFormatAttributesList = [ 6 | 'alt', 7 | 'height', 8 | 'width', 9 | 'style', 10 | 'class', 11 | '' 12 | ]; 13 | 14 | export class ImageFormat extends BaseImageFormat { 15 | static formats(domNode) { 16 | return ImageFormatAttributesList.reduce(function(formats, attribute) { 17 | if (domNode.hasAttribute(attribute)) { 18 | formats[attribute] = domNode.getAttribute(attribute); 19 | } 20 | return formats; 21 | }, {}); 22 | } 23 | format(name, value) { 24 | if (ImageFormatAttributesList.indexOf(name) > -1) { 25 | if (value) { 26 | this.domNode.setAttribute(name, value); 27 | } else { 28 | this.domNode.removeAttribute(name); 29 | } 30 | } else { 31 | super.format(name, value); 32 | } 33 | } 34 | } 35 | 36 | 37 | // 富文本编辑工具栏 38 | export const toolbarOptions = [ 39 | ['bold', 'italic', 'underline', 'strike'], 40 | ['emoji'], 41 | ['blockquote', 'code-block'], 42 | [{ 43 | header: 1 44 | }, { 45 | header: 2 46 | }], 47 | [{ 48 | list: 'ordered' 49 | }, { 50 | list: 'bullet' 51 | }], 52 | [{ 53 | script: 'sub' 54 | }, { 55 | script: 'super' 56 | }], 57 | [{ 58 | indent: '-1' 59 | }, { 60 | indent: '+1' 61 | }], 62 | [{ 63 | direction: 'rtl' 64 | }], 65 | [{ 66 | size: ['small', false, 'large', 'huge'] 67 | }], 68 | [{ 69 | header: [1, 2, 3, 4, 5, 6, false] 70 | }], 71 | [{ 72 | color: [] 73 | }, { 74 | background: [] 75 | }], 76 | [{ 77 | align: [] 78 | }], 79 | ['link', 'image'], 80 | ['clean'], 81 | ['preview'] 82 | ] 83 | -------------------------------------------------------------------------------- /src/config/sections.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 栏目 3 | */ 4 | const sections = [ 5 | { 6 | chsname: '首页', 7 | enname: 'HOME', 8 | path: '/' 9 | }, 10 | { 11 | chsname: '分类', 12 | enname: 'CATEGORY', 13 | path: '/' 14 | }, 15 | { 16 | chsname: '标签', 17 | enname: 'TAG', 18 | path: '/' 19 | }, 20 | { 21 | chsname: '归档', 22 | enname: 'ARCHIVES', 23 | path: '/' 24 | }, 25 | { 26 | chsname: '友链', 27 | enname: 'FRIEND', 28 | path: '/' 29 | }, 30 | { 31 | chsname: '留言', 32 | enname: 'WORDS', 33 | path: '/' 34 | } 35 | ] 36 | 37 | export default sections 38 | -------------------------------------------------------------------------------- /src/element-ui.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import { 3 | Pagination, 4 | Dialog, 5 | Autocomplete, 6 | Dropdown, 7 | DropdownMenu, 8 | DropdownItem, 9 | Menu, 10 | Submenu, 11 | MenuItem, 12 | MenuItemGroup, 13 | Input, 14 | InputNumber, 15 | Radio, 16 | Checkbox, 17 | Switch, 18 | Select, 19 | Option, 20 | Button, 21 | Table, 22 | TableColumn, 23 | DatePicker, 24 | Popover, 25 | Breadcrumb, 26 | BreadcrumbItem, 27 | Form, 28 | FormItem, 29 | Tag, 30 | Icon, 31 | Upload, 32 | Spinner, 33 | Badge, 34 | Card, 35 | Steps, 36 | Step, 37 | Aside, 38 | Timeline, 39 | TimelineItem, 40 | Loading, 41 | MessageBox, 42 | Message, 43 | Notification, 44 | Drawer 45 | } from 'element-ui' 46 | 47 | Vue.use(Pagination) 48 | Vue.use(Dialog) 49 | Vue.use(Autocomplete) 50 | Vue.use(Dropdown) 51 | Vue.use(DropdownMenu) 52 | Vue.use(DropdownItem) 53 | Vue.use(Menu) 54 | Vue.use(Submenu) 55 | Vue.use(MenuItem) 56 | Vue.use(MenuItemGroup) 57 | Vue.use(Input) 58 | Vue.use(InputNumber) 59 | Vue.use(Radio) 60 | Vue.use(Checkbox) 61 | Vue.use(Switch) 62 | Vue.use(Select) 63 | Vue.use(Option) 64 | Vue.use(Button) 65 | Vue.use(Table) 66 | Vue.use(TableColumn) 67 | Vue.use(DatePicker) 68 | Vue.use(Popover) 69 | Vue.use(Breadcrumb) 70 | Vue.use(BreadcrumbItem) 71 | Vue.use(Form) 72 | Vue.use(FormItem) 73 | Vue.use(Tag) 74 | Vue.use(Icon) 75 | Vue.use(Upload) 76 | Vue.use(Spinner) 77 | Vue.use(Badge) 78 | Vue.use(Card) 79 | Vue.use(Steps) 80 | Vue.use(Step) 81 | Vue.use(Aside) 82 | Vue.use(Timeline) 83 | Vue.use(TimelineItem) 84 | Vue.use(Loading) 85 | Vue.use(Drawer) 86 | // 这里有个奇怪的bug,需要复值,要不然报is not defined 87 | const MessageBox1 = MessageBox 88 | 89 | Vue.prototype.$message = Message 90 | Vue.prototype.$confirm = MessageBox1.confirm 91 | Vue.prototype.$prompt = MessageBox1.prompt 92 | -------------------------------------------------------------------------------- /src/icons/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import SvgIcon from '@/components/SvgIcon'// svg component 3 | 4 | // register globally 5 | Vue.component('svg-icon', SvgIcon) 6 | 7 | const req = require.context('./svg', false, /\.svg$/) 8 | const requireAll = requireContext => requireContext.keys().map(requireContext) 9 | requireAll(req) 10 | -------------------------------------------------------------------------------- /src/icons/svg/archives-menu.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/art-edit.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/art-list.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/art-manage.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/category-manage.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/collect-manage.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/collect.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/svg/copy.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/dashboard.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/example.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/eye-open.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/eye.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/file-manage.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/friend-chain-manage.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/gitee-login.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/github-login.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/like.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/svg/line.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/link.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/menu.svg: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/icons/svg/nested.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/password.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/qq-login.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/qq.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/svg/recommend-manage.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/svg/table.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/tag-manage.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/tag.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/tree.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/user-manage.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/user.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/wx.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/svgo.yml: -------------------------------------------------------------------------------- 1 | # replace default config 2 | 3 | # multipass: true 4 | # full: true 5 | 6 | plugins: 7 | 8 | # - name 9 | # 10 | # or: 11 | # - name: false 12 | # - name: true 13 | # 14 | # or: 15 | # - name: 16 | # param1: 1 17 | # param2: 2 18 | 19 | - removeAttrs: 20 | attrs: 21 | - 'fill' 22 | - 'fill-rule' 23 | -------------------------------------------------------------------------------- /src/layout/components/AppMain.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 19 | 20 | 32 | 33 | 41 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/FixiOSBug.js: -------------------------------------------------------------------------------- 1 | export default { 2 | computed: { 3 | device() { 4 | return this.$store.state.app.device 5 | } 6 | }, 7 | mounted() { 8 | this.fixBugIniOS() 9 | }, 10 | methods: { 11 | fixBugIniOS() { 12 | const $subMenu = this.$refs.subMenu 13 | if ($subMenu) { 14 | const handleMouseleave = $subMenu.handleMouseleave 15 | $subMenu.handleMouseleave = (e) => { 16 | if (this.device === 'mobile') { 17 | return 18 | } 19 | handleMouseleave(e) 20 | } 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/Item.vue: -------------------------------------------------------------------------------- 1 | 30 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/Link.vue: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 36 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/Logo.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 33 | 34 | 84 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/SidebarItem.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 90 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/index.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 58 | -------------------------------------------------------------------------------- /src/layout/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as Navbar } from './Navbar' 2 | export { default as Sidebar } from './Sidebar' 3 | export { default as AppMain } from './AppMain' 4 | -------------------------------------------------------------------------------- /src/layout/index.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 52 | 53 | 95 | -------------------------------------------------------------------------------- /src/layout/mixin/ResizeHandler.js: -------------------------------------------------------------------------------- 1 | import store from '@/store' 2 | 3 | const { body } = document 4 | const WIDTH = 992 // refer to Bootstrap's responsive design 5 | 6 | export default { 7 | watch: { 8 | $route(route) { 9 | if (this.device === 'mobile' && this.sidebar.opened) { 10 | store.dispatch('app/closeSideBar', { withoutAnimation: false }) 11 | } 12 | } 13 | }, 14 | beforeMount() { 15 | window.addEventListener('resize', this.$_resizeHandler) 16 | }, 17 | beforeDestroy() { 18 | window.removeEventListener('resize', this.$_resizeHandler) 19 | }, 20 | mounted() { 21 | const isMobile = this.$_isMobile() 22 | if (isMobile) { 23 | store.dispatch('app/toggleDevice', 'mobile') 24 | store.dispatch('app/closeSideBar', { withoutAnimation: true }) 25 | } 26 | }, 27 | methods: { 28 | // use $_ for mixins properties 29 | // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential 30 | $_isMobile() { 31 | const rect = body.getBoundingClientRect() 32 | return rect.width - 1 < WIDTH 33 | }, 34 | $_resizeHandler() { 35 | if (!document.hidden) { 36 | const isMobile = this.$_isMobile() 37 | store.dispatch('app/toggleDevice', isMobile ? 'mobile' : 'desktop') 38 | 39 | if (isMobile) { 40 | store.dispatch('app/closeSideBar', { withoutAnimation: true }) 41 | } 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | // 消除各浏览器差异 4 | import 'normalize.css/normalize.css' 5 | 6 | // ElementUI 按需加载 7 | import './element-ui.js' 8 | 9 | // 富文本编辑器 10 | import VueQuillEditor from 'vue-quill-editor' 11 | import 'quill/dist/quill.core.css' 12 | import 'quill/dist/quill.snow.css' 13 | 14 | // 点击复制 15 | import VueClipboard from 'vue-clipboard2' 16 | 17 | // 全局样式 18 | import '@/styles/index.scss' 19 | 20 | import App from './App' 21 | 22 | // vuex 23 | import store from './store' 24 | 25 | // 路由 26 | import router from './router' 27 | 28 | // 图标 29 | import '@/icons' 30 | 31 | // 权限控制 32 | import '@/permission' 33 | 34 | Vue.use(VueQuillEditor) 35 | Vue.use(VueClipboard) 36 | 37 | Vue.config.productionTip = false 38 | 39 | new Vue({ 40 | el: '#app', 41 | router, 42 | store, 43 | render: h => h(App) 44 | }) 45 | -------------------------------------------------------------------------------- /src/permission.js: -------------------------------------------------------------------------------- 1 | import router from './router' 2 | import store from './store' 3 | import { Message } from 'element-ui' 4 | import NProgress from 'nprogress' 5 | import 'nprogress/nprogress.css' 6 | import { getAccessToken } from '@/utils/auth' 7 | import getPageTitle from '@/utils/get-page-title' 8 | import pathToRegexp from 'path-to-regexp' 9 | import { bindEmail } from '@/api/user.js' 10 | 11 | NProgress.configure({ showSpinner: false }) 12 | // 免登录白名单 13 | const whiteList = ['/', '/tag', '/category', '/archives', 14 | '/message', '/friend-link', '/article/:id', '/reset-password', '/about', '/search', '/terms', '/privacy', '/email-bind', '/404', '/oauth'] 15 | 16 | router.beforeEach(async(to, from, next) => { 17 | 18 | // 百度统计 19 | if (window.location.hostname === 'www.poile.cn' && to.path) { 20 | if (window._hmt) { 21 | window._hmt.push(['_trackPageview', '/#' + to.fullPath]); 22 | } else { 23 | var _hmt = []; 24 | window._hmt = _hmt; 25 | (function () { 26 | let hm = document.createElement('script'); 27 | hm.src = 'https://hm.baidu.com/hm.js?0365897af075de8b1b3ba64f3cc7b423'; 28 | let s = document.getElementsByTagName('script')[0]; 29 | s.parentNode.insertBefore(hm, s); 30 | })(); 31 | } 32 | } 33 | 34 | // 进度条开始 35 | NProgress.start() 36 | 37 | // 获取AccessToken,判断是否已登录 38 | const hasAccessToken = getAccessToken() 39 | 40 | // 是否邮箱绑定路由, 邮箱绑定成功如果有token则刷新用户信息 41 | // 并且跳转用户信息页,没token跳回首页,绑定失败或没获取到code跳404 42 | if (pathToRegexp('/email-bind').exec(to.path)) { 43 | const code = to.query.code 44 | if (code) { 45 | const params = { code: code } 46 | let result = true 47 | await bindEmail(params).then( 48 | res => { 49 | result = true 50 | }, 51 | error => { 52 | result = false 53 | } 54 | ) 55 | if (!result) { 56 | next('/404') 57 | NProgress.done() 58 | } 59 | if (result && hasAccessToken) { 60 | // 获取用户线信息,从用户信息中获取角色 61 | const { roles } = await store.dispatch('user/getUserInfo') 62 | // 根据角色动态加载路由 63 | const accessRoutes = await store.dispatch('permission/generateRoutes', roles) 64 | // 添加动态路由 65 | router.addRoutes(accessRoutes) 66 | next('/user/info') 67 | NProgress.done() 68 | } 69 | if (result && !hasAccessToken) { 70 | next('/') 71 | NProgress.done() 72 | } 73 | } 74 | } 75 | 76 | // 页面标题 77 | document.title = getPageTitle(to.meta.title) 78 | 79 | if (hasAccessToken) { 80 | // 获取角色,判断是否已调获取用户信息接口 81 | const hasRoles = store.getters.roles && store.getters.roles.length > 0 82 | if (hasRoles) { 83 | next() 84 | } else { 85 | try { 86 | // 获取用户线信息,从用户信息中获取角色 87 | const { roles } = await store.dispatch('user/getUserInfo') 88 | 89 | // 根据角色动态加载路由 90 | const accessRoutes = await store.dispatch('permission/generateRoutes', roles) 91 | 92 | // 添加动态路由 93 | router.addRoutes(accessRoutes) 94 | 95 | next({ ...to, replace: true }) 96 | } catch (error) { 97 | // 获取用户信息失败重置登录信息 98 | await store.dispatch('user/resetToken') 99 | Message.error(error || 'Has Error') 100 | // 跳回首页 101 | next('/') 102 | NProgress.done() 103 | } 104 | } 105 | } else { 106 | // 未登录,并且路径存在免登录白名单中 107 | const included = whiteList.some( ele => pathToRegexp(ele).exec(to.path) ) 108 | if (included) { 109 | next() 110 | } else { 111 | const path = to.matched.length === 0 ? '/404' : '/' 112 | next(path) 113 | NProgress.done() 114 | } 115 | } 116 | }) 117 | 118 | router.afterEach(() => { 119 | // 进度条结束 120 | NProgress.done() 121 | }) 122 | -------------------------------------------------------------------------------- /src/settings.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | title: '个人阅读分享', 4 | 5 | /** 6 | * @type {boolean} true | false 7 | * @description 后台管理 是否固定头部 8 | */ 9 | fixedHeader: true, 10 | 11 | /** 12 | * @type {boolean} true | false 13 | * @description 后台管理 是否显示logo 14 | */ 15 | sidebarLogo: true, 16 | 17 | /** 18 | * @description 用户未上传头像时使用的默认头像 19 | */ 20 | defaultAvatar: 'https://poile-img.nos-eastchina1.126.net/without-avatar.png' 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/store/getters.js: -------------------------------------------------------------------------------- 1 | const getters = { 2 | sidebar: state => state.app.sidebar, 3 | device: state => state.app.device, 4 | defaultAvatar: state => state.settings.defaultAvatar, 5 | token: state => state.user.token, 6 | userInfo: state => state.user.userInfo, 7 | roles: state => state.user.roles, 8 | permission_routes: state => state.permission.routes, 9 | login_visible: state => state.login.visible, 10 | login_username: state => state.login.username, 11 | login_password: state => state.login.password 12 | } 13 | export default getters 14 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import getters from './getters' 4 | import app from './modules/app' 5 | import permission from './modules/permission' 6 | import settings from './modules/settings' 7 | import user from './modules/user' 8 | import login from './modules/login' 9 | 10 | Vue.use(Vuex) 11 | 12 | const store = new Vuex.Store({ 13 | modules: { 14 | app, 15 | permission, 16 | settings, 17 | user, 18 | login 19 | }, 20 | getters 21 | }) 22 | 23 | export default store 24 | -------------------------------------------------------------------------------- /src/store/modules/app.js: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie' 2 | 3 | const state = { 4 | sidebar: { 5 | opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true, 6 | withoutAnimation: false 7 | }, 8 | device: 'desktop' 9 | } 10 | 11 | const mutations = { 12 | TOGGLE_SIDEBAR: state => { 13 | state.sidebar.opened = !state.sidebar.opened 14 | state.sidebar.withoutAnimation = false 15 | if (state.sidebar.opened) { 16 | Cookies.set('sidebarStatus', 1) 17 | } else { 18 | Cookies.set('sidebarStatus', 0) 19 | } 20 | }, 21 | CLOSE_SIDEBAR: (state, withoutAnimation) => { 22 | Cookies.set('sidebarStatus', 0) 23 | state.sidebar.opened = false 24 | state.sidebar.withoutAnimation = withoutAnimation 25 | }, 26 | TOGGLE_DEVICE: (state, device) => { 27 | state.device = device 28 | } 29 | } 30 | 31 | const actions = { 32 | // 切换 33 | toggleSideBar({ commit }) { 34 | commit('TOGGLE_SIDEBAR') 35 | }, 36 | // 关闭 37 | closeSideBar({ commit }, { withoutAnimation }) { 38 | commit('CLOSE_SIDEBAR', withoutAnimation) 39 | }, 40 | // 切换设备 41 | toggleDevice({ commit }, device) { 42 | commit('TOGGLE_DEVICE', device) 43 | } 44 | } 45 | 46 | export default { 47 | namespaced: true, 48 | state, 49 | mutations, 50 | actions 51 | } 52 | -------------------------------------------------------------------------------- /src/store/modules/login.js: -------------------------------------------------------------------------------- 1 | import { 2 | getUsername, 3 | setUsername, 4 | removeUsername, 5 | setPassword, 6 | getPassword, 7 | removePassword 8 | } from '@/utils/auth' 9 | 10 | const state = { 11 | visible: false, 12 | username: getUsername(), 13 | password: getPassword() 14 | } 15 | 16 | const mutations = { 17 | CHANGE_VISIBLE: (state, value) => { 18 | state.visible = value 19 | }, 20 | 21 | SET_USERNAME: (state, value) => { 22 | state.username = value 23 | }, 24 | 25 | SET_PASSWORD: (state, value) => { 26 | state.password = value 27 | } 28 | } 29 | 30 | const actions = { 31 | 32 | /** 33 | * 记住用户名和密码 34 | */ 35 | setUsernameAndPassword({ commit }, params) { 36 | return new Promise((resolve, reject) => { 37 | const username = params.username 38 | const password = params.password 39 | commit('SET_USERNAME', username) 40 | commit('SET_PASSWORD', password) 41 | setUsername(username) 42 | setPassword(password) 43 | resolve() 44 | }).catch(error => { 45 | reject(error) 46 | }) 47 | }, 48 | 49 | /** 50 | * 清除用户和密码 51 | */ 52 | clearUsernameAndPassword({ commit }, params) { 53 | return new Promise((resolve, reject) => { 54 | commit('SET_USERNAME', '') 55 | commit('SET_PASSWORD', '') 56 | removeUsername() 57 | removePassword() 58 | resolve() 59 | }).catch(error => { 60 | reject(error) 61 | }) 62 | } 63 | } 64 | 65 | export default { 66 | namespaced: true, 67 | state, 68 | mutations, 69 | actions 70 | } 71 | -------------------------------------------------------------------------------- /src/store/modules/permission.js: -------------------------------------------------------------------------------- 1 | import { asyncRoutes, constantRoutes } from '@/router' 2 | 3 | /** 4 | * 判断是否拥有权限 5 | * @param roles 6 | * @param route 7 | */ 8 | function hasPermission(roles, route) { 9 | if (route.meta && route.meta.roles) { 10 | return roles.some(role => route.meta.roles.includes(role)) 11 | } else { 12 | return true 13 | } 14 | } 15 | 16 | /** 17 | * 递归权限路由 18 | * @param routes asyncRoutes 19 | * @param roles 20 | */ 21 | export function filterAsyncRoutes(routes, roles) { 22 | const res = [] 23 | 24 | routes.forEach(route => { 25 | const tmp = { ...route } 26 | if (hasPermission(roles, tmp)) { 27 | if (tmp.children) { 28 | tmp.children = filterAsyncRoutes(tmp.children, roles) 29 | } 30 | res.push(tmp) 31 | } 32 | }) 33 | 34 | return res 35 | } 36 | 37 | const state = { 38 | routes: [], 39 | addRoutes: [] 40 | } 41 | 42 | const mutations = { 43 | SET_ROUTES: (state, routes) => { 44 | state.addRoutes = routes 45 | state.routes = constantRoutes.concat(routes) 46 | } 47 | } 48 | 49 | const actions = { 50 | // 根据角色动态加载路由 51 | generateRoutes({ commit }, roles) { 52 | return new Promise(resolve => { 53 | let accessedRoutes 54 | // 管理员全部路由 55 | if (roles.includes('admin')) { 56 | accessedRoutes = asyncRoutes || [] 57 | } else { 58 | // 非管理员根据角色获取对应路由 59 | accessedRoutes = filterAsyncRoutes(asyncRoutes, roles) 60 | } 61 | commit('SET_ROUTES', accessedRoutes) 62 | resolve(accessedRoutes) 63 | }) 64 | } 65 | } 66 | 67 | export default { 68 | namespaced: true, 69 | state, 70 | mutations, 71 | actions 72 | } 73 | -------------------------------------------------------------------------------- /src/store/modules/settings.js: -------------------------------------------------------------------------------- 1 | import defaultSettings from '@/settings' 2 | 3 | const { fixedHeader, sidebarLogo, defaultAvatar } = defaultSettings 4 | 5 | const state = { 6 | fixedHeader: fixedHeader, 7 | sidebarLogo: sidebarLogo, 8 | defaultAvatar: defaultAvatar 9 | } 10 | 11 | const mutations = { 12 | CHANGE_SETTING: (state, { key, value }) => { 13 | if (state.hasOwnProperty(key)) { 14 | state[key] = value 15 | } 16 | } 17 | } 18 | 19 | const actions = { 20 | changeSetting({ commit }, data) { 21 | commit('CHANGE_SETTING', data) 22 | } 23 | } 24 | 25 | export default { 26 | namespaced: true, 27 | state, 28 | mutations, 29 | actions 30 | } 31 | -------------------------------------------------------------------------------- /src/store/modules/user.js: -------------------------------------------------------------------------------- 1 | import { accountLogin, codeLogin, logout, getUserInfo } from '@/api/user' 2 | import { getAccessToken, setAccessToken, removeAccessToken } from '@/utils/auth' 3 | import { resetRouter } from '@/router' 4 | import { thirdLogin } from '@/api/user.js' 5 | const state = { 6 | token: getAccessToken(), 7 | nickname: '', 8 | avatar: '', 9 | roles: [], 10 | userInfo: '' 11 | } 12 | 13 | const mutations = { 14 | SET_TOKEN: (state, token) => { 15 | state.token = token 16 | }, 17 | SET_USER_INFO: (state, userInfo) => { 18 | state.userInfo = userInfo 19 | }, 20 | SET_ROLES: (state, roles) => { 21 | state.roles = roles 22 | } 23 | } 24 | 25 | const actions = { 26 | /** 27 | * 账号登录 28 | */ 29 | accountLogin({ commit }, params) { 30 | return new Promise((resolve, reject) => { 31 | accountLogin(params).then(response => { 32 | const { data } = response 33 | commit('SET_TOKEN', data.access_token) 34 | setAccessToken(data.access_token) 35 | resolve() 36 | }).catch(error => { 37 | reject(error) 38 | }) 39 | }) 40 | }, 41 | 42 | /** 43 | * 第三方登录 44 | */ 45 | thirdLogin({ commit }, params) { 46 | return new Promise((resolve, reject) => { 47 | thirdLogin(params).then(response => { 48 | const { data } = response 49 | commit('SET_TOKEN', data.access_token) 50 | setAccessToken(data.access_token) 51 | resolve() 52 | }).catch(error => { 53 | reject(error) 54 | }) 55 | }) 56 | }, 57 | 58 | /** 59 | * 账号登录 60 | */ 61 | codeLogin({ commit }, params) { 62 | return new Promise((resolve, reject) => { 63 | codeLogin(params).then(response => { 64 | const { data } = response 65 | commit('SET_TOKEN', data.access_token) 66 | setAccessToken(data.access_token) 67 | resolve() 68 | }).catch(error => { 69 | reject(error) 70 | }) 71 | }) 72 | }, 73 | 74 | /** 75 | * 获取用户信息 76 | */ 77 | getUserInfo({ commit, state }) { 78 | return new Promise((resolve, reject) => { 79 | getUserInfo().then(response => { 80 | const { data } = response 81 | 82 | if (!data) { 83 | reject('获取用户信息失败,请重新登录') 84 | } 85 | const { roles } = data 86 | // 角色列表校验 87 | if (!roles || roles.length <= 0) { 88 | reject('角色列表要求非null列表') 89 | } 90 | commit('SET_ROLES', roles) 91 | commit('SET_USER_INFO', data) 92 | resolve(data) 93 | }).catch(error => { 94 | reject(error) 95 | }) 96 | }) 97 | }, 98 | 99 | /** 100 | * 退出 101 | */ 102 | logout({ commit, state }) { 103 | return new Promise((resolve, reject) => { 104 | const access_token = state.token 105 | commit('SET_TOKEN', '') 106 | commit('SET_ROLES', []) 107 | commit('SET_USER_INFO', '') 108 | removeAccessToken() 109 | resetRouter() 110 | const params = { 111 | access_token: access_token 112 | } 113 | logout(params).then(res => { 114 | resolve() 115 | }).catch(error => { 116 | reject(error) 117 | }) 118 | }) 119 | }, 120 | 121 | /** 122 | * 重置 123 | */ 124 | resetToken({ commit }) { 125 | return new Promise(resolve => { 126 | commit('SET_TOKEN', '') 127 | commit('SET_ROLES', []) 128 | commit('SET_USER_INFO', '') 129 | removeAccessToken() 130 | resetRouter() 131 | resolve() 132 | }) 133 | } 134 | } 135 | 136 | export default { 137 | namespaced: true, 138 | state, 139 | mutations, 140 | actions 141 | } 142 | -------------------------------------------------------------------------------- /src/styles/element-ui.scss: -------------------------------------------------------------------------------- 1 | // 覆盖一些 element-ui 样式 2 | 3 | .el-breadcrumb__inner, 4 | .el-breadcrumb__inner a { 5 | font-weight: 400 !important; 6 | } 7 | 8 | .el-upload { 9 | input[type="file"] { 10 | display: none !important; 11 | } 12 | } 13 | 14 | .el-upload__input { 15 | display: none; 16 | } 17 | 18 | 19 | // to fixed https://github.com/ElemeFE/element/issues/2461 20 | .el-dialog { 21 | transform: none; 22 | left: 0; 23 | position: relative; 24 | margin: 0 auto; 25 | } 26 | 27 | // 手机华为自带浏览器bug 28 | .el-dialog__wrapper { 29 | overflow: hidden; 30 | } 31 | 32 | // refine element ui el-dialog__body 33 | .el-dialog__body { 34 | padding: 10px 25px 15px 25px; 35 | } 36 | 37 | // refine element ui el-dialog__title 38 | .el-dialog__title { 39 | font-weight: bold; 40 | } 41 | 42 | // refine element ui upload 43 | .upload-container { 44 | .el-upload { 45 | width: 100%; 46 | 47 | .el-upload-dragger { 48 | width: 100%; 49 | height: 200px; 50 | } 51 | } 52 | } 53 | 54 | // el-popover 55 | .el-popover { 56 | min-width: 100px; 57 | padding: 8px; 58 | font-size: 12px; 59 | } 60 | 61 | // dropdown 62 | .el-dropdown-menu { 63 | a { 64 | display: block 65 | } 66 | } 67 | 68 | // message-box 69 | .el-message-box { 70 | width: 420px; 71 | 72 | @media screen and (max-width: 922px){ 73 | width: 280px; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/styles/heilingt.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Gradient Dark (c) Samia Ali 4 | 5 | */ 6 | pre { 7 | background: #2d2d2d!important; 8 | /* background: linear-gradient(166deg, rgba(80,31,122,1) 0%, rgba(40,32,179,1) 80%); */ 9 | color:#e7e4eb!important; 10 | display: block!important; 11 | overflow-x: auto!important; 12 | /* white-space: pre-wrap!important; */ 13 | box-sizing: border-box!important; 14 | padding: 0!important; 15 | } 16 | 17 | .hljs 18 | { 19 | display: block!important; 20 | overflow-x: auto!important; 21 | padding: 16px!important; 22 | background: #2d2d2d!important; 23 | /* background: linear-gradient(166deg, rgba(80,31,122,1) 0%, rgba(40,32,179,1) 80%)!important; */ 24 | color:#e7e4eb!important; 25 | } 26 | 27 | .hljs-subtr{ 28 | color:#e7e4eb!important; 29 | } 30 | 31 | .markdown-body .highlight pre, .markdown-body pre 32 | 33 | .hljs-doctag, 34 | .hljs-meta, 35 | .hljs-comment, 36 | .hljs-quote 37 | { 38 | color:#af8dd9!important; 39 | } 40 | 41 | .hljs-selector-tag, 42 | .hljs-selector-id, 43 | .hljs-template-tag, 44 | .hljs-regexp, 45 | .hljs-attr, 46 | .hljs-tag 47 | { 48 | color:#AEFBFF!important; 49 | } 50 | 51 | .hljs-params, 52 | .hljs-selector-class, 53 | .hljs-bullet 54 | 55 | { 56 | color:#F19FFF!important; 57 | 58 | } 59 | 60 | .hljs-keyword, 61 | .hljs-section, 62 | .hljs-meta-keyword, 63 | .hljs-symbol, 64 | .hljs-type 65 | 66 | { 67 | 68 | color:#17fc95!important; 69 | } 70 | 71 | .hljs-addition, 72 | .hljs-number, 73 | .hljs-link 74 | { 75 | color:#C5FE00!important; 76 | } 77 | 78 | 79 | .hljs-string 80 | { 81 | color: #38c0ff!important; 82 | } 83 | 84 | 85 | .hljs-attribute, 86 | .hljs-addition 87 | { 88 | color:#E7FF9F!important; 89 | } 90 | 91 | .hljs-variable, 92 | .hljs-template-variable 93 | 94 | { 95 | color:#E447FF!important; 96 | } 97 | 98 | .hljs-builtin-name, 99 | .hljs-built_in, 100 | .hljs-formula, 101 | .hljs-name, 102 | .hljs-title, 103 | .hljs-class, 104 | .hljs-function 105 | { 106 | color: #FFC800!important; 107 | 108 | } 109 | 110 | .hljs-selector-pseudo, 111 | .hljs-deletion, 112 | .hljs-literal 113 | { 114 | color:#FF9E44!important; 115 | 116 | } 117 | 118 | .hljs-emphasis, 119 | .hljs-quote 120 | { 121 | font-style:italic!important; 122 | } 123 | 124 | .hljs-params, 125 | .hljs-selector-class, 126 | .hljs-strong, 127 | .hljs-selector-tag, 128 | .hljs-selector-id, 129 | .hljs-template-tag, 130 | .hljs-section, 131 | .hljs-keyword 132 | { 133 | font-weight:bold!important; 134 | } 135 | -------------------------------------------------------------------------------- /src/styles/index.scss: -------------------------------------------------------------------------------- 1 | @import './variables.scss'; 2 | @import './mixin.scss'; 3 | @import './transition.scss'; 4 | @import './element-ui.scss'; 5 | @import './sidebar.scss'; 6 | 7 | body { 8 | height: 100%; 9 | -moz-osx-font-smoothing: grayscale; 10 | -webkit-font-smoothing: antialiased; 11 | text-rendering: optimizeLegibility; 12 | font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif; 13 | } 14 | 15 | label { 16 | font-weight: 700; 17 | } 18 | 19 | html { 20 | height: 100%; 21 | box-sizing: border-box; 22 | overflow-y: overlay; 23 | } 24 | 25 | #app { 26 | background: #eee; 27 | } 28 | 29 | *, 30 | *:before, 31 | *:after { 32 | box-sizing: inherit; 33 | } 34 | 35 | a:focus, 36 | a:active { 37 | outline: none; 38 | } 39 | 40 | a, 41 | a:focus, 42 | a:hover { 43 | cursor: pointer; 44 | color: inherit; 45 | text-decoration: none; 46 | } 47 | 48 | div:focus { 49 | outline: none; 50 | } 51 | 52 | .clearfix { 53 | &:after { 54 | visibility: hidden; 55 | display: block; 56 | font-size: 0; 57 | content: " "; 58 | clear: both; 59 | height: 0; 60 | } 61 | } 62 | 63 | // main-container global css 64 | .app-container { 65 | padding: 20px; 66 | } 67 | 68 | // button 69 | .btn { 70 | cursor: pointer; 71 | } 72 | 73 | // 单行超出显示... 74 | .text-ellipsis { 75 | white-space: nowrap; 76 | overflow: hidden; 77 | text-overflow: ellipsis; 78 | } 79 | 80 | // 最多两行,超出显示... 81 | .multi-ellipsis--l2 { 82 | display: -webkit-box; 83 | -webkit-line-clamp: 2; 84 | -webkit-box-orient: vertical; 85 | text-overflow: ellipsis; 86 | overflow: hidden; 87 | } 88 | 89 | // 最多三行,超出显示... 90 | .multi-ellipsis--l3 { 91 | display: -webkit-box; 92 | -webkit-line-clamp: 3; 93 | -webkit-box-orient: vertical; 94 | text-overflow: ellipsis; 95 | overflow: hidden; 96 | } 97 | -------------------------------------------------------------------------------- /src/styles/mixin.scss: -------------------------------------------------------------------------------- 1 | @mixin clearfix { 2 | &:after { 3 | content: ""; 4 | display: table; 5 | clear: both; 6 | } 7 | } 8 | 9 | @mixin scrollBar { 10 | &::-webkit-scrollbar-track-piece { 11 | background: #d3dce6; 12 | } 13 | 14 | &::-webkit-scrollbar { 15 | width: 6px; 16 | } 17 | 18 | &::-webkit-scrollbar-thumb { 19 | background: #99a9bf; 20 | border-radius: 20px; 21 | } 22 | } 23 | 24 | @mixin relative { 25 | position: relative; 26 | width: 100%; 27 | height: 100%; 28 | } 29 | -------------------------------------------------------------------------------- /src/styles/transition.scss: -------------------------------------------------------------------------------- 1 | // 全局过度样式 2 | 3 | /* fade */ 4 | .fade-enter-active, 5 | .fade-leave-active { 6 | transition: opacity 0.28s; 7 | } 8 | 9 | .fade-enter, 10 | .fade-leave-active { 11 | opacity: 0; 12 | } 13 | 14 | /* fade-transform */ 15 | .fade-transform-leave-active, 16 | .fade-transform-enter-active { 17 | transition: all .5s; 18 | } 19 | 20 | .fade-transform-enter { 21 | opacity: 0; 22 | transform: translateX(-30px); 23 | } 24 | 25 | .fade-transform-leave-to { 26 | opacity: 0; 27 | transform: translateX(30px); 28 | } 29 | 30 | /* fade list transition */ 31 | .fade-list-enter-active { 32 | transition: opacity 0.4s; 33 | } 34 | 35 | .fade-list-leave-active { 36 | transition: opacity 0s; 37 | } 38 | 39 | .fade-list-enter, 40 | .fade-list-leave-active { 41 | opacity: 0; 42 | } 43 | 44 | /* breadcrumb transition */ 45 | .breadcrumb-enter-active, 46 | .breadcrumb-leave-active { 47 | transition: all .5s; 48 | } 49 | 50 | .breadcrumb-enter, 51 | .breadcrumb-leave-active { 52 | opacity: 0; 53 | transform: translateX(20px); 54 | } 55 | 56 | .breadcrumb-move { 57 | transition: all .5s; 58 | } 59 | 60 | .breadcrumb-leave-active { 61 | position: absolute; 62 | } 63 | -------------------------------------------------------------------------------- /src/styles/variables.scss: -------------------------------------------------------------------------------- 1 | // 导航栏样式变量 2 | 3 | // 菜单字体颜色 4 | $menuText:#bfcbd9; 5 | 6 | // 活动菜单字体颜色 7 | $menuActiveText:#409EFF; 8 | 9 | // 活动子菜单字体颜色 10 | $subMenuActiveText:#f4f4f5; 11 | 12 | // 菜单背景颜色 13 | $menuBg:#304156; 14 | 15 | // 菜单鼠标移入颜色 16 | $menuHover:#263445; 17 | 18 | // 子菜单背景颜色 19 | $subMenuBg:#1f2d3d; 20 | 21 | // 子菜单鼠标移入颜色 22 | $subMenuHover:#001528; 23 | 24 | // 宽度 25 | $sideBarWidth: 210px; 26 | 27 | // 内容容器宽度 28 | $ContentContainerW: 960px; 29 | 30 | // 内容右边side的宽度 31 | $ContentRightSideW: 240px; 32 | 33 | :export { 34 | menuText: $menuText; 35 | menuActiveText: $menuActiveText; 36 | subMenuActiveText: $subMenuActiveText; 37 | menuBg: $menuBg; 38 | menuHover: $menuHover; 39 | subMenuBg: $subMenuBg; 40 | subMenuHover: $subMenuHover; 41 | sideBarWidth: $sideBarWidth; 42 | ContentContainerW: $ContentContainerW; 43 | ContentRightSideW: $ContentRightSideW; 44 | } 45 | -------------------------------------------------------------------------------- /src/utils/auth.js: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie' 2 | import { Base64 } from 'js-base64' 3 | 4 | const AccessTokenKey = 'access_token' 5 | const UsernameKey = 'username' 6 | const PasswordKey = 'password' 7 | const RememberKey = 'remember' 8 | /** 9 | * 获取 accessToken 10 | */ 11 | export function getAccessToken() { 12 | return Cookies.get(AccessTokenKey) 13 | } 14 | 15 | /** 16 | * 设置 accessToken 17 | * @param {String} token 18 | */ 19 | export function setAccessToken(token) { 20 | return Cookies.set(AccessTokenKey, token, { expires: 15 }) 21 | } 22 | 23 | /** 24 | * 移除 accessToken 25 | */ 26 | export function removeAccessToken() { 27 | return Cookies.remove(AccessTokenKey) 28 | } 29 | 30 | /** 31 | * 设置用户名 32 | * @param {String} username 33 | */ 34 | export function setUsername(username) { 35 | return Cookies.set(UsernameKey, Base64.encode(username), { expires: 15 }) 36 | } 37 | 38 | /** 39 | * 获取用户名 40 | */ 41 | export function getUsername() { 42 | const v = Cookies.get(UsernameKey) 43 | return v ? Base64.decode(v) : v 44 | } 45 | 46 | /** 47 | * 移除 用户名 48 | */ 49 | export function removeUsername() { 50 | return Cookies.remove(UsernameKey) 51 | } 52 | 53 | /** 54 | * 设置密码 55 | * @param {String} password 56 | */ 57 | export function setPassword(password) { 58 | return Cookies.set(PasswordKey, Base64.encode(password), { expires: 15 }) 59 | } 60 | 61 | /** 62 | * 获取密码 63 | */ 64 | export function getPassword() { 65 | const v = Cookies.get(PasswordKey) 66 | return v ? Base64.decode(v) : v 67 | } 68 | 69 | /** 70 | * 移除 密码 71 | */ 72 | export function removePassword() { 73 | return Cookies.remove(PasswordKey) 74 | } 75 | 76 | /** 77 | * 设置记住密码 78 | */ 79 | export function setRemember(checked) { 80 | return Cookies.set(RememberKey, checked, { expires: 15 }) 81 | } 82 | 83 | /** 84 | * 获取记住密码 85 | */ 86 | export function getRemember() { 87 | return Cookies.get(RememberKey) 88 | } 89 | -------------------------------------------------------------------------------- /src/utils/get-page-title.js: -------------------------------------------------------------------------------- 1 | import defaultSettings from '@/settings' 2 | 3 | const title = defaultSettings.title || '个人悦读分享' 4 | 5 | export default function getPageTitle(pageTitle) { 6 | if (pageTitle) { 7 | return `${pageTitle} - ${title}` 8 | } 9 | return `${title}` 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/request.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { MessageBox, Message } from 'element-ui' 3 | import store from '@/store' 4 | import { getAccessToken } from '@/utils/auth' 5 | 6 | // 创建 axios 实例 7 | const service = axios.create({ 8 | baseURL: process.env.VUE_APP_BASE_API, 9 | // withCredentials: true, 10 | timeout: 10000 11 | }) 12 | 13 | // 请求拦截器 14 | service.interceptors.request.use( 15 | config => { 16 | // 添加请求头 17 | if (store.getters.token) { 18 | config.headers['Authorization'] = 'Bearer ' + getAccessToken() 19 | } 20 | return config 21 | }, 22 | error => { 23 | console.log(error) 24 | return Promise.reject(error) 25 | } 26 | ) 27 | 28 | // 响应拦截器 29 | service.interceptors.response.use( 30 | response => { 31 | const res = response.data 32 | 33 | if (res.code !== 0) { 34 | // 凭证无效或过期 35 | if (res.code === 1006 || res.code === 1009) { 36 | store.dispatch('user/resetToken') 37 | MessageBox.confirm('登录信息已过期', { 38 | confirmButtonText: '确定', 39 | cancelButtonText: '取消', 40 | type: 'warning' 41 | }).then(() => { 42 | location.reload() 43 | }).catch(() => { 44 | location.reload() 45 | }) 46 | } else { 47 | // 其他 48 | Message({ 49 | message: res.message || 'Error', 50 | type: 'error', 51 | duration: 5 * 1000 52 | }) 53 | } 54 | return Promise.reject(new Error(res.message || 'Error')) 55 | } else { 56 | return res 57 | } 58 | }, 59 | error => { 60 | console.log('err' + error) 61 | Message({ 62 | message: error.message, 63 | type: 'error', 64 | duration: 5 * 1000 65 | }) 66 | return Promise.reject(error) 67 | } 68 | ) 69 | 70 | export default service 71 | -------------------------------------------------------------------------------- /src/utils/validate.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {string} path 3 | * @returns {Boolean} 4 | */ 5 | export function isExternal(path) { 6 | return /^(https?:|mailto:|tel:)/.test(path) 7 | } 8 | 9 | /** 10 | * @param {string} str 11 | * @returns {Boolean} 12 | */ 13 | export function validUsername(str) { 14 | const valid_map = ['admin', 'editor'] 15 | return valid_map.indexOf(str.trim()) >= 0 16 | } 17 | /** 18 | * @param {string | Number} param 19 | * @returns {Boolean} 20 | */ 21 | export function validMobile(mobile) { 22 | const str = '' + mobile 23 | const reg = /^(13[0-9]|14[5-9]|15[012356789]|166|17[0-8]|18[0-9]|19[0-9])[0-9]{8}$/ 24 | return reg.test(str) 25 | } 26 | 27 | /** 28 | * 验证邮箱 29 | * @param {String} email 30 | */ 31 | export function validEmail(email) { 32 | const reg = /^([a-zA-Z]|[0-9])(\w|\-)+@[a-zA-Z0-9]+\.([a-zA-Z]{2,4})$/ 33 | return reg.test(email) 34 | } 35 | -------------------------------------------------------------------------------- /src/views/about/index.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 38 | 39 | 65 | -------------------------------------------------------------------------------- /src/views/article-manage/components/ArticlePreview.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 70 | 71 | 83 | -------------------------------------------------------------------------------- /src/views/article-manage/components/DynamicTags.vue: -------------------------------------------------------------------------------- 1 | 2 | 41 | 42 | 142 | 143 | 162 | -------------------------------------------------------------------------------- /src/views/article-manage/components/ImgUpload.vue: -------------------------------------------------------------------------------- 1 | 2 | 39 | 40 | 115 | 116 | 118 | -------------------------------------------------------------------------------- /src/views/article/components/ArtTags.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 29 | 30 | 50 | -------------------------------------------------------------------------------- /src/views/article/components/CopyRight.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 25 | 26 | 55 | -------------------------------------------------------------------------------- /src/views/article/components/InterrelatedList.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 53 | 54 | 101 | -------------------------------------------------------------------------------- /src/views/article/components/SupportAuthor.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 10 | 11 | 44 | -------------------------------------------------------------------------------- /src/views/email-validate/index.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 75 | 76 | 140 | -------------------------------------------------------------------------------- /src/views/file-manage/components/ImgUpload.vue: -------------------------------------------------------------------------------- 1 | 2 | 39 | 40 | 115 | 116 | 118 | -------------------------------------------------------------------------------- /src/views/friend-link/index.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 49 | 50 | 125 | -------------------------------------------------------------------------------- /src/views/index/components/RightSideAbout.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 12 | 13 | 37 | -------------------------------------------------------------------------------- /src/views/index/components/RightSideComment.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 73 | 74 | 144 | -------------------------------------------------------------------------------- /src/views/index/components/RightSideRecommend.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 48 | 49 | 96 | -------------------------------------------------------------------------------- /src/views/index/components/RightSideSection.vue: -------------------------------------------------------------------------------- 1 | 77 | 78 | 80 | 81 | 144 | -------------------------------------------------------------------------------- /src/views/index/components/RightSideTags.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 58 | 59 | 108 | -------------------------------------------------------------------------------- /src/views/oauth/index.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 65 | 66 | 91 | -------------------------------------------------------------------------------- /src/views/privacy/index.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 49 | 50 | 76 | -------------------------------------------------------------------------------- /src/views/recommend-manage/components/ArticlePreview.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 65 | 66 | 78 | -------------------------------------------------------------------------------- /src/views/search/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 29 | 30 | 98 | 99 | 159 | -------------------------------------------------------------------------------- /src/views/terms/index.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 32 | 33 | 59 | -------------------------------------------------------------------------------- /tests/unit/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | jest: true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /tests/unit/components/Breadcrumb.spec.js: -------------------------------------------------------------------------------- 1 | import { mount, createLocalVue } from '@vue/test-utils' 2 | import VueRouter from 'vue-router' 3 | import ElementUI from 'element-ui' 4 | import Breadcrumb from '@/components/Breadcrumb/index.vue' 5 | 6 | const localVue = createLocalVue() 7 | localVue.use(VueRouter) 8 | localVue.use(ElementUI) 9 | 10 | const routes = [ 11 | { 12 | path: '/', 13 | name: 'home', 14 | children: [{ 15 | path: 'dashboard', 16 | name: 'dashboard' 17 | }] 18 | }, 19 | { 20 | path: '/menu', 21 | name: 'menu', 22 | children: [{ 23 | path: 'menu1', 24 | name: 'menu1', 25 | meta: { title: 'menu1' }, 26 | children: [{ 27 | path: 'menu1-1', 28 | name: 'menu1-1', 29 | meta: { title: 'menu1-1' } 30 | }, 31 | { 32 | path: 'menu1-2', 33 | name: 'menu1-2', 34 | redirect: 'noredirect', 35 | meta: { title: 'menu1-2' }, 36 | children: [{ 37 | path: 'menu1-2-1', 38 | name: 'menu1-2-1', 39 | meta: { title: 'menu1-2-1' } 40 | }, 41 | { 42 | path: 'menu1-2-2', 43 | name: 'menu1-2-2' 44 | }] 45 | }] 46 | }] 47 | }] 48 | 49 | const router = new VueRouter({ 50 | routes 51 | }) 52 | 53 | describe('Breadcrumb.vue', () => { 54 | const wrapper = mount(Breadcrumb, { 55 | localVue, 56 | router 57 | }) 58 | it('dashboard', () => { 59 | router.push('/dashboard') 60 | const len = wrapper.findAll('.el-breadcrumb__inner').length 61 | expect(len).toBe(1) 62 | }) 63 | it('normal route', () => { 64 | router.push('/menu/menu1') 65 | const len = wrapper.findAll('.el-breadcrumb__inner').length 66 | expect(len).toBe(2) 67 | }) 68 | it('nested route', () => { 69 | router.push('/menu/menu1/menu1-2/menu1-2-1') 70 | const len = wrapper.findAll('.el-breadcrumb__inner').length 71 | expect(len).toBe(4) 72 | }) 73 | it('no meta.title', () => { 74 | router.push('/menu/menu1/menu1-2/menu1-2-2') 75 | const len = wrapper.findAll('.el-breadcrumb__inner').length 76 | expect(len).toBe(3) 77 | }) 78 | // it('click link', () => { 79 | // router.push('/menu/menu1/menu1-2/menu1-2-2') 80 | // const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner') 81 | // const second = breadcrumbArray.at(1) 82 | // console.log(breadcrumbArray) 83 | // const href = second.find('a').attributes().href 84 | // expect(href).toBe('#/menu/menu1') 85 | // }) 86 | // it('noRedirect', () => { 87 | // router.push('/menu/menu1/menu1-2/menu1-2-1') 88 | // const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner') 89 | // const redirectBreadcrumb = breadcrumbArray.at(2) 90 | // expect(redirectBreadcrumb.contains('a')).toBe(false) 91 | // }) 92 | it('last breadcrumb', () => { 93 | router.push('/menu/menu1/menu1-2/menu1-2-1') 94 | const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner') 95 | const redirectBreadcrumb = breadcrumbArray.at(3) 96 | expect(redirectBreadcrumb.contains('a')).toBe(false) 97 | }) 98 | }) 99 | -------------------------------------------------------------------------------- /tests/unit/components/Hamburger.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils' 2 | import Hamburger from '@/components/Hamburger/index.vue' 3 | describe('Hamburger.vue', () => { 4 | it('toggle click', () => { 5 | const wrapper = shallowMount(Hamburger) 6 | const mockFn = jest.fn() 7 | wrapper.vm.$on('toggleClick', mockFn) 8 | wrapper.find('.hamburger').trigger('click') 9 | expect(mockFn).toBeCalled() 10 | }) 11 | it('prop isActive', () => { 12 | const wrapper = shallowMount(Hamburger) 13 | wrapper.setProps({ isActive: true }) 14 | expect(wrapper.contains('.is-active')).toBe(true) 15 | wrapper.setProps({ isActive: false }) 16 | expect(wrapper.contains('.is-active')).toBe(false) 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /tests/unit/components/SvgIcon.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils' 2 | import SvgIcon from '@/components/SvgIcon/index.vue' 3 | describe('SvgIcon.vue', () => { 4 | it('iconClass', () => { 5 | const wrapper = shallowMount(SvgIcon, { 6 | propsData: { 7 | iconClass: 'test' 8 | } 9 | }) 10 | expect(wrapper.find('use').attributes().href).toBe('#icon-test') 11 | }) 12 | it('className', () => { 13 | const wrapper = shallowMount(SvgIcon, { 14 | propsData: { 15 | iconClass: 'test' 16 | } 17 | }) 18 | expect(wrapper.classes().length).toBe(1) 19 | wrapper.setProps({ className: 'test' }) 20 | expect(wrapper.classes().includes('test')).toBe(true) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /tests/unit/utils/formatTime.spec.js: -------------------------------------------------------------------------------- 1 | import { formatTime } from '@/utils/index.js' 2 | 3 | describe('Utils:formatTime', () => { 4 | const d = new Date('2018-07-13 17:54:01') // "2018-07-13 17:54:01" 5 | const retrofit = 5 * 1000 6 | 7 | it('ten digits timestamp', () => { 8 | expect(formatTime((d / 1000).toFixed(0))).toBe('7月13日17时54分') 9 | }) 10 | it('test now', () => { 11 | expect(formatTime(+new Date() - 1)).toBe('刚刚') 12 | }) 13 | it('less two minute', () => { 14 | expect(formatTime(+new Date() - 60 * 2 * 1000 + retrofit)).toBe('2分钟前') 15 | }) 16 | it('less two hour', () => { 17 | expect(formatTime(+new Date() - 60 * 60 * 2 * 1000 + retrofit)).toBe('2小时前') 18 | }) 19 | it('less one day', () => { 20 | expect(formatTime(+new Date() - 60 * 60 * 24 * 1 * 1000)).toBe('1天前') 21 | }) 22 | it('more than one day', () => { 23 | expect(formatTime(d)).toBe('7月13日17时54分') 24 | }) 25 | it('format', () => { 26 | expect(formatTime(d, '{y}-{m}-{d} {h}:{i}')).toBe('2018-07-13 17:54') 27 | expect(formatTime(d, '{y}-{m}-{d}')).toBe('2018-07-13') 28 | expect(formatTime(d, '{y}/{m}/{d} {h}-{i}')).toBe('2018/07/13 17-54') 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /tests/unit/utils/parseTime.spec.js: -------------------------------------------------------------------------------- 1 | import { parseTime } from '@/utils/index.js' 2 | 3 | describe('Utils:parseTime', () => { 4 | const d = new Date('2018-07-13 17:54:01') // "2018-07-13 17:54:01" 5 | it('timestamp', () => { 6 | expect(parseTime(d)).toBe('2018-07-13 17:54:01') 7 | }) 8 | it('ten digits timestamp', () => { 9 | expect(parseTime((d / 1000).toFixed(0))).toBe('2018-07-13 17:54:01') 10 | }) 11 | it('new Date', () => { 12 | expect(parseTime(new Date(d))).toBe('2018-07-13 17:54:01') 13 | }) 14 | it('format', () => { 15 | expect(parseTime(d, '{y}-{m}-{d} {h}:{i}')).toBe('2018-07-13 17:54') 16 | expect(parseTime(d, '{y}-{m}-{d}')).toBe('2018-07-13') 17 | expect(parseTime(d, '{y}/{m}/{d} {h}-{i}')).toBe('2018/07/13 17-54') 18 | }) 19 | it('get the day of the week', () => { 20 | expect(parseTime(d, '{a}')).toBe('五') // 星期五 21 | }) 22 | it('get the day of the week', () => { 23 | expect(parseTime(+d + 1000 * 60 * 60 * 24 * 2, '{a}')).toBe('日') // 星期日 24 | }) 25 | it('empty argument', () => { 26 | expect(parseTime()).toBeNull() 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /tests/unit/utils/validate.spec.js: -------------------------------------------------------------------------------- 1 | import { validUsername, isExternal } from '@/utils/validate.js' 2 | 3 | describe('Utils:validate', () => { 4 | it('validUsername', () => { 5 | expect(validUsername('admin')).toBe(true) 6 | expect(validUsername('editor')).toBe(true) 7 | expect(validUsername('xxxx')).toBe(false) 8 | }) 9 | it('isExternal', () => { 10 | expect(isExternal('https://github.com/PanJiaChen/vue-element-admin')).toBe(true) 11 | expect(isExternal('http://github.com/PanJiaChen/vue-element-admin')).toBe(true) 12 | expect(isExternal('github.com/PanJiaChen/vue-element-admin')).toBe(false) 13 | expect(isExternal('/dashboard')).toBe(false) 14 | expect(isExternal('./dashboard')).toBe(false) 15 | expect(isExternal('dashboard')).toBe(false) 16 | }) 17 | }) 18 | --------------------------------------------------------------------------------