├── .eslintignore
├── tests
└── unit
│ ├── .eslintrc.js
│ ├── utils
│ ├── param2Obj.spec.js
│ ├── formatTime.spec.js
│ ├── validate.spec.js
│ └── parseTime.spec.js
│ └── components
│ ├── Hamburger.spec.js
│ └── SvgIcon.spec.js
├── postcss.config.js
├── public
├── favicon.ico
└── index.html
├── .env.development
├── .travis.yml
├── src
├── assets
│ ├── 401_images
│ │ └── 401.gif
│ ├── 404_images
│ │ ├── 404.png
│ │ └── 404_cloud.png
│ └── custom-theme
│ │ └── fonts
│ │ ├── element-icons.ttf
│ │ └── element-icons.woff
├── views
│ ├── article
│ │ ├── components
│ │ │ ├── Dropdown
│ │ │ │ ├── index.js
│ │ │ │ ├── Comment.vue
│ │ │ │ └── PublishType.vue
│ │ │ └── Warning.vue
│ │ ├── edit.vue
│ │ └── create.vue
│ ├── profile
│ │ ├── about.vue
│ │ ├── base.vue
│ │ └── components
│ │ │ └── Account.vue
│ ├── setting
│ │ ├── storage.vue
│ │ ├── sys.vue
│ │ └── ui.vue
│ └── dashboard
│ │ └── components
│ │ ├── mixins
│ │ └── resize.js
│ │ ├── TodoList
│ │ └── Todo.vue
│ │ ├── PieChart.vue
│ │ ├── CommentTable.vue
│ │ └── LineChart.vue
├── App.vue
├── icons
│ ├── svg
│ │ ├── chart.svg
│ │ ├── size.svg
│ │ ├── link.svg
│ │ ├── guide.svg
│ │ ├── component.svg
│ │ ├── money.svg
│ │ ├── email.svg
│ │ ├── drag.svg
│ │ ├── documentation.svg
│ │ ├── fullscreen.svg
│ │ ├── user.svg
│ │ ├── lock.svg
│ │ ├── excel.svg
│ │ ├── example.svg
│ │ ├── star.svg
│ │ ├── table.svg
│ │ ├── search.svg
│ │ ├── password.svg
│ │ ├── education.svg
│ │ ├── tab.svg
│ │ ├── message.svg
│ │ ├── theme.svg
│ │ ├── peoples.svg
│ │ ├── edit.svg
│ │ ├── nested.svg
│ │ ├── tree-table.svg
│ │ ├── eye.svg
│ │ ├── clipboard.svg
│ │ ├── list.svg
│ │ ├── icon.svg
│ │ ├── international.svg
│ │ ├── wechat.svg
│ │ ├── skill.svg
│ │ ├── people.svg
│ │ ├── language.svg
│ │ ├── eye-open.svg
│ │ ├── 404.svg
│ │ ├── zip.svg
│ │ ├── bug.svg
│ │ ├── pdf.svg
│ │ ├── exit-fullscreen.svg
│ │ ├── tree.svg
│ │ ├── shopping.svg
│ │ ├── dashboard.svg
│ │ └── form.svg
│ ├── index.js
│ └── svgo.yml
├── components
│ ├── ImageCropper
│ │ └── utils
│ │ │ ├── mimes.js
│ │ │ ├── data2blob.js
│ │ │ └── effectRipple.js
│ ├── Tinymce
│ │ ├── toolbar.js
│ │ ├── plugins.js
│ │ └── dynamicLoadScript.js
│ ├── Screenfull
│ │ └── index.vue
│ ├── Hamburger
│ │ └── index.vue
│ ├── SvgIcon
│ │ └── index.vue
│ ├── SizeSelect
│ │ └── index.vue
│ ├── Charts
│ │ └── mixins
│ │ │ └── resize.js
│ ├── DragSelect
│ │ └── index.vue
│ ├── GithubCorner
│ │ └── index.vue
│ ├── MarkdownEditor
│ │ └── index.vue
│ ├── Sticky
│ │ └── index.vue
│ ├── Kanban
│ │ └── index.vue
│ ├── Pagination
│ │ └── index.vue
│ ├── ErrorLog
│ │ └── index.vue
│ ├── Breadcrumb
│ │ └── index.vue
│ ├── Share
│ │ └── DropdownMenu.vue
│ └── TextHoverEffect
│ │ └── Mallki.vue
├── layout
│ ├── components
│ │ ├── index.js
│ │ ├── Sidebar
│ │ │ ├── FixiOSBug.js
│ │ │ ├── Link.vue
│ │ │ ├── Item.vue
│ │ │ ├── index.vue
│ │ │ └── Logo.vue
│ │ ├── AppMain.vue
│ │ ├── Settings
│ │ │ └── index.vue
│ │ └── TagsView
│ │ │ └── ScrollPane.vue
│ ├── mixin
│ │ └── ResizeHandler.js
│ └── index.vue
├── utils
│ ├── get-page-title.js
│ ├── auth.js
│ ├── permission.js
│ ├── clipboard.js
│ ├── error-log.js
│ ├── open-window.js
│ ├── scroll-to.js
│ ├── validate.js
│ └── request.js
├── directive
│ ├── waves
│ │ ├── index.js
│ │ ├── waves.css
│ │ └── waves.js
│ ├── el-drag-dialog
│ │ ├── index.js
│ │ └── drag.js
│ ├── clipboard
│ │ ├── index.js
│ │ └── clipboard.js
│ ├── permission
│ │ ├── index.js
│ │ └── permission.js
│ ├── el-table
│ │ ├── index.js
│ │ └── adaptive.js
│ └── sticky.js
├── api
│ ├── remote-search.js
│ ├── other.js
│ ├── article-label.js
│ ├── comment.js
│ ├── link.js
│ ├── role.js
│ ├── file.js
│ ├── article.js
│ ├── sys.js
│ └── user.js
├── store
│ ├── modules
│ │ ├── errorLog.js
│ │ ├── settings.js
│ │ ├── app.js
│ │ └── permission.js
│ ├── getters.js
│ └── index.js
├── styles
│ ├── variables.scss
│ ├── element-variables.scss
│ ├── transition.scss
│ ├── element-ui.scss
│ ├── mixin.scss
│ └── btn.scss
├── settings.js
├── main.js
├── filters
│ └── index.js
└── permission.js
├── .env.production
├── .env.staging
├── jsconfig.json
├── .editorconfig
├── Dockerfile
├── .gitignore
├── plopfile.js
├── babel.config.js
├── jest.config.js
├── mock
├── comment.js
├── link.js
├── utils.js
├── article-label.js
├── index.js
├── role
│ └── index.js
├── file.js
├── mock-server.js
└── user.js
└── LICENSE
/.eslintignore:
--------------------------------------------------------------------------------
1 | build/*.js
2 | src/assets
3 | public
4 | dist
5 |
--------------------------------------------------------------------------------
/tests/unit/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | jest: true
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | autoprefixer: {}
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WindSnowLi/vue-admin-blog/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/.env.development:
--------------------------------------------------------------------------------
1 | # just a flag
2 | ENV = 'development'
3 |
4 | # base api
5 | VUE_APP_BASE_API = '/dev-api'
6 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js: 10
3 | script: npm run test
4 | notifications:
5 | email: false
6 |
--------------------------------------------------------------------------------
/src/assets/401_images/401.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WindSnowLi/vue-admin-blog/HEAD/src/assets/401_images/401.gif
--------------------------------------------------------------------------------
/src/assets/404_images/404.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WindSnowLi/vue-admin-blog/HEAD/src/assets/404_images/404.png
--------------------------------------------------------------------------------
/.env.production:
--------------------------------------------------------------------------------
1 | # just a flag
2 | ENV = 'production'
3 |
4 | # base api
5 | VUE_APP_BASE_API = 'http://127.0.0.1:9000/api'
6 |
--------------------------------------------------------------------------------
/src/assets/404_images/404_cloud.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WindSnowLi/vue-admin-blog/HEAD/src/assets/404_images/404_cloud.png
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/src/assets/custom-theme/fonts/element-icons.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WindSnowLi/vue-admin-blog/HEAD/src/assets/custom-theme/fonts/element-icons.ttf
--------------------------------------------------------------------------------
/src/assets/custom-theme/fonts/element-icons.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WindSnowLi/vue-admin-blog/HEAD/src/assets/custom-theme/fonts/element-icons.woff
--------------------------------------------------------------------------------
/src/views/article/components/Dropdown/index.js:
--------------------------------------------------------------------------------
1 | export { default as CommentDropdown } from './Comment'
2 | export { default as PublishType } from './PublishType'
3 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "./",
4 | "paths": {
5 | "@/*": ["src/*"]
6 | }
7 | },
8 | "exclude": ["node_modules", "dist"]
9 | }
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
--------------------------------------------------------------------------------
/src/icons/svg/chart.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/ImageCropper/utils/mimes.js:
--------------------------------------------------------------------------------
1 | export default {
2 | 'jpg': 'image/jpeg',
3 | 'png': 'image/png',
4 | 'gif': 'image/gif',
5 | 'svg': 'image/svg+xml',
6 | 'psd': 'image/photoshop'
7 | }
8 |
--------------------------------------------------------------------------------
/src/icons/svg/size.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/views/article/edit.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
13 |
14 |
--------------------------------------------------------------------------------
/src/icons/svg/link.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/layout/components/index.js:
--------------------------------------------------------------------------------
1 | export { default as AppMain } from './AppMain'
2 | export { default as Navbar } from './Navbar'
3 | export { default as Settings } from './Settings'
4 | export { default as Sidebar } from './Sidebar/index.vue'
5 | export { default as TagsView } from './TagsView/index.vue'
6 |
--------------------------------------------------------------------------------
/src/utils/get-page-title.js:
--------------------------------------------------------------------------------
1 | import defaultSettings from '@/settings'
2 |
3 | const title = defaultSettings.title || 'Vue Element Admin'
4 |
5 | export default function getPageTitle(pageTitle) {
6 | if (pageTitle) {
7 | return `${pageTitle} - ${title}`
8 | }
9 | return `${title}`
10 | }
11 |
--------------------------------------------------------------------------------
/src/views/article/create.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
15 |
--------------------------------------------------------------------------------
/src/directive/waves/index.js:
--------------------------------------------------------------------------------
1 | import waves from './waves'
2 |
3 | const install = function(Vue) {
4 | Vue.directive('waves', waves)
5 | }
6 |
7 | if (window.Vue) {
8 | window.waves = waves
9 | Vue.use(install); // eslint-disable-line
10 | }
11 |
12 | waves.install = install
13 | export default waves
14 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # https://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 |
--------------------------------------------------------------------------------
/src/icons/svg/guide.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/directive/el-drag-dialog/index.js:
--------------------------------------------------------------------------------
1 | import drag from './drag'
2 |
3 | const install = function(Vue) {
4 | Vue.directive('el-drag-dialog', drag)
5 | }
6 |
7 | if (window.Vue) {
8 | window['el-drag-dialog'] = drag
9 | Vue.use(install); // eslint-disable-line
10 | }
11 |
12 | drag.install = install
13 | export default drag
14 |
--------------------------------------------------------------------------------
/src/icons/svg/component.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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/directive/clipboard/index.js:
--------------------------------------------------------------------------------
1 | import Clipboard from './clipboard'
2 |
3 | const install = function(Vue) {
4 | Vue.directive('Clipboard', Clipboard)
5 | }
6 |
7 | if (window.Vue) {
8 | window.clipboard = Clipboard
9 | Vue.use(install); // eslint-disable-line
10 | }
11 |
12 | Clipboard.install = install
13 | export default Clipboard
14 |
--------------------------------------------------------------------------------
/src/icons/svg/money.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM httpd:alpine
2 |
3 | LABEL "author"="WindSnowLi"
4 | LABEL "version"="1.0.1"
5 | LABEL "email"="windsnowli@163.com"
6 | # 配置环境变量支持中文
7 | ENV LANG="en_US.UTF-8"
8 |
9 | COPY ./dist /admin
10 | WORKDIR /admin
11 |
12 | RUN rm -rf /usr/local/apache2/htdocs/*
13 | RUN mv /admin/* /usr/local/apache2/htdocs/
14 | # 后台端口
15 | EXPOSE 80
16 |
--------------------------------------------------------------------------------
/src/utils/auth.js:
--------------------------------------------------------------------------------
1 | import Cookies from 'js-cookie'
2 |
3 | const TokenKey = 'Admin-Token'
4 |
5 | export function getToken() {
6 | return Cookies.get(TokenKey)
7 | }
8 |
9 | export function setToken(token) {
10 | return Cookies.set(TokenKey, token)
11 | }
12 |
13 | export function removeToken() {
14 | return Cookies.remove(TokenKey)
15 | }
16 |
--------------------------------------------------------------------------------
/src/directive/permission/index.js:
--------------------------------------------------------------------------------
1 | import permission from './permission'
2 |
3 | const install = function(Vue) {
4 | Vue.directive('permission', permission)
5 | }
6 |
7 | if (window.Vue) {
8 | window['permission'] = permission
9 | Vue.use(install); // eslint-disable-line
10 | }
11 |
12 | permission.install = install
13 | export default permission
14 |
--------------------------------------------------------------------------------
/src/icons/svg/email.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/svg/drag.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules/
3 | dist/
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | **/*.log
8 |
9 | tests/**/coverage/
10 | tests/e2e/reports
11 | selenium-debug.log
12 |
13 | # Editor directories and files
14 | .idea
15 | .vscode
16 | *.suo
17 | *.ntvs*
18 | *.njsproj
19 | *.sln
20 | *.local
21 |
22 | package-lock.json
23 | yarn.lock
24 |
--------------------------------------------------------------------------------
/src/directive/el-table/index.js:
--------------------------------------------------------------------------------
1 | import adaptive from './adaptive'
2 |
3 | const install = function(Vue) {
4 | Vue.directive('el-height-adaptive-table', adaptive)
5 | }
6 |
7 | if (window.Vue) {
8 | window['el-height-adaptive-table'] = adaptive
9 | Vue.use(install); // eslint-disable-line
10 | }
11 |
12 | adaptive.install = install
13 | export default adaptive
14 |
--------------------------------------------------------------------------------
/plopfile.js:
--------------------------------------------------------------------------------
1 | const viewGenerator = require('./plop-templates/view/prompt')
2 | const componentGenerator = require('./plop-templates/component/prompt')
3 | const storeGenerator = require('./plop-templates/store/prompt.js')
4 |
5 | module.exports = function(plop) {
6 | plop.setGenerator('view', viewGenerator)
7 | plop.setGenerator('component', componentGenerator)
8 | plop.setGenerator('store', storeGenerator)
9 | }
10 |
--------------------------------------------------------------------------------
/src/api/remote-search.js:
--------------------------------------------------------------------------------
1 | import request from '@/utils/request'
2 |
3 | export function searchUser(name) {
4 | return request({
5 | url: '/vue-element-admin/search/user',
6 | method: 'get',
7 | params: { name }
8 | })
9 | }
10 |
11 | export function transactionList(query) {
12 | return request({
13 | url: '/vue-element-admin/transaction/list',
14 | method: 'get',
15 | params: query
16 | })
17 | }
18 |
--------------------------------------------------------------------------------
/src/icons/svg/documentation.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/svg/fullscreen.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/svg/user.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/svg/lock.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/svg/excel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/unit/utils/param2Obj.spec.js:
--------------------------------------------------------------------------------
1 | import { param2Obj } from '@/utils/index.js'
2 | describe('Utils:param2Obj', () => {
3 | const url = 'https://github.com/PanJiaChen/vue-element-admin?name=bill&age=29&sex=1&field=dGVzdA==&key=%E6%B5%8B%E8%AF%95'
4 |
5 | it('param2Obj test', () => {
6 | expect(param2Obj(url)).toEqual({
7 | name: 'bill',
8 | age: '29',
9 | sex: '1',
10 | field: window.btoa('test'),
11 | key: '测试'
12 | })
13 | })
14 | })
15 |
--------------------------------------------------------------------------------
/src/icons/svg/example.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/api/other.js:
--------------------------------------------------------------------------------
1 | import request from '@/utils/request'
2 |
3 | /**
4 | * 获取仪表盘折线图和panel-group部分
5 | * @param token 验证信息
6 | */
7 | export function getPanel(token) {
8 | return request({
9 | url: '/other/getPanel',
10 | method: 'post',
11 | data: { token }
12 | })
13 | }
14 |
15 | /**
16 | * 获取图表信息
17 | * @param token 验证信息
18 | */
19 | export function getChart(token) {
20 | return request({
21 | url: '/other/getChart',
22 | method: 'post',
23 | data: { token }
24 | })
25 | }
26 |
--------------------------------------------------------------------------------
/src/components/Tinymce/toolbar.js:
--------------------------------------------------------------------------------
1 | // Here is a list of the toolbar
2 | // Detail list see https://www.tinymce.com/docs/advanced/editor-control-identifiers/#toolbarcontrols
3 |
4 | const toolbar = ['searchreplace bold italic underline strikethrough alignleft aligncenter alignright outdent indent blockquote undo redo removeformat subscript superscript code codesample', 'hr bullist numlist link image charmap preview anchor pagebreak insertdatetime media table emoticons forecolor backcolor fullscreen']
5 |
6 | export default toolbar
7 |
--------------------------------------------------------------------------------
/src/store/modules/errorLog.js:
--------------------------------------------------------------------------------
1 | const state = {
2 | logs: []
3 | }
4 |
5 | const mutations = {
6 | ADD_ERROR_LOG: (state, log) => {
7 | state.logs.push(log)
8 | },
9 | CLEAR_ERROR_LOG: (state) => {
10 | state.logs.splice(0)
11 | }
12 | }
13 |
14 | const actions = {
15 | addErrorLog({ commit }, log) {
16 | commit('ADD_ERROR_LOG', log)
17 | },
18 | clearErrorLog({ commit }) {
19 | commit('CLEAR_ERROR_LOG')
20 | }
21 | }
22 |
23 | export default {
24 | namespaced: true,
25 | state,
26 | mutations,
27 | actions
28 | }
29 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | <%= webpackConfig.name %>
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/icons/svg/star.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/svg/table.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/svg/search.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/Tinymce/plugins.js:
--------------------------------------------------------------------------------
1 | // Any plugins you want to use has to be imported
2 | // Detail plugins list see https://www.tinymce.com/docs/plugins/
3 | // Custom builds see https://www.tinymce.com/download/custom-builds/
4 |
5 | const plugins = ['advlist anchor autolink autosave code codesample colorpicker colorpicker contextmenu directionality emoticons fullscreen hr image imagetools insertdatetime link lists media nonbreaking noneditable pagebreak paste preview print save searchreplace spellchecker tabfocus table template textcolor textpattern visualblocks visualchars wordcount']
6 |
7 | export default plugins
8 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | // https://github.com/vuejs/vue-cli/tree/master/packages/@vue/babel-preset-app
4 | '@vue/cli-plugin-babel/preset'
5 | ],
6 | 'env': {
7 | 'development': {
8 | // babel-plugin-dynamic-import-node plugin only does one thing by converting all import() to require().
9 | // This plugin can significantly increase the speed of hot updates, when you have a large number of pages.
10 | // https://panjiachen.github.io/vue-element-admin-site/guide/advanced/lazy-loading.html
11 | 'plugins': ['dynamic-import-node']
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/icons/svg/password.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/ImageCropper/utils/data2blob.js:
--------------------------------------------------------------------------------
1 | /**
2 | * database64文件格式转换为2进制
3 | *
4 | * @param {[String]} data dataURL 的格式为 “data:image/png;base64,****”,逗号之前都是一些说明性的文字,我们只需要逗号之后的就行了
5 | * @param {[String]} mime [description]
6 | * @return {[blob]} [description]
7 | */
8 | export default function(data, mime) {
9 | data = data.split(',')[1]
10 | data = window.atob(data)
11 | var ia = new Uint8Array(data.length)
12 | for (var i = 0; i < data.length; i++) {
13 | ia[i] = data.charCodeAt(i)
14 | }
15 | // canvas.toDataURL 返回的默认格式就是 image/png
16 | return new Blob([ia], {
17 | type: mime
18 | })
19 | }
20 |
--------------------------------------------------------------------------------
/src/icons/svg/education.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/svg/tab.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/store/getters.js:
--------------------------------------------------------------------------------
1 | const getters = {
2 | sidebar: state => state.app.sidebar,
3 | size: state => state.app.size,
4 | device: state => state.app.device,
5 | visitedViews: state => state.tagsView.visitedViews,
6 | cachedViews: state => state.tagsView.cachedViews,
7 | token: state => state.user.token,
8 | id: state => state.user.id,
9 | avatar: state => state.user.avatar,
10 | name: state => state.user.name,
11 | introduction: state => state.user.introduction,
12 | roles: state => state.user.roles,
13 | permission_routes: state => state.permission.routes,
14 | errorLogs: state => state.errorLog.logs
15 | }
16 | export default getters
17 |
--------------------------------------------------------------------------------
/src/views/article/components/Warning.vue:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/utils/permission.js:
--------------------------------------------------------------------------------
1 | import store from '@/store'
2 |
3 | /**
4 | * @param {Array} value
5 | * @returns {Boolean}
6 | * @example see @/views/permission/directive.vue
7 | */
8 | export default function checkPermission(value) {
9 | if (value && value instanceof Array && value.length > 0) {
10 | const roles = store.getters && store.getters.roles
11 | const permissionRoles = value
12 |
13 | const hasPermission = roles.some(role => {
14 | return permissionRoles.includes(role)
15 | })
16 | return hasPermission
17 | } else {
18 | console.error(`need roles! Like v-permission="['admin','editor']"`)
19 | return false
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/icons/svg/message.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/svg/theme.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/api/article-label.js:
--------------------------------------------------------------------------------
1 | // 文章可选标签
2 | import request from '@/utils/request'
3 |
4 | /**
5 | * 文章可选择标签
6 | */
7 | export function getLabels() {
8 | return request({
9 | url: '/articleLabel/labels',
10 | method: 'post'
11 | })
12 | }
13 |
14 | /**
15 | * 获取标签对象列表
16 | * @param query 分页查询参数对象
17 | */
18 | export function getLabelByPage(query) {
19 | return request({
20 | url: '/articleLabel/getLabelByPage',
21 | method: 'post',
22 | data: query
23 | })
24 | }
25 |
26 | /**
27 | * 设置标签内容
28 | * @param label 标签对象
29 | */
30 | export function setLabel(label) {
31 | return request({
32 | url: '/articleLabel/setLabel',
33 | method: 'post',
34 | data: label
35 | })
36 | }
37 |
--------------------------------------------------------------------------------
/src/api/comment.js:
--------------------------------------------------------------------------------
1 | import request from '@/utils/request'
2 |
3 | /**
4 | * 评论列表
5 | * @param query 分页对象
6 | */
7 | export function getCommentList(query) {
8 | const { page, limit, status, sort } = query
9 | return request({
10 | url: '/comment/getCommentList',
11 | method: 'post',
12 | data: {
13 | page,
14 | limit,
15 | status,
16 | sort
17 | }
18 | })
19 | }
20 |
21 | /**
22 | * 修改评论ID
23 | * @param id 评论ID
24 | * @param status 状态
25 | */
26 | export function setCommentStatus(id, status) {
27 | return request({
28 | url: '/comment/setCommentStatus',
29 | method: 'post',
30 | data: {
31 | id,
32 | content: status
33 | }
34 | })
35 | }
36 |
--------------------------------------------------------------------------------
/src/icons/svg/peoples.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/api/link.js:
--------------------------------------------------------------------------------
1 | import request from '../utils/request.js'
2 |
3 | /**
4 | * 获取友链列表
5 | */
6 | export function getFriendLinks() {
7 | return request({
8 | url: '/links/getFriendLinks',
9 | method: 'post',
10 | data: {
11 | status: 'ALL'
12 | }
13 | })
14 | }
15 |
16 | /**
17 | * 申请友链
18 | */
19 | export function applyFriendLink(friendLinks) {
20 | return request({
21 | url: '/links/applyFriendLink',
22 | method: 'post',
23 | data: {
24 | ...friendLinks
25 | }
26 | })
27 | }
28 |
29 | /**
30 | * 设置友链
31 | */
32 | export function setFriendLink(link) {
33 | return request({
34 | url: '/links/setFriendLink',
35 | method: 'post',
36 | data: {
37 | ...link
38 | }
39 | })
40 | }
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 | // In order to fix the click on menu on the ios device will trigger the mouseleave bug
9 | // https://github.com/PanJiaChen/vue-element-admin/issues/1135
10 | this.fixBugIniOS()
11 | },
12 | methods: {
13 | fixBugIniOS() {
14 | const $subMenu = this.$refs.subMenu
15 | if ($subMenu) {
16 | const handleMouseleave = $subMenu.handleMouseleave
17 | $subMenu.handleMouseleave = (e) => {
18 | if (this.device === 'mobile') {
19 | return
20 | }
21 | handleMouseleave(e)
22 | }
23 | }
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/utils/clipboard.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Clipboard from 'clipboard'
3 |
4 | function clipboardSuccess() {
5 | Vue.prototype.$message({
6 | message: 'Copy successfully',
7 | type: 'success',
8 | duration: 1500
9 | })
10 | }
11 |
12 | function clipboardError() {
13 | Vue.prototype.$message({
14 | message: 'Copy failed',
15 | type: 'error'
16 | })
17 | }
18 |
19 | export default function handleClipboard(text, event) {
20 | const clipboard = new Clipboard(event.target, {
21 | text: () => text
22 | })
23 | clipboard.on('success', () => {
24 | clipboardSuccess()
25 | clipboard.destroy()
26 | })
27 | clipboard.on('error', () => {
28 | clipboardError()
29 | clipboard.destroy()
30 | })
31 | clipboard.onClick(event)
32 | }
33 |
--------------------------------------------------------------------------------
/src/store/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Vuex from 'vuex'
3 | import getters from './getters'
4 |
5 | Vue.use(Vuex)
6 |
7 | // https://webpack.js.org/guides/dependency-management/#requirecontext
8 | const modulesFiles = require.context('./modules', true, /\.js$/)
9 |
10 | // you do not need `import app from './modules/app'`
11 | // it will auto require all vuex module from modules file
12 | const modules = modulesFiles.keys().reduce((modules, modulePath) => {
13 | // set './app.js' => 'app'
14 | const moduleName = modulePath.replace(/^\.\/(.*)\.\w+$/, '$1')
15 | const value = modulesFiles(modulePath)
16 | modules[moduleName] = value.default
17 | return modules
18 | }, {})
19 |
20 | const store = new Vuex.Store({
21 | modules,
22 | getters
23 | })
24 |
25 | export default store
26 |
--------------------------------------------------------------------------------
/src/icons/svg/edit.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/svg/nested.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/api/role.js:
--------------------------------------------------------------------------------
1 | import request from '@/utils/request'
2 |
3 | export function getRoutes() {
4 | return request({
5 | url: '/vue-element-admin/routes',
6 | method: 'get'
7 | })
8 | }
9 |
10 | export function getRoles() {
11 | return request({
12 | url: '/vue-element-admin/roles',
13 | method: 'get'
14 | })
15 | }
16 |
17 | export function addRole(data) {
18 | return request({
19 | url: '/vue-element-admin/role',
20 | method: 'post',
21 | data
22 | })
23 | }
24 |
25 | export function updateRole(id, data) {
26 | return request({
27 | url: `/vue-element-admin/role/${id}`,
28 | method: 'put',
29 | data
30 | })
31 | }
32 |
33 | export function deleteRole(id) {
34 | return request({
35 | url: `/vue-element-admin/role/${id}`,
36 | method: 'delete'
37 | })
38 | }
39 |
--------------------------------------------------------------------------------
/src/directive/permission/permission.js:
--------------------------------------------------------------------------------
1 | import store from '@/store'
2 |
3 | function checkPermission(el, binding) {
4 | const { value } = binding
5 | const roles = store.getters && store.getters.roles
6 |
7 | if (value && value instanceof Array) {
8 | if (value.length > 0) {
9 | const permissionRoles = value
10 |
11 | const hasPermission = roles.some(role => {
12 | return permissionRoles.includes(role)
13 | })
14 |
15 | if (!hasPermission) {
16 | el.parentNode && el.parentNode.removeChild(el)
17 | }
18 | }
19 | } else {
20 | throw new Error(`need roles! Like v-permission="['admin','editor']"`)
21 | }
22 | }
23 |
24 | export default {
25 | inserted(el, binding) {
26 | checkPermission(el, binding)
27 | },
28 | update(el, binding) {
29 | checkPermission(el, binding)
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/store/modules/settings.js:
--------------------------------------------------------------------------------
1 | import variables from '@/styles/element-variables.scss'
2 | import defaultSettings from '@/settings'
3 |
4 | const { showSettings, tagsView, fixedHeader, sidebarLogo } = defaultSettings
5 |
6 | const state = {
7 | theme: variables.theme,
8 | showSettings: showSettings,
9 | tagsView: tagsView,
10 | fixedHeader: fixedHeader,
11 | sidebarLogo: sidebarLogo
12 | }
13 |
14 | const mutations = {
15 | CHANGE_SETTING: (state, { key, value }) => {
16 | // eslint-disable-next-line no-prototype-builtins
17 | if (state.hasOwnProperty(key)) {
18 | state[key] = value
19 | }
20 | }
21 | }
22 |
23 | const actions = {
24 | changeSetting({ commit }, data) {
25 | commit('CHANGE_SETTING', data)
26 | }
27 | }
28 |
29 | export default {
30 | namespaced: true,
31 | state,
32 | mutations,
33 | actions
34 | }
35 |
36 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/layout/components/Sidebar/Link.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
44 |
--------------------------------------------------------------------------------
/src/layout/components/Sidebar/Item.vue:
--------------------------------------------------------------------------------
1 |
34 |
35 |
42 |
--------------------------------------------------------------------------------
/src/icons/svg/tree-table.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/svg/eye.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/styles/variables.scss:
--------------------------------------------------------------------------------
1 | // base color
2 | $blue:#324157;
3 | $light-blue:#3A71A8;
4 | $red:#C03639;
5 | $pink: #E65D6E;
6 | $green: #30B08F;
7 | $tiffany: #4AB7BD;
8 | $yellow:#FEC171;
9 | $panGreen: #30B08F;
10 |
11 | // sidebar
12 | $menuText:#bfcbd9;
13 | $menuActiveText:#409EFF;
14 | $subMenuActiveText:#f4f4f5; // https://github.com/ElemeFE/element/issues/12951
15 |
16 | $menuBg:#304156;
17 | $menuHover:#263445;
18 |
19 | $subMenuBg:#1f2d3d;
20 | $subMenuHover:#001528;
21 |
22 | $sideBarWidth: 210px;
23 |
24 | // the :export directive is the magic sauce for webpack
25 | // https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass
26 | :export {
27 | menuText: $menuText;
28 | menuActiveText: $menuActiveText;
29 | subMenuActiveText: $subMenuActiveText;
30 | menuBg: $menuBg;
31 | menuHover: $menuHover;
32 | subMenuBg: $subMenuBg;
33 | subMenuHover: $subMenuHover;
34 | sideBarWidth: $sideBarWidth;
35 | }
36 |
--------------------------------------------------------------------------------
/src/directive/waves/waves.css:
--------------------------------------------------------------------------------
1 | .waves-ripple {
2 | position: absolute;
3 | border-radius: 100%;
4 | background-color: rgba(0, 0, 0, 0.15);
5 | background-clip: padding-box;
6 | pointer-events: none;
7 | -webkit-user-select: none;
8 | -moz-user-select: none;
9 | -ms-user-select: none;
10 | user-select: none;
11 | -webkit-transform: scale(0);
12 | -ms-transform: scale(0);
13 | transform: scale(0);
14 | opacity: 1;
15 | }
16 |
17 | .waves-ripple.z-active {
18 | opacity: 0;
19 | -webkit-transform: scale(2);
20 | -ms-transform: scale(2);
21 | transform: scale(2);
22 | -webkit-transition: opacity 1.2s ease-out, -webkit-transform 0.6s ease-out;
23 | transition: opacity 1.2s ease-out, -webkit-transform 0.6s ease-out;
24 | transition: opacity 1.2s ease-out, transform 0.6s ease-out;
25 | transition: opacity 1.2s ease-out, transform 0.6s ease-out, -webkit-transform 0.6s ease-out;
26 | }
--------------------------------------------------------------------------------
/src/settings.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | title: '个人博客',
3 |
4 | /**
5 | * @type {boolean} true | false
6 | * @description Whether show the settings right-panel
7 | */
8 | showSettings: true,
9 |
10 | /**
11 | * @type {boolean} true | false
12 | * @description Whether need tagsView
13 | */
14 | tagsView: true,
15 |
16 | /**
17 | * @type {boolean} true | false
18 | * @description Whether fix the header
19 | */
20 | fixedHeader: false,
21 |
22 | /**
23 | * @type {boolean} true | false
24 | * @description Whether show the logo in sidebar
25 | */
26 | sidebarLogo: false,
27 |
28 | /**
29 | * @type {string | array} 'production' | ['production', 'development']
30 | * @description Need show err logs component.
31 | * The default is only used in the production env
32 | * If you want to also use it in dev, you can pass ['production', 'development']
33 | */
34 | errorLog: 'production'
35 | }
36 |
--------------------------------------------------------------------------------
/src/styles/element-variables.scss:
--------------------------------------------------------------------------------
1 | /**
2 | * I think element-ui's default theme color is too light for long-term use.
3 | * So I modified the default color and you can modify it to your liking.
4 | **/
5 |
6 | /* theme color */
7 | $--color-primary: #1890ff;
8 | $--color-success: #13ce66;
9 | $--color-warning: #ffba00;
10 | $--color-danger: #ff4949;
11 | // $--color-info: #1E1E1E;
12 |
13 | $--button-font-weight: 400;
14 |
15 | // $--color-text-regular: #1f2d3d;
16 |
17 | $--border-color-light: #dfe4ed;
18 | $--border-color-lighter: #e6ebf5;
19 |
20 | $--table-border: 1px solid #dfe6ec;
21 |
22 | /* icon font path, required */
23 | $--font-path: "~element-ui/lib/theme-chalk/fonts";
24 |
25 | @import "~element-ui/packages/theme-chalk/src/index";
26 |
27 | // the :export directive is the magic sauce for webpack
28 | // https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass
29 | :export {
30 | theme: $--color-primary;
31 | }
32 |
--------------------------------------------------------------------------------
/src/styles/transition.scss:
--------------------------------------------------------------------------------
1 | // global transition css
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 | /* breadcrumb transition */
31 | .breadcrumb-enter-active,
32 | .breadcrumb-leave-active {
33 | transition: all .5s;
34 | }
35 |
36 | .breadcrumb-enter,
37 | .breadcrumb-leave-active {
38 | opacity: 0;
39 | transform: translateX(20px);
40 | }
41 |
42 | .breadcrumb-move {
43 | transition: all .5s;
44 | }
45 |
46 | .breadcrumb-leave-active {
47 | position: absolute;
48 | }
49 |
--------------------------------------------------------------------------------
/src/icons/svg/clipboard.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/svg/list.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/svg/icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/utils/error-log.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import store from '@/store'
3 | import { isString, isArray } from '@/utils/validate'
4 | import settings from '@/settings'
5 |
6 | // you can set in settings.js
7 | // errorLog:'production' | ['production', 'development']
8 | const { errorLog: needErrorLog } = settings
9 |
10 | function checkNeed() {
11 | const env = process.env.NODE_ENV
12 | if (isString(needErrorLog)) {
13 | return env === needErrorLog
14 | }
15 | if (isArray(needErrorLog)) {
16 | return needErrorLog.includes(env)
17 | }
18 | return false
19 | }
20 |
21 | if (checkNeed()) {
22 | Vue.config.errorHandler = function(err, vm, info, a) {
23 | // Don't ask me why I use Vue.nextTick, it just a hack.
24 | // detail see https://forum.vuejs.org/t/dispatch-in-vue-config-errorhandler-has-some-problem/23500
25 | Vue.nextTick(() => {
26 | store.dispatch('errorLog/addErrorLog', {
27 | err,
28 | vm,
29 | info,
30 | url: window.location.href
31 | })
32 | console.error(err, info)
33 | })
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/mock/comment.js:
--------------------------------------------------------------------------------
1 | const Mock = require('mockjs')
2 | const List = []
3 | const count = 20
4 | for (let i = 0; i < count; i++) {
5 | List.push(Mock.mock({
6 | id: '@increment',
7 | content: '@guid()',
8 | time: +Mock.Random.date('T'),
9 | fromUser: {
10 | id: '@increment',
11 | nickname: '@name()'
12 | },
13 | toUser: {
14 | id: '@increment',
15 | nickname: '@name()'
16 | },
17 | target: {
18 | title: '@title(5, 10)'
19 | }
20 | }))
21 | }
22 |
23 | module.exports = [
24 | // 获取评论列表
25 | {
26 | url: '/comment/getCommentList',
27 | type: 'post',
28 | dataType: 'json',
29 | response: _ => {
30 | return {
31 | code: 20000,
32 | data: {
33 | total: 20,
34 | items: List
35 | }
36 | }
37 | }
38 | },
39 | // 修改评论状态
40 | {
41 | url: '/comment/setCommentStatus',
42 | type: 'post',
43 | dataType: 'json',
44 | response: _ => {
45 | return {
46 | code: 20000,
47 | message: 'OK'
48 | }
49 | }
50 | }
51 | ]
52 |
--------------------------------------------------------------------------------
/src/icons/svg/international.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/views/article/components/Dropdown/Comment.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ !comment_disabled?'评论: 开启':'评论: 关闭' }}
5 |
6 |
7 |
8 |
9 |
10 |
11 | 关闭评论
12 |
13 |
14 | 打开评论
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
42 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 WindSnowLi
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/icons/svg/wechat.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/views/profile/about.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
47 |
--------------------------------------------------------------------------------
/src/icons/svg/skill.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/unit/utils/formatTime.spec.js:
--------------------------------------------------------------------------------
1 | import { formatTime } from '@/utils/index.js'
2 | describe('Utils:formatTime', () => {
3 | const d = new Date('2018-07-13 17:54:01') // "2018-07-13 17:54:01"
4 | const retrofit = 5 * 1000
5 |
6 | it('ten digits timestamp', () => {
7 | expect(formatTime((d / 1000).toFixed(0))).toBe('7月13日17时54分')
8 | })
9 | it('test now', () => {
10 | expect(formatTime(+new Date() - 1)).toBe('刚刚')
11 | })
12 | it('less two minute', () => {
13 | expect(formatTime(+new Date() - 60 * 2 * 1000 + retrofit)).toBe('2分钟前')
14 | })
15 | it('less two hour', () => {
16 | expect(formatTime(+new Date() - 60 * 60 * 2 * 1000 + retrofit)).toBe('2小时前')
17 | })
18 | it('less one day', () => {
19 | expect(formatTime(+new Date() - 60 * 60 * 24 * 1 * 1000)).toBe('1天前')
20 | })
21 | it('more than one day', () => {
22 | expect(formatTime(d)).toBe('7月13日17时54分')
23 | })
24 | it('format', () => {
25 | expect(formatTime(d, '{y}-{m}-{d} {h}:{i}')).toBe('2018-07-13 17:54')
26 | expect(formatTime(d, '{y}-{m}-{d}')).toBe('2018-07-13')
27 | expect(formatTime(d, '{y}/{m}/{d} {h}-{i}')).toBe('2018/07/13 17-54')
28 | })
29 | })
30 |
--------------------------------------------------------------------------------
/src/icons/svg/people.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/mock/link.js:
--------------------------------------------------------------------------------
1 | const Mock = require('mockjs')
2 | const List = []
3 | const count = 20
4 |
5 | for (let i = 0; i < count; i++) {
6 | List.push(Mock.mock({
7 | id: '@increment',
8 | link: '@url',
9 | title: '@csentence',
10 | describe: '@csentence',
11 | email: '@email',
12 | 'status|1': ['PASS', 'APPLY', 'REFUSE', 'HIDE', 'DELETE'],
13 | coverPic: 'https://wpimg.wallstcn.com/e4558086-631c-425c-9430-56ffb46e70b3',
14 | createTime: +Mock.Random.date('T'),
15 | updateTime: +Mock.Random.date('T')
16 | }))
17 | }
18 |
19 | module.exports = [
20 | // 获取友链
21 | {
22 | url: '/links/getFriendLinks',
23 | type: 'post',
24 | response: _ => {
25 | return {
26 | code: 20000,
27 | data: List
28 | }
29 | }
30 | },
31 | // 添加友链
32 | {
33 | url: '/links/applyFriendLink',
34 | type: 'post',
35 | response: _ => {
36 | return {
37 | code: 20000,
38 | data: 'OK'
39 | }
40 | }
41 | },
42 | // 设置友链状态
43 | {
44 | url: '/links/setFriendLink',
45 | type: 'post',
46 | response: _ => {
47 | return {
48 | code: 20000,
49 | data: 'OK'
50 | }
51 | }
52 | }
53 | ]
54 |
--------------------------------------------------------------------------------
/src/icons/svg/language.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/layout/components/AppMain.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
24 |
25 |
49 |
50 |
58 |
--------------------------------------------------------------------------------
/src/views/article/components/Dropdown/PublishType.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 发布类型
5 |
6 |
7 |
8 |
9 |
10 | {{ item.name }}
11 |
12 |
13 |
14 |
15 |
16 |
17 |
47 |
--------------------------------------------------------------------------------
/tests/unit/utils/validate.spec.js:
--------------------------------------------------------------------------------
1 | import { validUsername, validURL, validLowerCase, validUpperCase, validAlphabets } from '@/utils/validate.js'
2 | describe('Utils:validate', () => {
3 | it('validUsername', () => {
4 | expect(validUsername('admin')).toBe(true)
5 | expect(validUsername('editor')).toBe(true)
6 | expect(validUsername('xxxx')).toBe(false)
7 | })
8 | it('validURL', () => {
9 | expect(validURL('https://github.com/PanJiaChen/vue-element-admin')).toBe(true)
10 | expect(validURL('http://github.com/PanJiaChen/vue-element-admin')).toBe(true)
11 | expect(validURL('github.com/PanJiaChen/vue-element-admin')).toBe(false)
12 | })
13 | it('validLowerCase', () => {
14 | expect(validLowerCase('abc')).toBe(true)
15 | expect(validLowerCase('Abc')).toBe(false)
16 | expect(validLowerCase('123abc')).toBe(false)
17 | })
18 | it('validUpperCase', () => {
19 | expect(validUpperCase('ABC')).toBe(true)
20 | expect(validUpperCase('Abc')).toBe(false)
21 | expect(validUpperCase('123ABC')).toBe(false)
22 | })
23 | it('validAlphabets', () => {
24 | expect(validAlphabets('ABC')).toBe(true)
25 | expect(validAlphabets('Abc')).toBe(true)
26 | expect(validAlphabets('123aBC')).toBe(false)
27 | })
28 | })
29 |
--------------------------------------------------------------------------------
/src/icons/svg/eye-open.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/utils/open-window.js:
--------------------------------------------------------------------------------
1 | /**
2 | *Created by PanJiaChen on 16/11/29.
3 | * @param {Sting} url
4 | * @param {Sting} title
5 | * @param {Number} w
6 | * @param {Number} h
7 | */
8 | export default function openWindow(url, title, w, h) {
9 | // Fixes dual-screen position Most browsers Firefox
10 | const dualScreenLeft = window.screenLeft !== undefined ? window.screenLeft : screen.left
11 | const dualScreenTop = window.screenTop !== undefined ? window.screenTop : screen.top
12 |
13 | const width = window.innerWidth ? window.innerWidth : document.documentElement.clientWidth ? document.documentElement.clientWidth : screen.width
14 | const height = window.innerHeight ? window.innerHeight : document.documentElement.clientHeight ? document.documentElement.clientHeight : screen.height
15 |
16 | const left = ((width / 2) - (w / 2)) + dualScreenLeft
17 | const top = ((height / 2) - (h / 2)) + dualScreenTop
18 | const newWindow = window.open(url, title, 'toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=yes, copyhistory=no, width=' + w + ', height=' + h + ', top=' + top + ', left=' + left)
19 |
20 | // Puts focus on the newWindow
21 | if (window.focus) {
22 | newWindow.focus()
23 | }
24 | }
25 |
26 |
--------------------------------------------------------------------------------
/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 |
9 | it('timestamp string', () => {
10 | expect(parseTime((d + ''))).toBe('2018-07-13 17:54:01')
11 | })
12 |
13 | it('ten digits timestamp', () => {
14 | expect(parseTime((d / 1000).toFixed(0))).toBe('2018-07-13 17:54:01')
15 | })
16 | it('new Date', () => {
17 | expect(parseTime(new Date(d))).toBe('2018-07-13 17:54:01')
18 | })
19 | it('format', () => {
20 | expect(parseTime(d, '{y}-{m}-{d} {h}:{i}')).toBe('2018-07-13 17:54')
21 | expect(parseTime(d, '{y}-{m}-{d}')).toBe('2018-07-13')
22 | expect(parseTime(d, '{y}/{m}/{d} {h}-{i}')).toBe('2018/07/13 17-54')
23 | })
24 | it('get the day of the week', () => {
25 | expect(parseTime(d, '{a}')).toBe('五') // 星期五
26 | })
27 | it('get the day of the week', () => {
28 | expect(parseTime(+d + 1000 * 60 * 60 * 24 * 2, '{a}')).toBe('日') // 星期日
29 | })
30 | it('empty argument', () => {
31 | expect(parseTime()).toBeNull()
32 | })
33 |
34 | it('null', () => {
35 | expect(parseTime(null)).toBeNull()
36 | })
37 | })
38 |
--------------------------------------------------------------------------------
/src/components/Screenfull/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
50 |
51 |
61 |
--------------------------------------------------------------------------------
/mock/utils.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @param {string} url
3 | * @returns {Object}
4 | */
5 | function param2Obj(url) {
6 | const search = decodeURIComponent(url.split('?')[1]).replace(/\+/g, ' ')
7 | if (!search) {
8 | return {}
9 | }
10 | const obj = {}
11 | const searchArr = search.split('&')
12 | searchArr.forEach(v => {
13 | const index = v.indexOf('=')
14 | if (index !== -1) {
15 | const name = v.substring(0, index)
16 | const val = v.substring(index + 1, v.length)
17 | obj[name] = val
18 | }
19 | })
20 | return obj
21 | }
22 |
23 | /**
24 | * This is just a simple version of deep copy
25 | * Has a lot of edge cases bug
26 | * If you want to use a perfect deep copy, use lodash's _.cloneDeep
27 | * @param {Object} source
28 | * @returns {Object}
29 | */
30 | function deepClone(source) {
31 | if (!source && typeof source !== 'object') {
32 | throw new Error('error arguments', 'deepClone')
33 | }
34 | const targetObj = source.constructor === Array ? [] : {}
35 | Object.keys(source).forEach(keys => {
36 | if (source[keys] && typeof source[keys] === 'object') {
37 | targetObj[keys] = deepClone(source[keys])
38 | } else {
39 | targetObj[keys] = source[keys]
40 | }
41 | })
42 | return targetObj
43 | }
44 |
45 | module.exports = {
46 | param2Obj,
47 | deepClone
48 | }
49 |
--------------------------------------------------------------------------------
/src/directive/el-table/adaptive.js:
--------------------------------------------------------------------------------
1 | import { addResizeListener, removeResizeListener } from 'element-ui/src/utils/resize-event'
2 |
3 | /**
4 | * How to use
5 | * ...
6 | * el-table height is must be set
7 | * bottomOffset: 30(default) // The height of the table from the bottom of the page.
8 | */
9 |
10 | const doResize = (el, binding, vnode) => {
11 | const { componentInstance: $table } = vnode
12 |
13 | const { value } = binding
14 |
15 | if (!$table.height) {
16 | throw new Error(`el-$table must set the height. Such as height='100px'`)
17 | }
18 | const bottomOffset = (value && value.bottomOffset) || 30
19 |
20 | if (!$table) return
21 |
22 | const height = window.innerHeight - el.getBoundingClientRect().top - bottomOffset
23 | $table.layout.setHeight(height)
24 | $table.doLayout()
25 | }
26 |
27 | export default {
28 | bind(el, binding, vnode) {
29 | el.resizeListener = () => {
30 | doResize(el, binding, vnode)
31 | }
32 | // parameter 1 is must be "Element" type
33 | addResizeListener(window.document.body, el.resizeListener)
34 | },
35 | inserted(el, binding, vnode) {
36 | doResize(el, binding, vnode)
37 | },
38 | unbind(el) {
39 | removeResizeListener(window.document.body, el.resizeListener)
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/components/Hamburger/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
16 |
32 |
33 |
45 |
--------------------------------------------------------------------------------
/src/api/file.js:
--------------------------------------------------------------------------------
1 | import request from '@/utils/request'
2 |
3 | /**
4 | * 获取头像上传的Url,含上传后的文件名
5 | * {url: 上传签名Url, GetUrl: Get请求Url}
6 | * @param {Object} token 验证信息
7 | */
8 | export function getUploadAvatarUrl(token) {
9 | return request({
10 | url: '/file/getUploadAvatarUrl',
11 | method: 'post',
12 | data: { token }
13 | })
14 | }
15 |
16 | /**
17 | * 获取上传文章封面的Url,含上传后的文件名
18 | * {url: 上传签名Url, GetUrl: Get请求Url}
19 | * @param {Object} token 验证信息
20 | */
21 | export function getUploadArticleCoverImageUrl(token) {
22 | return request({
23 | url: '/file/getUploadArticleCoverImageUrl',
24 | method: 'post',
25 | data: { token }
26 | })
27 | }
28 |
29 | /**
30 | * 获取上传文章内容图片的Url,含上传后的文件名
31 | * {url: 上传签名Url, GetUrl: Get请求Url}
32 | * @param {Object} token 验证信息
33 | */
34 | export function getUploadArticleImageUrl(token) {
35 | return request({
36 | url: '/file/getUploadArticleImageUrl',
37 | method: 'post',
38 | data: { token }
39 | })
40 | }
41 |
42 | /**
43 | * 获取上传文章内容图片的Url,含上传后的文件名
44 | * {url: 上传签名Url, GetUrl: Get请求Url}
45 | * @param {Object} token 验证信息
46 | * @param {String} objectName 文件路径
47 | */
48 | export function deleteObject(token, objectName) {
49 | return request({
50 | url: '/file/deleteObject',
51 | method: 'post',
52 | data: {
53 | token,
54 | content: objectName
55 | }
56 | })
57 | }
58 |
--------------------------------------------------------------------------------
/mock/article-label.js:
--------------------------------------------------------------------------------
1 | const Mock = require('mockjs')
2 | const List = []
3 | const count = 100
4 | const coverPic = 'https://wpimg.wallstcn.com/e4558086-631c-425c-9430-56ffb46e70b3'
5 |
6 | for (let i = 0; i < count; i++) {
7 | List.push(Mock.mock({
8 | id: '@increment',
9 | name: '@title(5, 10)',
10 | coverPic,
11 | describe: '这是其中一个标签的描述'
12 | }))
13 | }
14 |
15 | module.exports = [
16 | // 文章可选标签
17 | {
18 | url: '/articleLabel/labels',
19 | type: 'post',
20 | dataType: 'json',
21 | response: _ => {
22 | return {
23 | code: 20000,
24 | message: '请求成功',
25 | data: [{
26 | value: 'C++',
27 | label: 'C++'
28 | }, {
29 | value: 'Java',
30 | label: 'Java'
31 | }, {
32 | value: 'Python',
33 | label: 'Python'
34 | }]
35 | }
36 | }
37 | },
38 | // 标签对象列表
39 | {
40 | url: '/articleLabel/labelList',
41 | type: 'post',
42 | dataType: 'json',
43 | response: _ => {
44 | return {
45 | code: 20000,
46 | message: '请求成功',
47 | data: List
48 | }
49 | }
50 | },
51 | // 标签对象列表
52 | {
53 | url: '/articleLabel/setLabel',
54 | type: 'post',
55 | dataType: 'json',
56 | response: _ => {
57 | return {
58 | code: 20000,
59 | message: '请求成功'
60 | }
61 | }
62 | }
63 | ]
64 |
--------------------------------------------------------------------------------
/src/icons/svg/404.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/svg/zip.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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/views/setting/storage.vue:
--------------------------------------------------------------------------------
1 |
2 |
15 |
16 |
17 |
57 |
58 |
64 |
--------------------------------------------------------------------------------
/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 | size: Cookies.get('size') || 'medium'
10 | }
11 |
12 | const mutations = {
13 | TOGGLE_SIDEBAR: state => {
14 | state.sidebar.opened = !state.sidebar.opened
15 | state.sidebar.withoutAnimation = false
16 | if (state.sidebar.opened) {
17 | Cookies.set('sidebarStatus', 1)
18 | } else {
19 | Cookies.set('sidebarStatus', 0)
20 | }
21 | },
22 | CLOSE_SIDEBAR: (state, withoutAnimation) => {
23 | Cookies.set('sidebarStatus', 0)
24 | state.sidebar.opened = false
25 | state.sidebar.withoutAnimation = withoutAnimation
26 | },
27 | TOGGLE_DEVICE: (state, device) => {
28 | state.device = device
29 | },
30 | SET_SIZE: (state, size) => {
31 | state.size = size
32 | Cookies.set('size', size)
33 | }
34 | }
35 |
36 | const actions = {
37 | toggleSideBar({ commit }) {
38 | commit('TOGGLE_SIDEBAR')
39 | },
40 | closeSideBar({ commit }, { withoutAnimation }) {
41 | commit('CLOSE_SIDEBAR', withoutAnimation)
42 | },
43 | toggleDevice({ commit }, device) {
44 | commit('TOGGLE_DEVICE', device)
45 | },
46 | setSize({ commit }, size) {
47 | commit('SET_SIZE', size)
48 | }
49 | }
50 |
51 | export default {
52 | namespaced: true,
53 | state,
54 | mutations,
55 | actions
56 | }
57 |
--------------------------------------------------------------------------------
/src/components/ImageCropper/utils/effectRipple.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 点击波纹效果
3 | *
4 | * @param {[event]} e [description]
5 | * @param {[Object]} arg_opts [description]
6 | * @return {[bollean]} [description]
7 | */
8 | export default function(e, arg_opts) {
9 | var opts = Object.assign({
10 | ele: e.target, // 波纹作用元素
11 | type: 'hit', // hit点击位置扩散center中心点扩展
12 | bgc: 'rgba(0, 0, 0, 0.15)' // 波纹颜色
13 | }, arg_opts)
14 | var target = opts.ele
15 | if (target) {
16 | var rect = target.getBoundingClientRect()
17 | var ripple = target.querySelector('.e-ripple')
18 | if (!ripple) {
19 | ripple = document.createElement('span')
20 | ripple.className = 'e-ripple'
21 | ripple.style.height = ripple.style.width = Math.max(rect.width, rect.height) + 'px'
22 | target.appendChild(ripple)
23 | } else {
24 | ripple.className = 'e-ripple'
25 | }
26 | switch (opts.type) {
27 | case 'center':
28 | ripple.style.top = (rect.height / 2 - ripple.offsetHeight / 2) + 'px'
29 | ripple.style.left = (rect.width / 2 - ripple.offsetWidth / 2) + 'px'
30 | break
31 | default:
32 | ripple.style.top = (e.pageY - rect.top - ripple.offsetHeight / 2 - document.body.scrollTop) + 'px'
33 | ripple.style.left = (e.pageX - rect.left - ripple.offsetWidth / 2 - document.body.scrollLeft) + 'px'
34 | }
35 | ripple.style.backgroundColor = opts.bgc
36 | ripple.className = 'e-ripple z-active'
37 | return false
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/views/profile/base.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
60 |
--------------------------------------------------------------------------------
/src/styles/element-ui.scss:
--------------------------------------------------------------------------------
1 | // cover some element-ui styles
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 | .cell {
19 | .el-tag {
20 | margin-right: 0px;
21 | }
22 | }
23 |
24 | .small-padding {
25 | .cell {
26 | padding-left: 5px;
27 | padding-right: 5px;
28 | }
29 | }
30 |
31 | .fixed-width {
32 | .el-button--mini {
33 | padding: 7px 10px;
34 | min-width: 60px;
35 | }
36 | }
37 |
38 | .status-col {
39 | .cell {
40 | padding: 0 10px;
41 | text-align: center;
42 |
43 | .el-tag {
44 | margin-right: 0px;
45 | }
46 | }
47 | }
48 |
49 | // to fixed https://github.com/ElemeFE/element/issues/2461
50 | .el-dialog {
51 | transform: none;
52 | left: 0;
53 | position: relative;
54 | margin: 0 auto;
55 | }
56 |
57 | // refine element ui upload
58 | .upload-container {
59 | .el-upload {
60 | width: 100%;
61 |
62 | .el-upload-dragger {
63 | width: 100%;
64 | height: 200px;
65 | }
66 | }
67 | }
68 |
69 | // dropdown
70 | .el-dropdown-menu {
71 | a {
72 | display: block
73 | }
74 | }
75 |
76 | // fix date-picker ui bug in filter-item
77 | .el-range-editor.el-input__inner {
78 | display: inline-flex !important;
79 | }
80 |
81 | // to fix el-date-picker css style
82 | .el-range-separator {
83 | box-sizing: content-box;
84 | }
85 |
--------------------------------------------------------------------------------
/src/icons/svg/bug.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/SvgIcon/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
47 |
48 |
63 |
--------------------------------------------------------------------------------
/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 |
30 | @mixin pct($pct) {
31 | width: #{$pct};
32 | position: relative;
33 | margin: 0 auto;
34 | }
35 |
36 | @mixin triangle($width, $height, $color, $direction) {
37 | $width: $width/2;
38 | $color-border-style: $height solid $color;
39 | $transparent-border-style: $width solid transparent;
40 | height: 0;
41 | width: 0;
42 |
43 | @if $direction==up {
44 | border-bottom: $color-border-style;
45 | border-left: $transparent-border-style;
46 | border-right: $transparent-border-style;
47 | }
48 |
49 | @else if $direction==right {
50 | border-left: $color-border-style;
51 | border-top: $transparent-border-style;
52 | border-bottom: $transparent-border-style;
53 | }
54 |
55 | @else if $direction==down {
56 | border-top: $color-border-style;
57 | border-left: $transparent-border-style;
58 | border-right: $transparent-border-style;
59 | }
60 |
61 | @else if $direction==left {
62 | border-right: $color-border-style;
63 | border-top: $transparent-border-style;
64 | border-bottom: $transparent-border-style;
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/components/SizeSelect/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | {{
9 | item.label }}
10 |
11 |
12 |
13 |
14 |
15 |
58 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 |
3 | import Cookies from 'js-cookie'
4 |
5 | import 'normalize.css/normalize.css' // a modern alternative to CSS resets
6 | // import 'default-passive-events'
7 | import Element from 'element-ui'
8 | import './styles/element-variables.scss'
9 |
10 | import '@/styles/index.scss' // global css
11 |
12 | import App from './App'
13 | import store from './store'
14 | import router from './router'
15 |
16 | import './icons' // icon
17 | import './permission' // permission control
18 | import './utils/error-log' // error log
19 | import * as filters from './filters' // global filters
20 |
21 | import VueForm from '@lljj/vue-json-schema-form'
22 |
23 | import mavonEditor from 'mavon-editor'
24 | import 'mavon-editor/dist/css/index.css'
25 | // use
26 | Vue.use(mavonEditor)
27 |
28 | /**
29 | * If you don't want to use mock-server
30 | * you want to use MockJs for mock api
31 | * you can execute: mockXHR()
32 | *
33 | * Currently MockJs will be used in the production environment,
34 | * please remove it before going online ! ! !
35 | */
36 |
37 | if (process.env.NODE_ENV !== 'production') {
38 | const { mockXHR } = require('../mock')
39 | mockXHR()
40 | }
41 |
42 | Vue.use(Element, {
43 | size: Cookies.get('size') || 'medium' // set element-ui default size
44 | })
45 |
46 | // register global utility filters
47 | Object.keys(filters).forEach(key => {
48 | Vue.filter(key, filters[key])
49 | })
50 |
51 | Vue.component('VueForm', VueForm)
52 |
53 | Vue.config.productionTip = true
54 |
55 | new Vue({
56 | el: '#app',
57 | router,
58 | store,
59 | render: h => h(App)
60 | })
61 |
--------------------------------------------------------------------------------
/src/layout/components/Sidebar/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
55 |
--------------------------------------------------------------------------------
/src/views/profile/components/Account.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | 保存
20 |
21 |
22 |
23 |
24 |
60 |
--------------------------------------------------------------------------------
/src/components/Charts/mixins/resize.js:
--------------------------------------------------------------------------------
1 | import { debounce } from '@/utils'
2 |
3 | export default {
4 | data() {
5 | return {
6 | $_sidebarElm: null,
7 | $_resizeHandler: null
8 | }
9 | },
10 | mounted() {
11 | this.initListener()
12 | },
13 | activated() {
14 | if (!this.$_resizeHandler) {
15 | // avoid duplication init
16 | this.initListener()
17 | }
18 |
19 | // when keep-alive chart activated, auto resize
20 | this.resize()
21 | },
22 | beforeDestroy() {
23 | this.destroyListener()
24 | },
25 | deactivated() {
26 | this.destroyListener()
27 | },
28 | methods: {
29 | // use $_ for mixins properties
30 | // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential
31 | $_sidebarResizeHandler(e) {
32 | if (e.propertyName === 'width') {
33 | this.$_resizeHandler()
34 | }
35 | },
36 | initListener() {
37 | this.$_resizeHandler = debounce(() => {
38 | this.resize()
39 | }, 100)
40 | window.addEventListener('resize', this.$_resizeHandler)
41 |
42 | this.$_sidebarElm = document.getElementsByClassName('sidebar-container')[0]
43 | this.$_sidebarElm && this.$_sidebarElm.addEventListener('transitionend', this.$_sidebarResizeHandler)
44 | },
45 | destroyListener() {
46 | window.removeEventListener('resize', this.$_resizeHandler)
47 | this.$_resizeHandler = null
48 |
49 | this.$_sidebarElm && this.$_sidebarElm.removeEventListener('transitionend', this.$_sidebarResizeHandler)
50 | },
51 | resize() {
52 | const { chart } = this
53 | chart && chart.resize()
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/icons/svg/pdf.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/DragSelect/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
50 |
51 |
66 |
--------------------------------------------------------------------------------
/src/store/modules/permission.js:
--------------------------------------------------------------------------------
1 | import { asyncRoutes, constantRoutes } from '@/router'
2 |
3 | /**
4 | * Use meta.role to determine if the current user has permission
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 | * Filter asynchronous routing tables by recursion
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 | generateRoutes({ commit }, roles) {
51 | return new Promise(resolve => {
52 | let accessedRoutes
53 | if (roles.includes('admin')) {
54 | accessedRoutes = asyncRoutes || []
55 | } else {
56 | accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
57 | }
58 | commit('SET_ROUTES', accessedRoutes)
59 | resolve(accessedRoutes)
60 | })
61 | }
62 | }
63 |
64 | export default {
65 | namespaced: true,
66 | state,
67 | mutations,
68 | actions
69 | }
70 |
--------------------------------------------------------------------------------
/src/directive/clipboard/clipboard.js:
--------------------------------------------------------------------------------
1 | // Inspired by https://github.com/Inndy/vue-clipboard2
2 | const Clipboard = require('clipboard')
3 | if (!Clipboard) {
4 | throw new Error('you should npm install `clipboard` --save at first ')
5 | }
6 |
7 | export default {
8 | bind(el, binding) {
9 | if (binding.arg === 'success') {
10 | el._v_clipboard_success = binding.value
11 | } else if (binding.arg === 'error') {
12 | el._v_clipboard_error = binding.value
13 | } else {
14 | const clipboard = new Clipboard(el, {
15 | text() { return binding.value },
16 | action() { return binding.arg === 'cut' ? 'cut' : 'copy' }
17 | })
18 | clipboard.on('success', e => {
19 | const callback = el._v_clipboard_success
20 | callback && callback(e) // eslint-disable-line
21 | })
22 | clipboard.on('error', e => {
23 | const callback = el._v_clipboard_error
24 | callback && callback(e) // eslint-disable-line
25 | })
26 | el._v_clipboard = clipboard
27 | }
28 | },
29 | update(el, binding) {
30 | if (binding.arg === 'success') {
31 | el._v_clipboard_success = binding.value
32 | } else if (binding.arg === 'error') {
33 | el._v_clipboard_error = binding.value
34 | } else {
35 | el._v_clipboard.text = function() { return binding.value }
36 | el._v_clipboard.action = function() { return binding.arg === 'cut' ? 'cut' : 'copy' }
37 | }
38 | },
39 | unbind(el, binding) {
40 | if (binding.arg === 'success') {
41 | delete el._v_clipboard_success
42 | } else if (binding.arg === 'error') {
43 | delete el._v_clipboard_error
44 | } else {
45 | el._v_clipboard.destroy()
46 | delete el._v_clipboard
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/icons/svg/exit-fullscreen.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/svg/tree.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/views/dashboard/components/mixins/resize.js:
--------------------------------------------------------------------------------
1 | import { debounce } from '@/utils'
2 |
3 | export default {
4 | data() {
5 | return {
6 | $_sidebarElm: null,
7 | $_resizeHandler: null
8 | }
9 | },
10 | mounted() {
11 | this.$_resizeHandler = debounce(() => {
12 | if (this.chart) {
13 | this.chart.resize()
14 | }
15 | }, 100)
16 | this.$_initResizeEvent()
17 | this.$_initSidebarResizeEvent()
18 | },
19 | beforeDestroy() {
20 | this.$_destroyResizeEvent()
21 | this.$_destroySidebarResizeEvent()
22 | },
23 | // to fixed bug when cached by keep-alive
24 | // https://github.com/PanJiaChen/vue-element-admin/issues/2116
25 | activated() {
26 | this.$_initResizeEvent()
27 | this.$_initSidebarResizeEvent()
28 | },
29 | deactivated() {
30 | this.$_destroyResizeEvent()
31 | this.$_destroySidebarResizeEvent()
32 | },
33 | methods: {
34 | // use $_ for mixins properties
35 | // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential
36 | $_initResizeEvent() {
37 | window.addEventListener('resize', this.$_resizeHandler)
38 | },
39 | $_destroyResizeEvent() {
40 | window.removeEventListener('resize', this.$_resizeHandler)
41 | },
42 | $_sidebarResizeHandler(e) {
43 | if (e.propertyName === 'width') {
44 | this.$_resizeHandler()
45 | }
46 | },
47 | $_initSidebarResizeEvent() {
48 | this.$_sidebarElm = document.getElementsByClassName('sidebar-container')[0]
49 | this.$_sidebarElm && this.$_sidebarElm.addEventListener('transitionend', this.$_sidebarResizeHandler)
50 | },
51 | $_destroySidebarResizeEvent() {
52 | this.$_sidebarElm && this.$_sidebarElm.removeEventListener('transitionend', this.$_sidebarResizeHandler)
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/api/article.js:
--------------------------------------------------------------------------------
1 | import request from '@/utils/request'
2 |
3 | // 所有文章列表
4 | /**
5 | * 分页获取文章列表
6 | * @param query 分页查询参数对象
7 | */
8 | export function fetchList(query) {
9 | return request({
10 | url: '/article/getArticlesByPage',
11 | method: 'post',
12 | data: query
13 | })
14 | }
15 |
16 | // 根据文章ID查询文章信息
17 | export function fetchArticle(id) {
18 | return request({
19 | url: '/article/getArticleById',
20 | method: 'post',
21 | data: {
22 | id
23 | }
24 | })
25 | }
26 |
27 | // 获取所有分类访问量
28 | export function getAllPVByType() {
29 | return request({
30 | url: '/article/getAllPVByType',
31 | method: 'post'
32 | })
33 | }
34 |
35 | // 获取文章创建历史
36 | export function getArticleCreateLog(token) {
37 | return request({
38 | url: '/article/getArticleCreateLog',
39 | method: 'post',
40 | data: {
41 | token
42 | }
43 | })
44 | }
45 |
46 | // 创建文章
47 | export function createArticle(token, article) {
48 | return request({
49 | url: '/article/createArticle',
50 | method: 'post',
51 | data: {
52 | token,
53 | content: article
54 | }
55 | })
56 | }
57 |
58 | // 更新文章
59 | export function updateArticle(article) {
60 | return request({
61 | url: '/article/updateArticle',
62 | method: 'post',
63 | data: {
64 | content: article
65 | }
66 | })
67 | }
68 |
69 | // 设置文章状态
70 | export function setStatus(articleId, status) {
71 | return request({
72 | url: '/article/setStatus',
73 | method: 'post',
74 | data: {
75 | id: articleId,
76 | content: status
77 | }
78 | })
79 | }
80 |
81 | // 删除文章
82 | export function delArticle(articleId) {
83 | return request({
84 | url: '/article/delArticle',
85 | method: 'post',
86 | data: {
87 | id: articleId
88 | }
89 | })
90 | }
91 |
--------------------------------------------------------------------------------
/src/components/Tinymce/dynamicLoadScript.js:
--------------------------------------------------------------------------------
1 | let callbacks = []
2 |
3 | function loadedTinymce() {
4 | // to fixed https://github.com/PanJiaChen/vue-element-admin/issues/2144
5 | // check is successfully downloaded script
6 | return window.tinymce
7 | }
8 |
9 | const dynamicLoadScript = (src, callback) => {
10 | const existingScript = document.getElementById(src)
11 | const cb = callback || function() {}
12 |
13 | if (!existingScript) {
14 | const script = document.createElement('script')
15 | script.src = src // src url for the third-party library being loaded.
16 | script.id = src
17 | document.body.appendChild(script)
18 | callbacks.push(cb)
19 | const onEnd = 'onload' in script ? stdOnEnd : ieOnEnd
20 | onEnd(script)
21 | }
22 |
23 | if (existingScript && cb) {
24 | if (loadedTinymce()) {
25 | cb(null, existingScript)
26 | } else {
27 | callbacks.push(cb)
28 | }
29 | }
30 |
31 | function stdOnEnd(script) {
32 | script.onload = function() {
33 | // this.onload = null here is necessary
34 | // because even IE9 works not like others
35 | this.onerror = this.onload = null
36 | for (const cb of callbacks) {
37 | cb(null, script)
38 | }
39 | callbacks = null
40 | }
41 | script.onerror = function() {
42 | this.onerror = this.onload = null
43 | cb(new Error('Failed to load ' + src), script)
44 | }
45 | }
46 |
47 | function ieOnEnd(script) {
48 | script.onreadystatechange = function() {
49 | if (this.readyState !== 'complete' && this.readyState !== 'loaded') return
50 | this.onreadystatechange = null
51 | for (const cb of callbacks) {
52 | cb(null, script) // there is no way to catch loading errors in IE8
53 | }
54 | callbacks = null
55 | }
56 | }
57 | }
58 |
59 | export default dynamicLoadScript
60 |
--------------------------------------------------------------------------------
/src/filters/index.js:
--------------------------------------------------------------------------------
1 | // import parseTime, formatTime and set to filter
2 | export { parseTime, formatTime } from '@/utils'
3 |
4 | /**
5 | * Show plural label if time is plural number
6 | * @param {number} time
7 | * @param {string} label
8 | * @return {string}
9 | */
10 | function pluralize(time, label) {
11 | if (time === 1) {
12 | return time + label
13 | }
14 | return time + label + 's'
15 | }
16 |
17 | /**
18 | * @param {number} time
19 | */
20 | export function timeAgo(time) {
21 | const between = Date.now() / 1000 - Number(time)
22 | if (between < 3600) {
23 | return pluralize(~~(between / 60), ' minute')
24 | } else if (between < 86400) {
25 | return pluralize(~~(between / 3600), ' hour')
26 | } else {
27 | return pluralize(~~(between / 86400), ' day')
28 | }
29 | }
30 |
31 | /**
32 | * Number formatting
33 | * like 10000 => 10k
34 | * @param {number} num
35 | * @param {number} digits
36 | */
37 | export function numberFormatter(num, digits) {
38 | const si = [
39 | { value: 1E18, symbol: 'E' },
40 | { value: 1E15, symbol: 'P' },
41 | { value: 1E12, symbol: 'T' },
42 | { value: 1E9, symbol: 'G' },
43 | { value: 1E6, symbol: 'M' },
44 | { value: 1E3, symbol: 'k' }
45 | ]
46 | for (let i = 0; i < si.length; i++) {
47 | if (num >= si[i].value) {
48 | return (num / si[i].value).toFixed(digits).replace(/\.0+$|(\.[0-9]*[1-9])0+$/, '$1') + si[i].symbol
49 | }
50 | }
51 | return num.toString()
52 | }
53 |
54 | /**
55 | * 10000 => "10,000"
56 | * @param {number} num
57 | */
58 | export function toThousandFilter(num) {
59 | return (+num || 0).toString().replace(/^-?\d+/g, m => m.replace(/(?=(?!\b)(\d{3})+$)/g, ','))
60 | }
61 |
62 | /**
63 | * Upper case first char
64 | * @param {String} string
65 | */
66 | export function uppercaseFirst(string) {
67 | return string.charAt(0).toUpperCase() + string.slice(1)
68 | }
69 |
--------------------------------------------------------------------------------
/src/api/sys.js:
--------------------------------------------------------------------------------
1 | import request from '@/utils/request'
2 |
3 | /**
4 | * 获取系统存储配置
5 | */
6 | export function getStorageConfig() {
7 | return request({
8 | url: '/sys/getStorageConfig',
9 | method: 'post'
10 | })
11 | }
12 |
13 | /**
14 | * 设置系统存储配置
15 | * @param {Object} storage 存储配置Json对象
16 | */
17 | export function setStorageConfig(storage) {
18 | return request({
19 | url: '/sys/setStorageConfig',
20 | method: 'post',
21 | data: {
22 | content: storage
23 | }
24 | })
25 | }
26 |
27 | /**
28 | * 获取Gitee登录配置,包含表单描述信息
29 | */
30 | export function getGiteeConfig() {
31 | return request({
32 | url: '/sys/getGiteeConfig',
33 | method: 'post'
34 | })
35 | }
36 |
37 | /**
38 | * 设置Gitee登录配置
39 | */
40 | export function setGiteeConfig(config) {
41 | return request({
42 | url: '/sys/setGiteeConfig',
43 | method: 'post',
44 | data: config
45 | })
46 | }
47 |
48 | /**
49 | * 获取系统基础配置,包含表单描述信息
50 | */
51 | export function getFixedConfig() {
52 | return request({
53 | url: '/sys/getFixedConfig',
54 | method: 'post'
55 | })
56 | }
57 |
58 | /**
59 | * 上传系统配置
60 | * @param {Object} config 系统设置信息
61 | */
62 | export function setFixedConfig(config) {
63 | return request({
64 | url: '/sys/setFixedConfig',
65 | method: 'post',
66 | data: {
67 | content: config
68 | }
69 | })
70 | }
71 |
72 | /**
73 | * 获取用户Ui配置信息
74 | * @param {int} id 用户ID
75 | */
76 | export function getUiConfig(id) {
77 | return request({
78 | url: '/sys/getUiConfig',
79 | method: 'post',
80 | data: { id }
81 | })
82 | }
83 |
84 | /**
85 | * 设置UI
86 | * @param {String} token 验证信息
87 | * @param {Object} data UI数据
88 | */
89 | export function setUiConfig(token, data) {
90 | return request({
91 | url: '/sys/setUiConfig',
92 | method: 'post',
93 | data: {
94 | token: token,
95 | content: data
96 | }
97 | })
98 | }
99 |
--------------------------------------------------------------------------------
/src/styles/btn.scss:
--------------------------------------------------------------------------------
1 | @import './variables.scss';
2 |
3 | @mixin colorBtn($color) {
4 | background: $color;
5 |
6 | &:hover {
7 | color: $color;
8 |
9 | &:before,
10 | &:after {
11 | background: $color;
12 | }
13 | }
14 | }
15 |
16 | .blue-btn {
17 | @include colorBtn($blue)
18 | }
19 |
20 | .light-blue-btn {
21 | @include colorBtn($light-blue)
22 | }
23 |
24 | .red-btn {
25 | @include colorBtn($red)
26 | }
27 |
28 | .pink-btn {
29 | @include colorBtn($pink)
30 | }
31 |
32 | .green-btn {
33 | @include colorBtn($green)
34 | }
35 |
36 | .tiffany-btn {
37 | @include colorBtn($tiffany)
38 | }
39 |
40 | .yellow-btn {
41 | @include colorBtn($yellow)
42 | }
43 |
44 | .pan-btn {
45 | font-size: 14px;
46 | color: #fff;
47 | padding: 14px 36px;
48 | border-radius: 8px;
49 | border: none;
50 | outline: none;
51 | transition: 600ms ease all;
52 | position: relative;
53 | display: inline-block;
54 |
55 | &:hover {
56 | background: #fff;
57 |
58 | &:before,
59 | &:after {
60 | width: 100%;
61 | transition: 600ms ease all;
62 | }
63 | }
64 |
65 | &:before,
66 | &:after {
67 | content: '';
68 | position: absolute;
69 | top: 0;
70 | right: 0;
71 | height: 2px;
72 | width: 0;
73 | transition: 400ms ease all;
74 | }
75 |
76 | &::after {
77 | right: inherit;
78 | top: inherit;
79 | left: 0;
80 | bottom: 0;
81 | }
82 | }
83 |
84 | .custom-button {
85 | display: inline-block;
86 | line-height: 1;
87 | white-space: nowrap;
88 | cursor: pointer;
89 | background: #fff;
90 | color: #fff;
91 | -webkit-appearance: none;
92 | text-align: center;
93 | box-sizing: border-box;
94 | outline: 0;
95 | margin: 0;
96 | padding: 10px 15px;
97 | font-size: 14px;
98 | border-radius: 4px;
99 | }
100 |
--------------------------------------------------------------------------------
/src/utils/scroll-to.js:
--------------------------------------------------------------------------------
1 | Math.easeInOutQuad = function(t, b, c, d) {
2 | t /= d / 2
3 | if (t < 1) {
4 | return c / 2 * t * t + b
5 | }
6 | t--
7 | return -c / 2 * (t * (t - 2) - 1) + b
8 | }
9 |
10 | // requestAnimationFrame for Smart Animating http://goo.gl/sx5sts
11 | var requestAnimFrame = (function() {
12 | return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function(callback) { window.setTimeout(callback, 1000 / 60) }
13 | })()
14 |
15 | /**
16 | * Because it's so fucking difficult to detect the scrolling element, just move them all
17 | * @param {number} amount
18 | */
19 | function move(amount) {
20 | document.documentElement.scrollTop = amount
21 | document.body.parentNode.scrollTop = amount
22 | document.body.scrollTop = amount
23 | }
24 |
25 | function position() {
26 | return document.documentElement.scrollTop || document.body.parentNode.scrollTop || document.body.scrollTop
27 | }
28 |
29 | /**
30 | * @param {number} to
31 | * @param {number} duration
32 | * @param {Function} callback
33 | */
34 | export function scrollTo(to, duration, callback) {
35 | const start = position()
36 | const change = to - start
37 | const increment = 20
38 | let currentTime = 0
39 | duration = (typeof (duration) === 'undefined') ? 500 : duration
40 | var animateScroll = function() {
41 | // increment the time
42 | currentTime += increment
43 | // find the value with the quadratic in-out easing function
44 | var val = Math.easeInOutQuad(currentTime, start, change, duration)
45 | // move the document.body
46 | move(val)
47 | // do the animation unless its over
48 | if (currentTime < duration) {
49 | requestAnimFrame(animateScroll)
50 | } else {
51 | if (callback && typeof (callback) === 'function') {
52 | // the animation is done so lets callback
53 | callback()
54 | }
55 | }
56 | }
57 | animateScroll()
58 | }
59 |
--------------------------------------------------------------------------------
/src/views/dashboard/components/TodoList/Todo.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
13 |
22 |
23 |
24 |
25 |
82 |
--------------------------------------------------------------------------------
/src/components/GithubCorner/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
23 |
24 |
25 |
26 |
55 |
--------------------------------------------------------------------------------
/mock/index.js:
--------------------------------------------------------------------------------
1 | const Mock = require('mockjs')
2 | const {param2Obj} = require('./utils')
3 |
4 | const user = require('./user')
5 | const role = require('./role')
6 | const article = require('./article')
7 | const sysConfig = require('./sys')
8 | const fileConfig = require('./file')
9 | const link = require('./link')
10 | const comment = require('./comment')
11 | const other = require('./other')
12 | const articleLabel = require('./article-label')
13 | const mocks = [
14 | ...user,
15 | ...role,
16 | ...article,
17 | ...sysConfig,
18 | ...fileConfig,
19 | ...link,
20 | ...comment,
21 | ...other,
22 | ...articleLabel
23 | ]
24 |
25 | // for front mock
26 | // please use it cautiously, it will redefine XMLHttpRequest,
27 | // which will cause many of your third-party libraries to be invalidated(like progress event).
28 | function mockXHR() {
29 | // mock patch
30 | // https://github.com/nuysoft/Mock/issues/300
31 | Mock.XHR.prototype.proxy_send = Mock.XHR.prototype.send
32 | Mock.XHR.prototype.send = function () {
33 | if (this.custom.xhr) {
34 | this.custom.xhr.withCredentials = this.withCredentials || false
35 |
36 | if (this.responseType) {
37 | this.custom.xhr.responseType = this.responseType
38 | }
39 | }
40 | this.proxy_send(...arguments)
41 | }
42 |
43 | function XHR2ExpressReqWrap(respond) {
44 | return function (options) {
45 | let result = null
46 | if (respond instanceof Function) {
47 | const {body, type, url} = options
48 | // https://expressjs.com/en/4x/api.html#req
49 | result = respond({
50 | method: type,
51 | body: JSON.parse(body),
52 | query: param2Obj(url)
53 | })
54 | } else {
55 | result = respond
56 | }
57 | return Mock.mock(result)
58 | }
59 | }
60 |
61 | for (const i of mocks) {
62 | Mock.mock(new RegExp(i.url), i.type || 'get', XHR2ExpressReqWrap(i.response))
63 | }
64 | }
65 |
66 | module.exports = {
67 | mocks,
68 | mockXHR
69 | }
70 |
--------------------------------------------------------------------------------
/src/views/dashboard/components/PieChart.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
87 |
--------------------------------------------------------------------------------
/src/layout/components/Sidebar/Logo.vue:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
16 |
33 |
34 |
83 |
--------------------------------------------------------------------------------
/src/icons/svg/shopping.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/svg/dashboard.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/mock/role/index.js:
--------------------------------------------------------------------------------
1 | const Mock = require('mockjs')
2 | const { deepClone } = require('../utils')
3 | const { asyncRoutes, constantRoutes } = require('./routes.js')
4 |
5 | const routes = deepClone([...constantRoutes, ...asyncRoutes])
6 |
7 | const roles = [
8 | {
9 | key: 'admin',
10 | name: 'admin',
11 | description: 'Super Administrator. Have access to view all pages.',
12 | routes: routes
13 | },
14 | {
15 | key: 'editor',
16 | name: 'editor',
17 | description: 'Normal Editor. Can see all pages except permission page',
18 | routes: routes.filter(i => i.path !== '/permission')// just a mock
19 | },
20 | {
21 | key: 'visitor',
22 | name: 'visitor',
23 | description: 'Just a visitor. Can only see the home page and the document page',
24 | routes: [{
25 | path: '',
26 | redirect: 'dashboard',
27 | children: [
28 | {
29 | path: 'dashboard',
30 | name: 'Dashboard',
31 | meta: { title: 'dashboard', icon: 'dashboard' }
32 | }
33 | ]
34 | }]
35 | }
36 | ]
37 |
38 | module.exports = [
39 | // mock get all routes form server
40 | {
41 | url: '/vue-element-admin/routes',
42 | type: 'get',
43 | response: _ => {
44 | return {
45 | code: 20000,
46 | data: routes
47 | }
48 | }
49 | },
50 |
51 | // mock get all roles form server
52 | {
53 | url: '/vue-element-admin/roles',
54 | type: 'get',
55 | response: _ => {
56 | return {
57 | code: 20000,
58 | data: roles
59 | }
60 | }
61 | },
62 |
63 | // add role
64 | {
65 | url: '/vue-element-admin/role',
66 | type: 'post',
67 | response: {
68 | code: 20000,
69 | data: {
70 | key: Mock.mock('@integer(300, 5000)')
71 | }
72 | }
73 | },
74 |
75 | // update role
76 | {
77 | url: '/vue-element-admin/role/[A-Za-z0-9]',
78 | type: 'put',
79 | response: {
80 | code: 20000,
81 | data: {
82 | status: 'success'
83 | }
84 | }
85 | },
86 |
87 | // delete role
88 | {
89 | url: '/vue-element-admin/role/[A-Za-z0-9]',
90 | type: 'delete',
91 | response: {
92 | code: 20000,
93 | data: {
94 | status: 'success'
95 | }
96 | }
97 | }
98 | ]
99 |
--------------------------------------------------------------------------------
/src/utils/validate.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by PanJiaChen on 16/11/18.
3 | */
4 |
5 | /**
6 | * @param {string} path
7 | * @returns {Boolean}
8 | */
9 | export function isExternal(path) {
10 | return /^(https?:|mailto:|tel:)/.test(path)
11 | }
12 |
13 | /**
14 | * @param {string} str
15 | * @returns {Boolean}
16 | */
17 | export function validUsername(str) {
18 | const valid_map = ['admin', 'editor']
19 | return valid_map.indexOf(str.trim()) >= 0
20 | }
21 |
22 | /**
23 | * @param {string} url
24 | * @returns {Boolean}
25 | */
26 | export function validURL(url) {
27 | const reg = /^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/
28 | return reg.test(url)
29 | }
30 |
31 | /**
32 | * @param {string} str
33 | * @returns {Boolean}
34 | */
35 | export function validLowerCase(str) {
36 | const reg = /^[a-z]+$/
37 | return reg.test(str)
38 | }
39 |
40 | /**
41 | * @param {string} str
42 | * @returns {Boolean}
43 | */
44 | export function validUpperCase(str) {
45 | const reg = /^[A-Z]+$/
46 | return reg.test(str)
47 | }
48 |
49 | /**
50 | * @param {string} str
51 | * @returns {Boolean}
52 | */
53 | export function validAlphabets(str) {
54 | const reg = /^[A-Za-z]+$/
55 | return reg.test(str)
56 | }
57 |
58 | /**
59 | * @param {string} email
60 | * @returns {Boolean}
61 | */
62 | export function validEmail(email) {
63 | const reg = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
64 | return reg.test(email)
65 | }
66 |
67 | /**
68 | * @param {string} str
69 | * @returns {Boolean}
70 | */
71 | export function isString(str) {
72 | if (typeof str === 'string' || str instanceof String) {
73 | return true
74 | }
75 | return false
76 | }
77 |
78 | /**
79 | * @param {Array} arg
80 | * @returns {Boolean}
81 | */
82 | export function isArray(arg) {
83 | if (typeof Array.isArray === 'undefined') {
84 | return Object.prototype.toString.call(arg) === '[object Array]'
85 | }
86 | return Array.isArray(arg)
87 | }
88 |
--------------------------------------------------------------------------------
/src/components/MarkdownEditor/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
77 |
78 |
84 |
--------------------------------------------------------------------------------
/src/components/Sticky/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 |
92 |
--------------------------------------------------------------------------------
/src/api/user.js:
--------------------------------------------------------------------------------
1 | import request from '@/utils/request'
2 |
3 | /**
4 | * 登录,返回验证信息
5 | * @param {Object} data 行号密码
6 | */
7 | export function login(data) {
8 | return request({
9 | url: '/user/login',
10 | method: 'post',
11 | data
12 | })
13 | }
14 |
15 | /**
16 | * 获取用户信息
17 | * @param {Object} token 验证信息
18 | */
19 | export function getInfo(token) {
20 | const data = { 'token': token }
21 | return request({
22 | url: '/user/getInfo',
23 | method: 'post',
24 | data
25 | })
26 | }
27 |
28 | /**
29 | * 设置个人信息
30 | * @param {Object} token 验证信息
31 | * @param {Object} user 新的用户信息对象
32 | */
33 | export function setInfo(token, user) {
34 | return request({
35 | url: '/user/setInfo',
36 | method: 'post',
37 | data: { token: token, content: user }
38 | })
39 | }
40 |
41 | /**
42 | * 登出
43 | */
44 | export function logout(token) {
45 | return request({
46 | url: '/user/logout',
47 | method: 'post',
48 | data: { token }
49 | })
50 | }
51 |
52 | /**
53 | * 获取最近活动记录
54 | * @param {Object} token 验证信息
55 | */
56 | export function getActivity(token) {
57 | return request({
58 | url: '/user/getActivity',
59 | method: 'post',
60 | data: { token }
61 | })
62 | }
63 |
64 | /**
65 | * 获取工作概览
66 | * @param {Object} token 验证信息
67 | */
68 | export function getWork(token) {
69 | return request({
70 | url: '/user/getWork',
71 | method: 'post',
72 | data: { token }
73 | })
74 | }
75 |
76 | /**
77 | * 设置头像
78 | * @param {Object} token 验证信息
79 | * @param {String} avatar 头像链接
80 | */
81 | export function setAvatar(token, avatar) {
82 | return request({
83 | url: '/user/setAvatar',
84 | method: 'post',
85 | data: {
86 | token,
87 | content: avatar
88 | }
89 | })
90 | }
91 |
92 | /**
93 | * 获取用户关于信息
94 | * @param {int} id 用户ID
95 | */
96 | export function getAboutByUserId(id) {
97 | return request({
98 | url: '/user/getAboutByUserId',
99 | method: 'post',
100 | data: { id }
101 | })
102 | }
103 |
104 | /**
105 | * 获取用户关于信息
106 | * @param token 验证信息
107 | * @param content 关于内容
108 | */
109 | export function setAboutByUserToken(token, content) {
110 | return request({
111 | url: '/user/setAboutByUserToken',
112 | method: 'post',
113 | data: { token, content }
114 | })
115 | }
116 |
--------------------------------------------------------------------------------
/mock/file.js:
--------------------------------------------------------------------------------
1 | module.exports = [
2 | // 获取头像上传的Url,含上传后的文件名
3 | // {url: 上传签名Url, GetUrl: Get请求Url, method: 上传方式}
4 | {
5 | url: '/file/getUploadAvatarUrl',
6 | type: 'post',
7 | response: _ => {
8 | return {
9 | code: 20000,
10 | data: {
11 | urlParams: {
12 | key: 'cece089d-dd3b-11eb-a1d2-1f59eeaa999a',
13 | policy: '',
14 | success_action_status: '200',
15 | OSSAccessKeyId: '',
16 | signature: ''
17 | },
18 | GetUrl: 'https://wpimg.wallstcn.com/577965b9-bb9e-4e02-9f0c-095b41417191',
19 | host: 'http://localhost:9527/dev-api/file/uploadData'
20 | }
21 | }
22 | }
23 | },
24 | // 获取上传文章封面Url
25 | {
26 | url: '/file/getUploadArticleCoverImageUrl',
27 | type: 'post',
28 | response: _ => {
29 | return {
30 | code: 20000,
31 | data: {
32 | urlParams: {
33 | key: 'cece089d-dd3b-11eb-a1d2-1f59eeaa999a',
34 | policy: '',
35 | success_action_status: '200',
36 | OSSAccessKeyId: '',
37 | signature: ''
38 | },
39 | GetUrl: 'https://wpimg.wallstcn.com/577965b9-bb9e-4e02-9f0c-095b41417191',
40 | host: 'http://localhost:9527/dev-api/file/uploadData'
41 | }
42 | }
43 | }
44 | },
45 | // 获取上传文章内容图片的Url
46 | {
47 | url: '/file/getUploadArticleImageUrl',
48 | type: 'post',
49 | response: _ => {
50 | return {
51 | code: 20000,
52 | data: {
53 | urlParams: {
54 | key: 'cece089d-dd3b-11eb-a1d2-1f59eeaa999a',
55 | policy: '',
56 | success_action_status: '200',
57 | OSSAccessKeyId: '',
58 | signature: ''
59 | },
60 | GetUrl: 'https://wpimg.wallstcn.com/577965b9-bb9e-4e02-9f0c-095b41417191',
61 | host: 'http://localhost:9527/dev-api/file/uploadData'
62 | }
63 | }
64 | }
65 | },
66 | // 上传文件
67 | {
68 | url: '/file/uploadData',
69 | type: 'post',
70 | response: _ => {
71 | return {
72 | code: 20000,
73 | data: 'OK'
74 | }
75 | }
76 | },
77 | // 删除文件
78 | {
79 | url: '/file/deleteObject',
80 | type: 'post',
81 | response: _ => {
82 | return {
83 | code: 20000,
84 | data: 'OK'
85 | }
86 | }
87 | }
88 | ]
89 |
--------------------------------------------------------------------------------
/src/icons/svg/form.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/Kanban/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ headerText }}
5 |
6 |
12 |
13 | {{ element.name }} {{ element.id }}
14 |
15 |
16 |
17 |
18 |
19 |
54 |
99 |
100 |
--------------------------------------------------------------------------------
/src/components/Pagination/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
15 |
16 |
17 |
92 |
93 |
102 |
--------------------------------------------------------------------------------
/src/directive/waves/waves.js:
--------------------------------------------------------------------------------
1 | import './waves.css'
2 |
3 | const context = '@@wavesContext'
4 |
5 | function handleClick(el, binding) {
6 | function handle(e) {
7 | const customOpts = Object.assign({}, binding.value)
8 | const opts = Object.assign({
9 | ele: el, // 波纹作用元素
10 | type: 'hit', // hit 点击位置扩散 center中心点扩展
11 | color: 'rgba(0, 0, 0, 0.15)' // 波纹颜色
12 | },
13 | customOpts
14 | )
15 | const target = opts.ele
16 | if (target) {
17 | target.style.position = 'relative'
18 | target.style.overflow = 'hidden'
19 | const rect = target.getBoundingClientRect()
20 | let ripple = target.querySelector('.waves-ripple')
21 | if (!ripple) {
22 | ripple = document.createElement('span')
23 | ripple.className = 'waves-ripple'
24 | ripple.style.height = ripple.style.width = Math.max(rect.width, rect.height) + 'px'
25 | target.appendChild(ripple)
26 | } else {
27 | ripple.className = 'waves-ripple'
28 | }
29 | switch (opts.type) {
30 | case 'center':
31 | ripple.style.top = rect.height / 2 - ripple.offsetHeight / 2 + 'px'
32 | ripple.style.left = rect.width / 2 - ripple.offsetWidth / 2 + 'px'
33 | break
34 | default:
35 | ripple.style.top =
36 | (e.pageY - rect.top - ripple.offsetHeight / 2 - document.documentElement.scrollTop ||
37 | document.body.scrollTop) + 'px'
38 | ripple.style.left =
39 | (e.pageX - rect.left - ripple.offsetWidth / 2 - document.documentElement.scrollLeft ||
40 | document.body.scrollLeft) + 'px'
41 | }
42 | ripple.style.backgroundColor = opts.color
43 | ripple.className = 'waves-ripple z-active'
44 | return false
45 | }
46 | }
47 |
48 | if (!el[context]) {
49 | el[context] = {
50 | removeHandle: handle
51 | }
52 | } else {
53 | el[context].removeHandle = handle
54 | }
55 |
56 | return handle
57 | }
58 |
59 | export default {
60 | bind(el, binding) {
61 | el.addEventListener('click', handleClick(el, binding), false)
62 | },
63 | update(el, binding) {
64 | el.removeEventListener('click', el[context].removeHandle, false)
65 | el.addEventListener('click', handleClick(el, binding), false)
66 | },
67 | unbind(el) {
68 | el.removeEventListener('click', el[context].removeHandle, false)
69 | el[context] = null
70 | delete el[context]
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/components/ErrorLog/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | Error Log
12 | Clear All
13 |
14 |
15 |
16 |
17 |
18 | Msg:
19 |
20 | {{ row.err.message }}
21 |
22 |
23 |
24 |
25 | Info:
26 |
27 | {{ row.vm.$vnode.tag }} error in {{ row.info }}
28 |
29 |
30 |
31 |
32 | Url:
33 |
34 | {{ row.url }}
35 |
36 |
37 |
38 |
39 |
40 |
41 | {{ scope.row.err.stack }}
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
70 |
71 |
79 |
--------------------------------------------------------------------------------
/src/components/Breadcrumb/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ item.meta.title }}
6 | {{ item.meta.title }}
7 |
8 |
9 |
10 |
11 |
12 |
69 |
70 |
83 |
--------------------------------------------------------------------------------
/src/views/setting/sys.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
15 |
16 |
17 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
90 |
91 |
97 |
--------------------------------------------------------------------------------
/src/views/setting/ui.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | 保存
23 |
24 |
25 |
26 |
27 |
28 |
29 |
81 |
--------------------------------------------------------------------------------
/src/views/dashboard/components/CommentTable.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ scope.row.content }}
6 |
7 |
8 |
9 |
10 | {{ scope.row.fromUser.nickname }}
11 |
12 |
13 |
14 |
15 | {{ scope.row.createTime | parseTime('{y}-{m}-{d} {h}:{i}') }}
16 |
17 |
18 |
19 |
20 |
25 | 通过
26 |
27 |
32 | 删除
33 |
34 |
35 |
36 |
37 |
38 |
39 |
89 |
--------------------------------------------------------------------------------
/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 | const Mock = require('mockjs')
6 |
7 | const mockDir = path.join(process.cwd(), 'mock')
8 |
9 | function registerRoutes(app) {
10 | let mockLastIndex
11 | const { mocks } = require('./index.js')
12 | const mocksForServer = mocks.map(route => {
13 | return responseFake(route.url, route.type, route.response)
14 | })
15 | for (const mock of mocksForServer) {
16 | app[mock.type](mock.url, mock.response)
17 | mockLastIndex = app._router.stack.length
18 | }
19 | const mockRoutesLength = Object.keys(mocksForServer).length
20 | return {
21 | mockRoutesLength: mockRoutesLength,
22 | mockStartIndex: mockLastIndex - mockRoutesLength
23 | }
24 | }
25 |
26 | function unregisterRoutes() {
27 | Object.keys(require.cache).forEach(i => {
28 | if (i.includes(mockDir)) {
29 | delete require.cache[require.resolve(i)]
30 | }
31 | })
32 | }
33 |
34 | // for mock server
35 | const responseFake = (url, type, respond) => {
36 | return {
37 | url: new RegExp(`${process.env.VUE_APP_BASE_API}${url}`),
38 | type: type || 'get',
39 | response(req, res) {
40 | console.log('request invoke:' + req.path)
41 | res.json(Mock.mock(respond instanceof Function ? respond(req, res) : respond))
42 | }
43 | }
44 | }
45 |
46 | module.exports = app => {
47 | // parse app.body
48 | // https://expressjs.com/en/4x/api.html#req.body
49 | app.use(bodyParser.json())
50 | app.use(bodyParser.urlencoded({
51 | extended: true
52 | }))
53 |
54 | const mockRoutes = registerRoutes(app)
55 | var mockRoutesLength = mockRoutes.mockRoutesLength
56 | var mockStartIndex = mockRoutes.mockStartIndex
57 |
58 | // watch files, hot reload mock server
59 | chokidar.watch(mockDir, {
60 | ignored: /mock-server/,
61 | ignoreInitial: true
62 | }).on('all', (event, path) => {
63 | if (event === 'change' || event === 'add') {
64 | try {
65 | // remove mock routes stack
66 | app._router.stack.splice(mockStartIndex, mockRoutesLength)
67 |
68 | // clear routes cache
69 | unregisterRoutes()
70 |
71 | const mockRoutes = registerRoutes(app)
72 | mockRoutesLength = mockRoutes.mockRoutesLength
73 | mockStartIndex = mockRoutes.mockStartIndex
74 |
75 | console.log(chalk.magentaBright(`\n > Mock Server hot reload success! changed ${path}`))
76 | } catch (error) {
77 | console.log(chalk.redBright(error))
78 | }
79 | }
80 | })
81 | }
82 |
--------------------------------------------------------------------------------
/src/components/Share/DropdownMenu.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 |
39 |
40 |
104 |
--------------------------------------------------------------------------------
/src/utils/request.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import { MessageBox, Message } from 'element-ui'
3 | import store from '@/store'
4 | import { getToken } from '@/utils/auth'
5 |
6 | // create an axios instance
7 | const service = axios.create({
8 | baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
9 | // withCredentials: true, // send cookies when cross-domain requests
10 | timeout: 5000 // request timeout
11 | })
12 |
13 | // request interceptor
14 | service.interceptors.request.use(
15 | config => {
16 | // do something before request is sent
17 |
18 | if (store.getters.token) {
19 | // let each request carry token
20 | // ['X-Token'] is a custom headers key
21 | // please modify it according to the actual situation
22 | config.headers['token'] = getToken()
23 | }
24 | return config
25 | },
26 | error => {
27 | // do something with request error
28 | console.log(error) // for debug
29 | return Promise.reject(error)
30 | }
31 | )
32 |
33 | // response interceptor
34 | service.interceptors.response.use(
35 | /**
36 | * If you want to get http information such as headers or status
37 | * Please return response => response
38 | */
39 |
40 | /**
41 | * Determine the request status by custom code
42 | * Here is just an example
43 | * You can also judge the status by HTTP Status Code
44 | */
45 | response => {
46 | const res = response.data
47 |
48 | // if the custom code is not 20000, it is judged as an error.
49 | if (res.code !== 20000) {
50 | Message({
51 | message: res.message || 'Error',
52 | type: 'error',
53 | duration: 5 * 1000
54 | })
55 |
56 | // 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired;
57 | if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
58 | // to re-login
59 | MessageBox.confirm('You have been logged out, you can cancel to stay on this page, or log in again', 'Confirm logout', {
60 | confirmButtonText: 'Re-Login',
61 | cancelButtonText: 'Cancel',
62 | type: 'warning'
63 | }).then(() => {
64 | store.dispatch('user/resetToken').then(() => {
65 | location.reload()
66 | })
67 | })
68 | }
69 | return Promise.reject(new Error(res.message || 'Error'))
70 | } else {
71 | return res
72 | }
73 | },
74 | error => {
75 | console.log('err' + error) // for debug
76 | Message({
77 | message: error.message,
78 | type: 'error',
79 | duration: 5 * 1000
80 | })
81 | return Promise.reject(error)
82 | }
83 | )
84 |
85 | export default service
86 |
--------------------------------------------------------------------------------
/src/directive/el-drag-dialog/drag.js:
--------------------------------------------------------------------------------
1 | export default {
2 | bind(el, binding, vnode) {
3 | const dialogHeaderEl = el.querySelector('.el-dialog__header')
4 | const dragDom = el.querySelector('.el-dialog')
5 | dialogHeaderEl.style.cssText += ';cursor:move;'
6 | dragDom.style.cssText += ';top:0px;'
7 |
8 | // 获取原有属性 ie dom元素.currentStyle 火狐谷歌 window.getComputedStyle(dom元素, null);
9 | const getStyle = (function() {
10 | if (window.document.currentStyle) {
11 | return (dom, attr) => dom.currentStyle[attr]
12 | } else {
13 | return (dom, attr) => getComputedStyle(dom, false)[attr]
14 | }
15 | })()
16 |
17 | dialogHeaderEl.onmousedown = (e) => {
18 | // 鼠标按下,计算当前元素距离可视区的距离
19 | const disX = e.clientX - dialogHeaderEl.offsetLeft
20 | const disY = e.clientY - dialogHeaderEl.offsetTop
21 |
22 | const dragDomWidth = dragDom.offsetWidth
23 | const dragDomHeight = dragDom.offsetHeight
24 |
25 | const screenWidth = document.body.clientWidth
26 | const screenHeight = document.body.clientHeight
27 |
28 | const minDragDomLeft = dragDom.offsetLeft
29 | const maxDragDomLeft = screenWidth - dragDom.offsetLeft - dragDomWidth
30 |
31 | const minDragDomTop = dragDom.offsetTop
32 | const maxDragDomTop = screenHeight - dragDom.offsetTop - dragDomHeight
33 |
34 | // 获取到的值带px 正则匹配替换
35 | let styL = getStyle(dragDom, 'left')
36 | let styT = getStyle(dragDom, 'top')
37 |
38 | if (styL.includes('%')) {
39 | styL = +document.body.clientWidth * (+styL.replace(/\%/g, '') / 100)
40 | styT = +document.body.clientHeight * (+styT.replace(/\%/g, '') / 100)
41 | } else {
42 | styL = +styL.replace(/\px/g, '')
43 | styT = +styT.replace(/\px/g, '')
44 | }
45 |
46 | document.onmousemove = function(e) {
47 | // 通过事件委托,计算移动的距离
48 | let left = e.clientX - disX
49 | let top = e.clientY - disY
50 |
51 | // 边界处理
52 | if (-(left) > minDragDomLeft) {
53 | left = -minDragDomLeft
54 | } else if (left > maxDragDomLeft) {
55 | left = maxDragDomLeft
56 | }
57 |
58 | if (-(top) > minDragDomTop) {
59 | top = -minDragDomTop
60 | } else if (top > maxDragDomTop) {
61 | top = maxDragDomTop
62 | }
63 |
64 | // 移动当前元素
65 | dragDom.style.cssText += `;left:${left + styL}px;top:${top + styT}px;`
66 |
67 | // emit onDrag event
68 | vnode.child.$emit('dragDialog')
69 | }
70 |
71 | document.onmouseup = function(e) {
72 | document.onmousemove = null
73 | document.onmouseup = null
74 | }
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/permission.js:
--------------------------------------------------------------------------------
1 | import router from './router'
2 | import store from './store'
3 | import { Message } from 'element-ui'
4 | import NProgress from 'nprogress' // progress bar
5 | import 'nprogress/nprogress.css' // progress bar style
6 | import { getToken } from '@/utils/auth' // get token from cookie
7 | import getPageTitle from '@/utils/get-page-title'
8 |
9 | NProgress.configure({ showSpinner: false }) // NProgress Configuration
10 |
11 | const whiteList = ['/login', '/auth-redirect'] // no redirect whitelist
12 |
13 | router.beforeEach(async(to, from, next) => {
14 | // start progress bar
15 | NProgress.start()
16 |
17 | // set page title
18 | document.title = getPageTitle(to.meta.title)
19 |
20 | // determine whether the user has logged in
21 | const hasToken = getToken()
22 |
23 | if (hasToken) {
24 | if (to.path === '/login') {
25 | // if is logged in, redirect to the home page
26 | next({ path: '/' })
27 | NProgress.done() // hack: https://github.com/PanJiaChen/vue-element-admin/pull/2939
28 | } else {
29 | // determine whether the user has obtained his permission roles through getInfo
30 | const hasRoles = store.getters.roles && store.getters.roles.length > 0
31 | if (hasRoles) {
32 | next()
33 | } else {
34 | try {
35 | // get user info
36 | // note: roles must be a object array! such as: ['admin'] or ,['developer','editor']
37 | const { roles } = await store.dispatch('user/getInfo')
38 |
39 | // generate accessible routes map based on roles
40 | const accessRoutes = await store.dispatch('permission/generateRoutes', roles)
41 |
42 | // dynamically add accessible routes
43 | router.addRoutes(accessRoutes)
44 |
45 | // hack method to ensure that addRoutes is complete
46 | // set the replace: true, so the navigation will not leave a history record
47 | next({ ...to, replace: true })
48 | } catch (error) {
49 | // remove token and go to login page to re-login
50 | await store.dispatch('user/resetToken')
51 | Message.error(error || 'Has Error')
52 | next(`/login?redirect=${to.path}`)
53 | NProgress.done()
54 | }
55 | }
56 | }
57 | } else {
58 | /* has no token*/
59 |
60 | if (whiteList.indexOf(to.path) !== -1) {
61 | // in the free login whitelist, go directly
62 | next()
63 | } else {
64 | // other pages that do not have permission to access are redirected to the login page.
65 | next(`/login?redirect=${to.path}`)
66 | NProgress.done()
67 | }
68 | }
69 | })
70 |
71 | router.afterEach(() => {
72 | // finish progress bar
73 | NProgress.done()
74 | })
75 |
--------------------------------------------------------------------------------
/src/views/dashboard/components/LineChart.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
117 |
--------------------------------------------------------------------------------
/src/layout/components/Settings/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Page style setting
5 |
6 |
7 | Theme Color
8 |
9 |
10 |
11 |
12 | Open Tags-View
13 |
14 |
15 |
16 |
17 | Fixed Header
18 |
19 |
20 |
21 |
22 | Sidebar Logo
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
83 |
84 |
109 |
--------------------------------------------------------------------------------
/src/layout/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
59 |
60 |
103 |
--------------------------------------------------------------------------------
/src/directive/sticky.js:
--------------------------------------------------------------------------------
1 | const vueSticky = {}
2 | let listenAction
3 | vueSticky.install = Vue => {
4 | Vue.directive('sticky', {
5 | inserted(el, binding) {
6 | const params = binding.value || {}
7 | const stickyTop = params.stickyTop || 0
8 | const zIndex = params.zIndex || 1000
9 | const elStyle = el.style
10 |
11 | elStyle.position = '-webkit-sticky'
12 | elStyle.position = 'sticky'
13 | // if the browser support css sticky(Currently Safari, Firefox and Chrome Canary)
14 | // if (~elStyle.position.indexOf('sticky')) {
15 | // elStyle.top = `${stickyTop}px`;
16 | // elStyle.zIndex = zIndex;
17 | // return
18 | // }
19 | const elHeight = el.getBoundingClientRect().height
20 | const elWidth = el.getBoundingClientRect().width
21 | elStyle.cssText = `top: ${stickyTop}px; z-index: ${zIndex}`
22 |
23 | const parentElm = el.parentNode || document.documentElement
24 | const placeholder = document.createElement('div')
25 | placeholder.style.display = 'none'
26 | placeholder.style.width = `${elWidth}px`
27 | placeholder.style.height = `${elHeight}px`
28 | parentElm.insertBefore(placeholder, el)
29 |
30 | let active = false
31 |
32 | const getScroll = (target, top) => {
33 | const prop = top ? 'pageYOffset' : 'pageXOffset'
34 | const method = top ? 'scrollTop' : 'scrollLeft'
35 | let ret = target[prop]
36 | if (typeof ret !== 'number') {
37 | ret = window.document.documentElement[method]
38 | }
39 | return ret
40 | }
41 |
42 | const sticky = () => {
43 | if (active) {
44 | return
45 | }
46 | if (!elStyle.height) {
47 | elStyle.height = `${el.offsetHeight}px`
48 | }
49 |
50 | elStyle.position = 'fixed'
51 | elStyle.width = `${elWidth}px`
52 | placeholder.style.display = 'inline-block'
53 | active = true
54 | }
55 |
56 | const reset = () => {
57 | if (!active) {
58 | return
59 | }
60 |
61 | elStyle.position = ''
62 | placeholder.style.display = 'none'
63 | active = false
64 | }
65 |
66 | const check = () => {
67 | const scrollTop = getScroll(window, true)
68 | const offsetTop = el.getBoundingClientRect().top
69 | if (offsetTop < stickyTop) {
70 | sticky()
71 | } else {
72 | if (scrollTop < elHeight + stickyTop) {
73 | reset()
74 | }
75 | }
76 | }
77 | listenAction = () => {
78 | check()
79 | }
80 |
81 | window.addEventListener('scroll', listenAction)
82 | },
83 |
84 | unbind() {
85 | window.removeEventListener('scroll', listenAction)
86 | }
87 | })
88 | }
89 |
90 | export default vueSticky
91 |
92 |
--------------------------------------------------------------------------------
/src/components/TextHoverEffect/Mallki.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ text }}
4 |
5 |
6 |
7 |
8 |
9 |
23 |
24 |
114 |
--------------------------------------------------------------------------------
/mock/user.js:
--------------------------------------------------------------------------------
1 | const users = {
2 | roles: ['admin'],
3 | introduction: 'I am a super administrator',
4 | avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
5 | name: 'Super Admin',
6 | id: 0,
7 | account: 'admin@test.com',
8 | nickname: 'WindSnowLi',
9 | qq: '706623475',
10 | token: '66666666666666666'
11 | }
12 |
13 | module.exports = [
14 | // user login
15 | {
16 | url: '/user/login',
17 | type: 'post',
18 | response: config => {
19 | // const { username } = config.body
20 | // const token = tokens[username]
21 |
22 | return {
23 | code: 20000,
24 | data: users
25 | }
26 | }
27 | },
28 |
29 | // get user info
30 | {
31 | url: '/user/getInfo',
32 | type: 'post',
33 | response: config => {
34 | // const { token } = config.query
35 | // const info = users[token]
36 |
37 | // // mock error
38 | // if (!info) {
39 | // return {
40 | // code: 50008,
41 | // message: 'Login failed, unable to get user details.'
42 | // }
43 | // }
44 |
45 | return {
46 | code: 20000,
47 | data: users
48 | }
49 | }
50 | },
51 | // set user info
52 | {
53 | url: '/user/setInfo',
54 | type: 'post',
55 | response: _ => {
56 | return {
57 | code: 20000,
58 | data: users
59 | }
60 | }
61 | },
62 |
63 | // user logout
64 | {
65 | url: '/user/logout',
66 | type: 'post',
67 | response: _ => {
68 | return {
69 | code: 20000,
70 | data: 'success'
71 | }
72 | }
73 | },
74 | // getWork
75 | {
76 | url: '/user/getWork',
77 | type: 'post',
78 | response: _ => {
79 | return {
80 | code: 20000,
81 | data: [
82 | {
83 | name: 'Vue',
84 | value: 30
85 | },
86 | {
87 | name: 'C++',
88 | value: 47
89 | },
90 | {
91 | name: 'Java',
92 | value: 56
93 | },
94 | {
95 | name: 'Python',
96 | value: 65
97 | }
98 | ]
99 | }
100 | }
101 | },
102 | // getWork
103 | {
104 | url: '/user/setAvatar',
105 | type: 'post',
106 | response: _ => {
107 | return {
108 | code: 20000,
109 | data: 'OK'
110 | }
111 | }
112 | },
113 | // 获取用户关于信息
114 | {
115 | url: '/user/getAboutByUserId',
116 | type: 'post',
117 | response: _ => {
118 | return {
119 | code: 20000,
120 | data: 'I LOVE YOU WITH ME!'
121 | }
122 | }
123 | },
124 | // 获取用户关于信息
125 | {
126 | url: '/user/setAboutByUserToken',
127 | type: 'post',
128 | response: _ => {
129 | return {
130 | code: 20000,
131 | data: 'OK'
132 | }
133 | }
134 | }
135 | ]
136 |
--------------------------------------------------------------------------------
/src/layout/components/TagsView/ScrollPane.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
78 |
79 |
95 |
--------------------------------------------------------------------------------