Initial
							
								
								
									
										14
									
								
								.editorconfig
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,14 @@
 | 
			
		||||
# https://editorconfig.org
 | 
			
		||||
root = true
 | 
			
		||||
 | 
			
		||||
[*]
 | 
			
		||||
charset = utf-8
 | 
			
		||||
indent_style = space
 | 
			
		||||
indent_size = 2
 | 
			
		||||
end_of_line = lf
 | 
			
		||||
insert_final_newline = true
 | 
			
		||||
trim_trailing_whitespace = true
 | 
			
		||||
 | 
			
		||||
[*.md]
 | 
			
		||||
insert_final_newline = false
 | 
			
		||||
trim_trailing_whitespace = false
 | 
			
		||||
							
								
								
									
										14
									
								
								.env.development
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,14 @@
 | 
			
		||||
# just a flag
 | 
			
		||||
ENV = 'development'
 | 
			
		||||
 | 
			
		||||
# base api
 | 
			
		||||
VUE_APP_BASE_API = '/dev-api'
 | 
			
		||||
 | 
			
		||||
# vue-cli uses the VUE_CLI_BABEL_TRANSPILE_MODULES environment variable,
 | 
			
		||||
# to control whether the babel-plugin-dynamic-import-node plugin is enabled.
 | 
			
		||||
# It only does one thing by converting all import() to require().
 | 
			
		||||
# This configuration can significantly increase the speed of hot updates,
 | 
			
		||||
# when you have a large number of pages.
 | 
			
		||||
# Detail:  https://github.com/vuejs/vue-cli/blob/dev/packages/@vue/babel-preset-app/index.js
 | 
			
		||||
 | 
			
		||||
VUE_CLI_BABEL_TRANSPILE_MODULES = true
 | 
			
		||||
							
								
								
									
										6
									
								
								.env.production
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,6 @@
 | 
			
		||||
# just a flag
 | 
			
		||||
ENV = 'production'
 | 
			
		||||
 | 
			
		||||
# base api
 | 
			
		||||
VUE_APP_BASE_API = '/prod-api'
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										8
									
								
								.env.staging
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,8 @@
 | 
			
		||||
NODE_ENV = production
 | 
			
		||||
 | 
			
		||||
# just a flag
 | 
			
		||||
ENV = 'staging'
 | 
			
		||||
 | 
			
		||||
# base api
 | 
			
		||||
VUE_APP_BASE_API = '/stage-api'
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										4
									
								
								.eslintignore
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,4 @@
 | 
			
		||||
build/*.js
 | 
			
		||||
src/assets
 | 
			
		||||
public
 | 
			
		||||
dist
 | 
			
		||||
							
								
								
									
										198
									
								
								.eslintrc.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,198 @@
 | 
			
		||||
module.exports = {
 | 
			
		||||
  root: true,
 | 
			
		||||
  parserOptions: {
 | 
			
		||||
    parser: 'babel-eslint',
 | 
			
		||||
    sourceType: 'module'
 | 
			
		||||
  },
 | 
			
		||||
  env: {
 | 
			
		||||
    browser: true,
 | 
			
		||||
    node: true,
 | 
			
		||||
    es6: true,
 | 
			
		||||
  },
 | 
			
		||||
  extends: ['plugin:vue/recommended', 'eslint:recommended'],
 | 
			
		||||
 | 
			
		||||
  // add your custom rules here
 | 
			
		||||
  //it is base on https://github.com/vuejs/eslint-config-vue
 | 
			
		||||
  rules: {
 | 
			
		||||
    "vue/max-attributes-per-line": [2, {
 | 
			
		||||
      "singleline": 10,
 | 
			
		||||
      "multiline": {
 | 
			
		||||
        "max": 1,
 | 
			
		||||
        "allowFirstLine": false
 | 
			
		||||
      }
 | 
			
		||||
    }],
 | 
			
		||||
    "vue/singleline-html-element-content-newline": "off",
 | 
			
		||||
    "vue/multiline-html-element-content-newline":"off",
 | 
			
		||||
    "vue/name-property-casing": ["error", "PascalCase"],
 | 
			
		||||
    "vue/no-v-html": "off",
 | 
			
		||||
    'accessor-pairs': 2,
 | 
			
		||||
    'arrow-spacing': [2, {
 | 
			
		||||
      'before': true,
 | 
			
		||||
      'after': true
 | 
			
		||||
    }],
 | 
			
		||||
    'block-spacing': [2, 'always'],
 | 
			
		||||
    'brace-style': [2, '1tbs', {
 | 
			
		||||
      'allowSingleLine': true
 | 
			
		||||
    }],
 | 
			
		||||
    'camelcase': [0, {
 | 
			
		||||
      'properties': 'always'
 | 
			
		||||
    }],
 | 
			
		||||
    'comma-dangle': [2, 'never'],
 | 
			
		||||
    'comma-spacing': [2, {
 | 
			
		||||
      'before': false,
 | 
			
		||||
      'after': true
 | 
			
		||||
    }],
 | 
			
		||||
    'comma-style': [2, 'last'],
 | 
			
		||||
    'constructor-super': 2,
 | 
			
		||||
    'curly': [2, 'multi-line'],
 | 
			
		||||
    'dot-location': [2, 'property'],
 | 
			
		||||
    'eol-last': 2,
 | 
			
		||||
    'eqeqeq': ["error", "always", {"null": "ignore"}],
 | 
			
		||||
    'generator-star-spacing': [2, {
 | 
			
		||||
      'before': true,
 | 
			
		||||
      'after': true
 | 
			
		||||
    }],
 | 
			
		||||
    'handle-callback-err': [2, '^(err|error)$'],
 | 
			
		||||
    'indent': [2, 2, {
 | 
			
		||||
      'SwitchCase': 1
 | 
			
		||||
    }],
 | 
			
		||||
    'jsx-quotes': [2, 'prefer-single'],
 | 
			
		||||
    'key-spacing': [2, {
 | 
			
		||||
      'beforeColon': false,
 | 
			
		||||
      'afterColon': true
 | 
			
		||||
    }],
 | 
			
		||||
    'keyword-spacing': [2, {
 | 
			
		||||
      'before': true,
 | 
			
		||||
      'after': true
 | 
			
		||||
    }],
 | 
			
		||||
    'new-cap': [2, {
 | 
			
		||||
      'newIsCap': true,
 | 
			
		||||
      'capIsNew': false
 | 
			
		||||
    }],
 | 
			
		||||
    'new-parens': 2,
 | 
			
		||||
    'no-array-constructor': 2,
 | 
			
		||||
    'no-caller': 2,
 | 
			
		||||
    'no-console': 'off',
 | 
			
		||||
    'no-class-assign': 2,
 | 
			
		||||
    'no-cond-assign': 2,
 | 
			
		||||
    'no-const-assign': 2,
 | 
			
		||||
    'no-control-regex': 0,
 | 
			
		||||
    'no-delete-var': 2,
 | 
			
		||||
    'no-dupe-args': 2,
 | 
			
		||||
    'no-dupe-class-members': 2,
 | 
			
		||||
    'no-dupe-keys': 2,
 | 
			
		||||
    'no-duplicate-case': 2,
 | 
			
		||||
    'no-empty-character-class': 2,
 | 
			
		||||
    'no-empty-pattern': 2,
 | 
			
		||||
    'no-eval': 2,
 | 
			
		||||
    'no-ex-assign': 2,
 | 
			
		||||
    'no-extend-native': 2,
 | 
			
		||||
    'no-extra-bind': 2,
 | 
			
		||||
    'no-extra-boolean-cast': 2,
 | 
			
		||||
    'no-extra-parens': [2, 'functions'],
 | 
			
		||||
    'no-fallthrough': 2,
 | 
			
		||||
    'no-floating-decimal': 2,
 | 
			
		||||
    'no-func-assign': 2,
 | 
			
		||||
    'no-implied-eval': 2,
 | 
			
		||||
    'no-inner-declarations': [2, 'functions'],
 | 
			
		||||
    'no-invalid-regexp': 2,
 | 
			
		||||
    'no-irregular-whitespace': 2,
 | 
			
		||||
    'no-iterator': 2,
 | 
			
		||||
    'no-label-var': 2,
 | 
			
		||||
    'no-labels': [2, {
 | 
			
		||||
      'allowLoop': false,
 | 
			
		||||
      'allowSwitch': false
 | 
			
		||||
    }],
 | 
			
		||||
    'no-lone-blocks': 2,
 | 
			
		||||
    'no-mixed-spaces-and-tabs': 2,
 | 
			
		||||
    'no-multi-spaces': 2,
 | 
			
		||||
    'no-multi-str': 2,
 | 
			
		||||
    'no-multiple-empty-lines': [2, {
 | 
			
		||||
      'max': 1
 | 
			
		||||
    }],
 | 
			
		||||
    'no-native-reassign': 2,
 | 
			
		||||
    'no-negated-in-lhs': 2,
 | 
			
		||||
    'no-new-object': 2,
 | 
			
		||||
    'no-new-require': 2,
 | 
			
		||||
    'no-new-symbol': 2,
 | 
			
		||||
    'no-new-wrappers': 2,
 | 
			
		||||
    'no-obj-calls': 2,
 | 
			
		||||
    'no-octal': 2,
 | 
			
		||||
    'no-octal-escape': 2,
 | 
			
		||||
    'no-path-concat': 2,
 | 
			
		||||
    'no-proto': 2,
 | 
			
		||||
    'no-redeclare': 2,
 | 
			
		||||
    'no-regex-spaces': 2,
 | 
			
		||||
    'no-return-assign': [2, 'except-parens'],
 | 
			
		||||
    'no-self-assign': 2,
 | 
			
		||||
    'no-self-compare': 2,
 | 
			
		||||
    'no-sequences': 2,
 | 
			
		||||
    'no-shadow-restricted-names': 2,
 | 
			
		||||
    'no-spaced-func': 2,
 | 
			
		||||
    'no-sparse-arrays': 2,
 | 
			
		||||
    'no-this-before-super': 2,
 | 
			
		||||
    'no-throw-literal': 2,
 | 
			
		||||
    'no-trailing-spaces': 2,
 | 
			
		||||
    'no-undef': 2,
 | 
			
		||||
    'no-undef-init': 2,
 | 
			
		||||
    'no-unexpected-multiline': 2,
 | 
			
		||||
    'no-unmodified-loop-condition': 2,
 | 
			
		||||
    'no-unneeded-ternary': [2, {
 | 
			
		||||
      'defaultAssignment': false
 | 
			
		||||
    }],
 | 
			
		||||
    'no-unreachable': 2,
 | 
			
		||||
    'no-unsafe-finally': 2,
 | 
			
		||||
    'no-unused-vars': [2, {
 | 
			
		||||
      'vars': 'all',
 | 
			
		||||
      'args': 'none'
 | 
			
		||||
    }],
 | 
			
		||||
    'no-useless-call': 2,
 | 
			
		||||
    'no-useless-computed-key': 2,
 | 
			
		||||
    'no-useless-constructor': 2,
 | 
			
		||||
    'no-useless-escape': 0,
 | 
			
		||||
    'no-whitespace-before-property': 2,
 | 
			
		||||
    'no-with': 2,
 | 
			
		||||
    'one-var': [2, {
 | 
			
		||||
      'initialized': 'never'
 | 
			
		||||
    }],
 | 
			
		||||
    'operator-linebreak': [2, 'after', {
 | 
			
		||||
      'overrides': {
 | 
			
		||||
        '?': 'before',
 | 
			
		||||
        ':': 'before'
 | 
			
		||||
      }
 | 
			
		||||
    }],
 | 
			
		||||
    'padded-blocks': [2, 'never'],
 | 
			
		||||
    'quotes': [2, 'single', {
 | 
			
		||||
      'avoidEscape': true,
 | 
			
		||||
      'allowTemplateLiterals': true
 | 
			
		||||
    }],
 | 
			
		||||
    'semi': [2, 'never'],
 | 
			
		||||
    'semi-spacing': [2, {
 | 
			
		||||
      'before': false,
 | 
			
		||||
      'after': true
 | 
			
		||||
    }],
 | 
			
		||||
    'space-before-blocks': [2, 'always'],
 | 
			
		||||
    'space-before-function-paren': [2, 'never'],
 | 
			
		||||
    'space-in-parens': [2, 'never'],
 | 
			
		||||
    'space-infix-ops': 2,
 | 
			
		||||
    'space-unary-ops': [2, {
 | 
			
		||||
      'words': true,
 | 
			
		||||
      'nonwords': false
 | 
			
		||||
    }],
 | 
			
		||||
    'spaced-comment': [2, 'always', {
 | 
			
		||||
      'markers': ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ',']
 | 
			
		||||
    }],
 | 
			
		||||
    'template-curly-spacing': [2, 'never'],
 | 
			
		||||
    'use-isnan': 2,
 | 
			
		||||
    'valid-typeof': 2,
 | 
			
		||||
    'wrap-iife': [2, 'any'],
 | 
			
		||||
    'yield-star-spacing': [2, 'both'],
 | 
			
		||||
    'yoda': [2, 'never'],
 | 
			
		||||
    'prefer-const': 2,
 | 
			
		||||
    'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
 | 
			
		||||
    'object-curly-spacing': [2, 'always', {
 | 
			
		||||
      objectsInObjects: false
 | 
			
		||||
    }],
 | 
			
		||||
    'array-bracket-spacing': [2, 'never']
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										23
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,23 @@
 | 
			
		||||
.DS_Store
 | 
			
		||||
node_modules/
 | 
			
		||||
dist/
 | 
			
		||||
npm-debug.log*
 | 
			
		||||
yarn-debug.log*
 | 
			
		||||
yarn-error.log*
 | 
			
		||||
**/*.log
 | 
			
		||||
 | 
			
		||||
tests/**/coverage/
 | 
			
		||||
tests/e2e/reports
 | 
			
		||||
selenium-debug.log
 | 
			
		||||
 | 
			
		||||
# Editor directories and files
 | 
			
		||||
.idea
 | 
			
		||||
.vscode
 | 
			
		||||
*.suo
 | 
			
		||||
*.ntvs*
 | 
			
		||||
*.njsproj
 | 
			
		||||
*.sln
 | 
			
		||||
*.local
 | 
			
		||||
 | 
			
		||||
package-lock.json
 | 
			
		||||
yarn.lock
 | 
			
		||||
							
								
								
									
										217
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,217 @@
 | 
			
		||||
<p align="center">
 | 
			
		||||
  <img width="320" src="https://wpimg.wallstcn.com/ecc53a42-d79b-42e2-8852-5126b810a4c8.svg">
 | 
			
		||||
</p>
 | 
			
		||||
 | 
			
		||||
<p align="center">
 | 
			
		||||
  <a href="https://github.com/vuejs/vue">
 | 
			
		||||
    <img src="https://img.shields.io/badge/vue-2.6.10-brightgreen.svg" alt="vue">
 | 
			
		||||
  </a>
 | 
			
		||||
  <a href="https://github.com/ElemeFE/element">
 | 
			
		||||
    <img src="https://img.shields.io/badge/element--ui-2.7.0-brightgreen.svg" alt="element-ui">
 | 
			
		||||
  </a>
 | 
			
		||||
  <a href="https://travis-ci.org/PanJiaChen/vue-element-admin" rel="nofollow">
 | 
			
		||||
    <img src="https://travis-ci.org/PanJiaChen/vue-element-admin.svg?branch=master" alt="Build Status">
 | 
			
		||||
  </a>
 | 
			
		||||
  <a href="https://github.com/PanJiaChen/vue-element-admin/blob/master/LICENSE">
 | 
			
		||||
    <img src="https://img.shields.io/github/license/mashape/apistatus.svg" alt="license">
 | 
			
		||||
  </a>
 | 
			
		||||
  <a href="https://github.com/PanJiaChen/vue-element-admin/releases">
 | 
			
		||||
    <img src="https://img.shields.io/github/release/PanJiaChen/vue-element-admin.svg" alt="GitHub release">
 | 
			
		||||
  </a>
 | 
			
		||||
  <a href="https://gitter.im/vue-element-admin/discuss">
 | 
			
		||||
    <img src="https://badges.gitter.im/Join%20Chat.svg" alt="gitter">
 | 
			
		||||
  </a>
 | 
			
		||||
  <a href="https://panjiachen.github.io/vue-element-admin-site/donate">
 | 
			
		||||
    <img src="https://img.shields.io/badge/%24-donate-ff69b4.svg" alt="donate">
 | 
			
		||||
  </a>
 | 
			
		||||
</p>
 | 
			
		||||
 | 
			
		||||
English | [简体中文](./README.zh-CN.md) | [日本語](./README.ja.md) | [Spanish](./README.es.md)
 | 
			
		||||
 | 
			
		||||
## Introduction
 | 
			
		||||
 | 
			
		||||
[vue-element-admin](https://panjiachen.github.io/vue-element-admin) is a production-ready front-end solution for admin interfaces. It is based on [vue](https://github.com/vuejs/vue) and uses the UI Toolkit [element-ui](https://github.com/ElemeFE/element).
 | 
			
		||||
 | 
			
		||||
[vue-element-admin](https://panjiachen.github.io/vue-element-admin) is based on the newest development stack of vue and it has a built-in i18n solution, typical templates for enterprise applications, and lots of awesome features. It helps you build large and complex Single-Page Applications. I believe whatever your needs are, this project will help you.
 | 
			
		||||
 | 
			
		||||
- [Preview](https://panjiachen.github.io/vue-element-admin)
 | 
			
		||||
 | 
			
		||||
- [Documentation](https://panjiachen.github.io/vue-element-admin-site/)
 | 
			
		||||
 | 
			
		||||
- [Gitter](https://gitter.im/vue-element-admin/discuss)
 | 
			
		||||
 | 
			
		||||
- [Donate](https://panjiachen.github.io/vue-element-admin-site/donate/)
 | 
			
		||||
 | 
			
		||||
- [Wiki](https://github.com/PanJiaChen/vue-element-admin/wiki)
 | 
			
		||||
 | 
			
		||||
- [Gitee](https://panjiachen.gitee.io/vue-element-admin/) 国内用户可访问该地址在线预览
 | 
			
		||||
 | 
			
		||||
- Base template recommends using: [vue-admin-template](https://github.com/PanJiaChen/vue-admin-template)
 | 
			
		||||
- Desktop: [electron-vue-admin](https://github.com/PanJiaChen/electron-vue-admin)
 | 
			
		||||
- Typescript: [vue-typescript-admin-template](https://github.com/Armour/vue-typescript-admin-template) (Credits: [@Armour](https://github.com/Armour))
 | 
			
		||||
- [awesome-project](https://github.com/PanJiaChen/vue-element-admin/issues/2312)
 | 
			
		||||
 | 
			
		||||
**After the `v4.1.0+` version, the default master branch will not support i18n. Please use [i18n Branch](https://github.com/PanJiaChen/vue-element-admin/tree/i18n), it will keep up with the master update**
 | 
			
		||||
 | 
			
		||||
**The current version is `v4.0+` build on `vue-cli`. If you find a problem, please put [issue](https://github.com/PanJiaChen/vue-element-admin/issues/new). If you want to use the old version , you can switch branch to [tag/3.11.0](https://github.com/PanJiaChen/vue-element-admin/tree/tag/3.11.0), it does not rely on `vue-cli`**
 | 
			
		||||
 | 
			
		||||
**This project does not support low version browsers (e.g. IE). Please add polyfill by yourself.**
 | 
			
		||||
 | 
			
		||||
## Preparation
 | 
			
		||||
 | 
			
		||||
You need to install [node](https://nodejs.org/) and [git](https://git-scm.com/) locally. The project is based on [ES2015+](https://es6.ruanyifeng.com/), [vue](https://cn.vuejs.org/index.html), [vuex](https://vuex.vuejs.org/zh-cn/), [vue-router](https://router.vuejs.org/zh-cn/), [vue-cli](https://github.com/vuejs/vue-cli) , [axios](https://github.com/axios/axios) and [element-ui](https://github.com/ElemeFE/element), all request data is simulated using [Mock.js](https://github.com/nuysoft/Mock).
 | 
			
		||||
Understanding and learning this knowledge in advance will greatly help the use of this project.
 | 
			
		||||
 | 
			
		||||
 <p align="center">
 | 
			
		||||
  <img width="900" src="https://wpimg.wallstcn.com/a5894c1b-f6af-456e-82df-1151da0839bf.png">
 | 
			
		||||
</p>
 | 
			
		||||
 | 
			
		||||
## Sponsors
 | 
			
		||||
 | 
			
		||||
Become a sponsor and get your logo on our README on GitHub with a link to your site. [[Become a sponsor]](https://www.patreon.com/panjiachen)
 | 
			
		||||
 | 
			
		||||
<a href="https://flatlogic.com/admin-dashboards?from=vue-element-admin"><img width="150px" src="https://wpimg.wallstcn.com/9c0b719b-5551-4c1e-b776-63994632d94a.png" /></a><p>Admin Dashboard Templates made with Vue, React and Angular.</p>
 | 
			
		||||
 | 
			
		||||
## Features
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
- Login / Logout
 | 
			
		||||
 | 
			
		||||
- Permission Authentication
 | 
			
		||||
  - Page permission
 | 
			
		||||
  - Directive permission
 | 
			
		||||
  - Permission configuration page
 | 
			
		||||
  - Two-step login
 | 
			
		||||
 | 
			
		||||
- Multi-environment build
 | 
			
		||||
  - dev sit stage prod
 | 
			
		||||
 | 
			
		||||
- Global Features
 | 
			
		||||
  - I18n
 | 
			
		||||
  - Multiple dynamic themes
 | 
			
		||||
  - Dynamic sidebar (supports multi-level routing)
 | 
			
		||||
  - Dynamic breadcrumb
 | 
			
		||||
  - Tags-view (Tab page Support right-click operation)
 | 
			
		||||
  - Svg Sprite
 | 
			
		||||
  - Mock data
 | 
			
		||||
  - Screenfull
 | 
			
		||||
  - Responsive Sidebar
 | 
			
		||||
 | 
			
		||||
- Editor
 | 
			
		||||
  - Rich Text Editor
 | 
			
		||||
  - Markdown Editor
 | 
			
		||||
  - JSON Editor
 | 
			
		||||
 | 
			
		||||
- Excel
 | 
			
		||||
  - Export Excel
 | 
			
		||||
  - Upload Excel
 | 
			
		||||
  - Visualization Excel
 | 
			
		||||
  - Export zip
 | 
			
		||||
 | 
			
		||||
- Table
 | 
			
		||||
  - Dynamic Table
 | 
			
		||||
  - Drag And Drop Table
 | 
			
		||||
  - Inline Edit Table
 | 
			
		||||
 | 
			
		||||
- Error Page
 | 
			
		||||
  - 401
 | 
			
		||||
  - 404
 | 
			
		||||
 | 
			
		||||
- Components
 | 
			
		||||
  - Avatar Upload
 | 
			
		||||
  - Back To Top
 | 
			
		||||
  - Drag Dialog
 | 
			
		||||
  - Drag Select
 | 
			
		||||
  - Drag Kanban
 | 
			
		||||
  - Drag List
 | 
			
		||||
  - SplitPane
 | 
			
		||||
  - Dropzone
 | 
			
		||||
  - Sticky
 | 
			
		||||
  - CountTo
 | 
			
		||||
 | 
			
		||||
- Advanced Example
 | 
			
		||||
- Error Log
 | 
			
		||||
- Dashboard
 | 
			
		||||
- Guide Page
 | 
			
		||||
- ECharts
 | 
			
		||||
- Clipboard
 | 
			
		||||
- Markdown to html
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Getting started
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
# clone the project
 | 
			
		||||
git clone https://github.com/PanJiaChen/vue-element-admin.git
 | 
			
		||||
 | 
			
		||||
# enter the project directory
 | 
			
		||||
cd vue-element-admin
 | 
			
		||||
 | 
			
		||||
# install dependency
 | 
			
		||||
npm install
 | 
			
		||||
 | 
			
		||||
# develop
 | 
			
		||||
npm run dev
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
This will automatically open http://localhost:9527
 | 
			
		||||
 | 
			
		||||
## Build
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
# build for test environment
 | 
			
		||||
npm run build:stage
 | 
			
		||||
 | 
			
		||||
# build for production environment
 | 
			
		||||
npm run build:prod
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Advanced
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
# preview the release environment effect
 | 
			
		||||
npm run preview
 | 
			
		||||
 | 
			
		||||
# preview the release environment effect + static resource analysis
 | 
			
		||||
npm run preview -- --report
 | 
			
		||||
 | 
			
		||||
# code format check
 | 
			
		||||
npm run lint
 | 
			
		||||
 | 
			
		||||
# code format check and auto fix
 | 
			
		||||
npm run lint -- --fix
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Refer to [Documentation](https://panjiachen.github.io/vue-element-admin-site/guide/essentials/deploy.html) for more information
 | 
			
		||||
 | 
			
		||||
## Changelog
 | 
			
		||||
 | 
			
		||||
Detailed changes for each release are documented in the [release notes](https://github.com/PanJiaChen/vue-element-admin/releases).
 | 
			
		||||
 | 
			
		||||
## Online Demo
 | 
			
		||||
 | 
			
		||||
[Preview](https://panjiachen.github.io/vue-element-admin)
 | 
			
		||||
 | 
			
		||||
## Donate
 | 
			
		||||
 | 
			
		||||
If you find this project useful, you can buy author a glass of juice :tropical_drink:
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
[Paypal Me](https://www.paypal.me/panfree23)
 | 
			
		||||
 | 
			
		||||
[Buy me a coffee](https://www.buymeacoffee.com/Pan)
 | 
			
		||||
 | 
			
		||||
## Browsers support
 | 
			
		||||
 | 
			
		||||
Modern browsers and Internet Explorer 10+.
 | 
			
		||||
 | 
			
		||||
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="IE / Edge" width="24px" height="24px" />](https://godban.github.io/browsers-support-badges/)</br>IE / Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](https://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](https://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](https://godban.github.io/browsers-support-badges/)</br>Safari |
 | 
			
		||||
| --------- | --------- | --------- | --------- |
 | 
			
		||||
| IE10, IE11, Edge| last 2 versions| last 2 versions| last 2 versions
 | 
			
		||||
 | 
			
		||||
## License
 | 
			
		||||
 | 
			
		||||
[MIT](https://github.com/PanJiaChen/vue-element-admin/blob/master/LICENSE)
 | 
			
		||||
 | 
			
		||||
Copyright (c) 2017-present PanJiaChen
 | 
			
		||||
							
								
								
									
										5
									
								
								babel.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,5 @@
 | 
			
		||||
module.exports = {
 | 
			
		||||
  presets: [
 | 
			
		||||
    '@vue/app'
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										35
									
								
								build/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,35 @@
 | 
			
		||||
const { run } = require('runjs')
 | 
			
		||||
const chalk = require('chalk')
 | 
			
		||||
const config = require('../vue.config.js')
 | 
			
		||||
const rawArgv = process.argv.slice(2)
 | 
			
		||||
const args = rawArgv.join(' ')
 | 
			
		||||
 | 
			
		||||
if (process.env.npm_config_preview || rawArgv.includes('--preview')) {
 | 
			
		||||
  const report = rawArgv.includes('--report')
 | 
			
		||||
 | 
			
		||||
  run(`vue-cli-service build ${args}`)
 | 
			
		||||
 | 
			
		||||
  const port = 9526
 | 
			
		||||
  const publicPath = config.publicPath
 | 
			
		||||
 | 
			
		||||
  var connect = require('connect')
 | 
			
		||||
  var serveStatic = require('serve-static')
 | 
			
		||||
  const app = connect()
 | 
			
		||||
 | 
			
		||||
  app.use(
 | 
			
		||||
    publicPath,
 | 
			
		||||
    serveStatic('./dist', {
 | 
			
		||||
      index: ['index.html', '/']
 | 
			
		||||
    })
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  app.listen(port, function () {
 | 
			
		||||
    console.log(chalk.green(`> Preview at  http://localhost:${port}${publicPath}`))
 | 
			
		||||
    if (report) {
 | 
			
		||||
      console.log(chalk.green(`> Report at  http://localhost:${port}${publicPath}report.html`))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
  })
 | 
			
		||||
} else {
 | 
			
		||||
  run(`vue-cli-service build ${args}`)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										24
									
								
								jest.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,24 @@
 | 
			
		||||
module.exports = {
 | 
			
		||||
  moduleFileExtensions: ['js', 'jsx', 'json', 'vue'],
 | 
			
		||||
  transform: {
 | 
			
		||||
    '^.+\\.vue$': 'vue-jest',
 | 
			
		||||
    '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$':
 | 
			
		||||
      'jest-transform-stub',
 | 
			
		||||
    '^.+\\.jsx?$': 'babel-jest'
 | 
			
		||||
  },
 | 
			
		||||
  moduleNameMapper: {
 | 
			
		||||
    '^@/(.*)$': '<rootDir>/src/$1'
 | 
			
		||||
  },
 | 
			
		||||
  snapshotSerializers: ['jest-serializer-vue'],
 | 
			
		||||
  testMatch: [
 | 
			
		||||
    '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'
 | 
			
		||||
  ],
 | 
			
		||||
  collectCoverageFrom: ['src/utils/**/*.{js,vue}', '!src/utils/auth.js', '!src/utils/request.js', 'src/components/**/*.{js,vue}'],
 | 
			
		||||
  coverageDirectory: '<rootDir>/tests/unit/coverage',
 | 
			
		||||
  // 'collectCoverage': true,
 | 
			
		||||
  'coverageReporters': [
 | 
			
		||||
    'lcov',
 | 
			
		||||
    'text-summary'
 | 
			
		||||
  ],
 | 
			
		||||
  testURL: 'http://localhost/'
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										9
									
								
								jsconfig.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,9 @@
 | 
			
		||||
{ 
 | 
			
		||||
  "compilerOptions": {
 | 
			
		||||
    "baseUrl": "./",
 | 
			
		||||
    "paths": {
 | 
			
		||||
        "@/*": ["src/*"]
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "exclude": ["node_modules", "dist"]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										116
									
								
								mock/article.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,116 @@
 | 
			
		||||
import Mock from 'mockjs'
 | 
			
		||||
 | 
			
		||||
const List = []
 | 
			
		||||
const count = 100
 | 
			
		||||
 | 
			
		||||
const baseContent = '<p>I am testing data, I am testing data.</p><p><img src="https://wpimg.wallstcn.com/4c69009c-0fd4-4153-b112-6cb53d1cf943"></p>'
 | 
			
		||||
const image_uri = 'https://wpimg.wallstcn.com/e4558086-631c-425c-9430-56ffb46e70b3'
 | 
			
		||||
 | 
			
		||||
for (let i = 0; i < count; i++) {
 | 
			
		||||
  List.push(Mock.mock({
 | 
			
		||||
    id: '@increment',
 | 
			
		||||
    timestamp: +Mock.Random.date('T'),
 | 
			
		||||
    author: '@first',
 | 
			
		||||
    reviewer: '@first',
 | 
			
		||||
    title: '@title(5, 10)',
 | 
			
		||||
    content_short: 'mock data',
 | 
			
		||||
    content: baseContent,
 | 
			
		||||
    forecast: '@float(0, 100, 2, 2)',
 | 
			
		||||
    importance: '@integer(1, 3)',
 | 
			
		||||
    'type|1': ['CN', 'US', 'JP', 'EU'],
 | 
			
		||||
    'status|1': ['published', 'draft', 'deleted'],
 | 
			
		||||
    display_time: '@datetime',
 | 
			
		||||
    comment_disabled: true,
 | 
			
		||||
    pageviews: '@integer(300, 5000)',
 | 
			
		||||
    image_uri,
 | 
			
		||||
    platforms: ['a-platform']
 | 
			
		||||
  }))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default [
 | 
			
		||||
  {
 | 
			
		||||
    url: '/article/list',
 | 
			
		||||
    type: 'get',
 | 
			
		||||
    response: config => {
 | 
			
		||||
      const { importance, type, title, page = 1, limit = 20, sort } = config.query
 | 
			
		||||
 | 
			
		||||
      let mockList = List.filter(item => {
 | 
			
		||||
        if (importance && item.importance !== +importance) return false
 | 
			
		||||
        if (type && item.type !== type) return false
 | 
			
		||||
        if (title && item.title.indexOf(title) < 0) return false
 | 
			
		||||
        return true
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      if (sort === '-id') {
 | 
			
		||||
        mockList = mockList.reverse()
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const pageList = mockList.filter((item, index) => index < limit * page && index >= limit * (page - 1))
 | 
			
		||||
 | 
			
		||||
      return {
 | 
			
		||||
        code: 20000,
 | 
			
		||||
        data: {
 | 
			
		||||
          total: mockList.length,
 | 
			
		||||
          items: pageList
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  {
 | 
			
		||||
    url: '/article/detail',
 | 
			
		||||
    type: 'get',
 | 
			
		||||
    response: config => {
 | 
			
		||||
      const { id } = config.query
 | 
			
		||||
      for (const article of List) {
 | 
			
		||||
        if (article.id === +id) {
 | 
			
		||||
          return {
 | 
			
		||||
            code: 20000,
 | 
			
		||||
            data: article
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  {
 | 
			
		||||
    url: '/article/pv',
 | 
			
		||||
    type: 'get',
 | 
			
		||||
    response: _ => {
 | 
			
		||||
      return {
 | 
			
		||||
        code: 20000,
 | 
			
		||||
        data: {
 | 
			
		||||
          pvData: [
 | 
			
		||||
            { key: 'PC', pv: 1024 },
 | 
			
		||||
            { key: 'mobile', pv: 1024 },
 | 
			
		||||
            { key: 'ios', pv: 1024 },
 | 
			
		||||
            { key: 'android', pv: 1024 }
 | 
			
		||||
          ]
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  {
 | 
			
		||||
    url: '/article/create',
 | 
			
		||||
    type: 'post',
 | 
			
		||||
    response: _ => {
 | 
			
		||||
      return {
 | 
			
		||||
        code: 20000,
 | 
			
		||||
        data: 'success'
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  {
 | 
			
		||||
    url: '/article/update',
 | 
			
		||||
    type: 'post',
 | 
			
		||||
    response: _ => {
 | 
			
		||||
      return {
 | 
			
		||||
        code: 20000,
 | 
			
		||||
        data: 'success'
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										70
									
								
								mock/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,70 @@
 | 
			
		||||
import Mock from 'mockjs'
 | 
			
		||||
import { param2Obj } from '../src/utils'
 | 
			
		||||
 | 
			
		||||
import user from './user'
 | 
			
		||||
import role from './role'
 | 
			
		||||
import article from './article'
 | 
			
		||||
import search from './remote-search'
 | 
			
		||||
 | 
			
		||||
const mocks = [
 | 
			
		||||
  ...user,
 | 
			
		||||
  ...role,
 | 
			
		||||
  ...article,
 | 
			
		||||
  ...search
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
// for front mock
 | 
			
		||||
// please use it cautiously, it will redefine XMLHttpRequest,
 | 
			
		||||
// which will cause many of your third-party libraries to be invalidated(like progress event).
 | 
			
		||||
export function mockXHR() {
 | 
			
		||||
  // mock patch
 | 
			
		||||
  // https://github.com/nuysoft/Mock/issues/300
 | 
			
		||||
  Mock.XHR.prototype.proxy_send = Mock.XHR.prototype.send
 | 
			
		||||
  Mock.XHR.prototype.send = function() {
 | 
			
		||||
    if (this.custom.xhr) {
 | 
			
		||||
      this.custom.xhr.withCredentials = this.withCredentials || false
 | 
			
		||||
 | 
			
		||||
      if (this.responseType) {
 | 
			
		||||
        this.custom.xhr.responseType = this.responseType
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    this.proxy_send(...arguments)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function XHR2ExpressReqWrap(respond) {
 | 
			
		||||
    return function(options) {
 | 
			
		||||
      let result = null
 | 
			
		||||
      if (respond instanceof Function) {
 | 
			
		||||
        const { body, type, url } = options
 | 
			
		||||
        // https://expressjs.com/en/4x/api.html#req
 | 
			
		||||
        result = respond({
 | 
			
		||||
          method: type,
 | 
			
		||||
          body: JSON.parse(body),
 | 
			
		||||
          query: param2Obj(url)
 | 
			
		||||
        })
 | 
			
		||||
      } else {
 | 
			
		||||
        result = respond
 | 
			
		||||
      }
 | 
			
		||||
      return Mock.mock(result)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  for (const i of mocks) {
 | 
			
		||||
    Mock.mock(new RegExp(i.url), i.type || 'get', XHR2ExpressReqWrap(i.response))
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// for mock server
 | 
			
		||||
const responseFake = (url, type, respond) => {
 | 
			
		||||
  return {
 | 
			
		||||
    url: new RegExp(`/mock${url}`),
 | 
			
		||||
    type: type || 'get',
 | 
			
		||||
    response(req, res) {
 | 
			
		||||
      res.json(Mock.mock(respond instanceof Function ? respond(req, res) : respond))
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default mocks.map(route => {
 | 
			
		||||
  return responseFake(route.url, route.type, route.response)
 | 
			
		||||
})
 | 
			
		||||
							
								
								
									
										68
									
								
								mock/mock-server.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,68 @@
 | 
			
		||||
const chokidar = require('chokidar')
 | 
			
		||||
const bodyParser = require('body-parser')
 | 
			
		||||
const chalk = require('chalk')
 | 
			
		||||
const path = require('path')
 | 
			
		||||
 | 
			
		||||
const mockDir = path.join(process.cwd(), 'mock')
 | 
			
		||||
 | 
			
		||||
function registerRoutes(app) {
 | 
			
		||||
  let mockLastIndex
 | 
			
		||||
  const { default: mocks } = require('./index.js')
 | 
			
		||||
  for (const mock of mocks) {
 | 
			
		||||
    app[mock.type](mock.url, mock.response)
 | 
			
		||||
    mockLastIndex = app._router.stack.length
 | 
			
		||||
  }
 | 
			
		||||
  const mockRoutesLength = Object.keys(mocks).length
 | 
			
		||||
  return {
 | 
			
		||||
    mockRoutesLength: mockRoutesLength,
 | 
			
		||||
    mockStartIndex: mockLastIndex - mockRoutesLength
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function unregisterRoutes() {
 | 
			
		||||
  Object.keys(require.cache).forEach(i => {
 | 
			
		||||
    if (i.includes(mockDir)) {
 | 
			
		||||
      delete require.cache[require.resolve(i)]
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = app => {
 | 
			
		||||
  // es6 polyfill
 | 
			
		||||
  require('@babel/register')
 | 
			
		||||
 | 
			
		||||
  // parse app.body
 | 
			
		||||
  // https://expressjs.com/en/4x/api.html#req.body
 | 
			
		||||
  app.use(bodyParser.json())
 | 
			
		||||
  app.use(bodyParser.urlencoded({
 | 
			
		||||
    extended: true
 | 
			
		||||
  }))
 | 
			
		||||
 | 
			
		||||
  const mockRoutes = registerRoutes(app)
 | 
			
		||||
  var mockRoutesLength = mockRoutes.mockRoutesLength
 | 
			
		||||
  var mockStartIndex = mockRoutes.mockStartIndex
 | 
			
		||||
 | 
			
		||||
  // watch files, hot reload mock server
 | 
			
		||||
  chokidar.watch(mockDir, {
 | 
			
		||||
    ignored: /mock-server/,
 | 
			
		||||
    ignoreInitial: true
 | 
			
		||||
  }).on('all', (event, path) => {
 | 
			
		||||
    if (event === 'change' || event === 'add') {
 | 
			
		||||
      try {
 | 
			
		||||
        // remove mock routes stack
 | 
			
		||||
        app._router.stack.splice(mockStartIndex, mockRoutesLength)
 | 
			
		||||
 | 
			
		||||
        // clear routes cache
 | 
			
		||||
        unregisterRoutes()
 | 
			
		||||
 | 
			
		||||
        const mockRoutes = registerRoutes(app)
 | 
			
		||||
        mockRoutesLength = mockRoutes.mockRoutesLength
 | 
			
		||||
        mockStartIndex = mockRoutes.mockStartIndex
 | 
			
		||||
 | 
			
		||||
        console.log(chalk.magentaBright(`\n > Mock Server hot reload success! changed  ${path}`))
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        console.log(chalk.redBright(error))
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										51
									
								
								mock/remote-search.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,51 @@
 | 
			
		||||
import Mock from 'mockjs'
 | 
			
		||||
 | 
			
		||||
const NameList = []
 | 
			
		||||
const count = 100
 | 
			
		||||
 | 
			
		||||
for (let i = 0; i < count; i++) {
 | 
			
		||||
  NameList.push(Mock.mock({
 | 
			
		||||
    name: '@first'
 | 
			
		||||
  }))
 | 
			
		||||
}
 | 
			
		||||
NameList.push({ name: 'mock-Pan' })
 | 
			
		||||
 | 
			
		||||
export default [
 | 
			
		||||
  // username search
 | 
			
		||||
  {
 | 
			
		||||
    url: '/search/user',
 | 
			
		||||
    type: 'get',
 | 
			
		||||
    response: config => {
 | 
			
		||||
      const { name } = config.query
 | 
			
		||||
      const mockNameList = NameList.filter(item => {
 | 
			
		||||
        const lowerCaseName = item.name.toLowerCase()
 | 
			
		||||
        return !(name && lowerCaseName.indexOf(name.toLowerCase()) < 0)
 | 
			
		||||
      })
 | 
			
		||||
      return {
 | 
			
		||||
        code: 20000,
 | 
			
		||||
        data: { items: mockNameList }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  // transaction list
 | 
			
		||||
  {
 | 
			
		||||
    url: '/transaction/list',
 | 
			
		||||
    type: 'get',
 | 
			
		||||
    response: _ => {
 | 
			
		||||
      return {
 | 
			
		||||
        code: 20000,
 | 
			
		||||
        data: {
 | 
			
		||||
          total: 20,
 | 
			
		||||
          'items|20': [{
 | 
			
		||||
            order_no: '@guid()',
 | 
			
		||||
            timestamp: +Mock.Random.date('T'),
 | 
			
		||||
            username: '@name()',
 | 
			
		||||
            price: '@float(1000, 15000, 0, 2)',
 | 
			
		||||
            'status|1': ['success', 'pending']
 | 
			
		||||
          }]
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
]
 | 
			
		||||
							
								
								
									
										98
									
								
								mock/role/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,98 @@
 | 
			
		||||
import Mock from 'mockjs'
 | 
			
		||||
import { deepClone } from '../../src/utils/index.js'
 | 
			
		||||
import { asyncRoutes, constantRoutes } from './routes.js'
 | 
			
		||||
 | 
			
		||||
const routes = deepClone([...constantRoutes, ...asyncRoutes])
 | 
			
		||||
 | 
			
		||||
const roles = [
 | 
			
		||||
  {
 | 
			
		||||
    key: 'admin',
 | 
			
		||||
    name: 'admin',
 | 
			
		||||
    description: 'Super Administrator. Have access to view all pages.',
 | 
			
		||||
    routes: routes
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    key: 'editor',
 | 
			
		||||
    name: 'editor',
 | 
			
		||||
    description: 'Normal Editor. Can see all pages except permission page',
 | 
			
		||||
    routes: routes.filter(i => i.path !== '/permission')// just a mock
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    key: 'visitor',
 | 
			
		||||
    name: 'visitor',
 | 
			
		||||
    description: 'Just a visitor. Can only see the home page and the document page',
 | 
			
		||||
    routes: [{
 | 
			
		||||
      path: '',
 | 
			
		||||
      redirect: 'dashboard',
 | 
			
		||||
      children: [
 | 
			
		||||
        {
 | 
			
		||||
          path: 'dashboard',
 | 
			
		||||
          name: 'Dashboard',
 | 
			
		||||
          meta: { title: 'dashboard', icon: 'dashboard' }
 | 
			
		||||
        }
 | 
			
		||||
      ]
 | 
			
		||||
    }]
 | 
			
		||||
  }
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
export default [
 | 
			
		||||
  // mock get all routes form server
 | 
			
		||||
  {
 | 
			
		||||
    url: '/routes',
 | 
			
		||||
    type: 'get',
 | 
			
		||||
    response: _ => {
 | 
			
		||||
      return {
 | 
			
		||||
        code: 20000,
 | 
			
		||||
        data: routes
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  // mock get all roles form server
 | 
			
		||||
  {
 | 
			
		||||
    url: '/roles',
 | 
			
		||||
    type: 'get',
 | 
			
		||||
    response: _ => {
 | 
			
		||||
      return {
 | 
			
		||||
        code: 20000,
 | 
			
		||||
        data: roles
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  // add role
 | 
			
		||||
  {
 | 
			
		||||
    url: '/role',
 | 
			
		||||
    type: 'post',
 | 
			
		||||
    response: {
 | 
			
		||||
      code: 20000,
 | 
			
		||||
      data: {
 | 
			
		||||
        key: Mock.mock('@integer(300, 5000)')
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  // update role
 | 
			
		||||
  {
 | 
			
		||||
    url: '/role/[A-Za-z0-9]',
 | 
			
		||||
    type: 'put',
 | 
			
		||||
    response: {
 | 
			
		||||
      code: 20000,
 | 
			
		||||
      data: {
 | 
			
		||||
        status: 'success'
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  // delete role
 | 
			
		||||
  {
 | 
			
		||||
    url: '/role/[A-Za-z0-9]',
 | 
			
		||||
    type: 'delete',
 | 
			
		||||
    response: {
 | 
			
		||||
      code: 20000,
 | 
			
		||||
      data: {
 | 
			
		||||
        status: 'success'
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
]
 | 
			
		||||
							
								
								
									
										525
									
								
								mock/role/routes.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,525 @@
 | 
			
		||||
// Just a mock data
 | 
			
		||||
 | 
			
		||||
export const constantRoutes = [
 | 
			
		||||
  {
 | 
			
		||||
    path: '/redirect',
 | 
			
		||||
    component: 'layout/Layout',
 | 
			
		||||
    hidden: true,
 | 
			
		||||
    children: [
 | 
			
		||||
      {
 | 
			
		||||
        path: '/redirect/:path*',
 | 
			
		||||
        component: 'views/redirect/index'
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: '/login',
 | 
			
		||||
    component: 'views/login/index',
 | 
			
		||||
    hidden: true
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: '/auth-redirect',
 | 
			
		||||
    component: 'views/login/auth-redirect',
 | 
			
		||||
    hidden: true
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: '/404',
 | 
			
		||||
    component: 'views/error-page/404',
 | 
			
		||||
    hidden: true
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: '/401',
 | 
			
		||||
    component: 'views/error-page/401',
 | 
			
		||||
    hidden: true
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: '',
 | 
			
		||||
    component: 'layout/Layout',
 | 
			
		||||
    redirect: 'dashboard',
 | 
			
		||||
    children: [
 | 
			
		||||
      {
 | 
			
		||||
        path: 'dashboard',
 | 
			
		||||
        component: 'views/dashboard/index',
 | 
			
		||||
        name: 'Dashboard',
 | 
			
		||||
        meta: { title: 'Dashboard', icon: 'dashboard', affix: true }
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: '/documentation',
 | 
			
		||||
    component: 'layout/Layout',
 | 
			
		||||
    children: [
 | 
			
		||||
      {
 | 
			
		||||
        path: 'index',
 | 
			
		||||
        component: 'views/documentation/index',
 | 
			
		||||
        name: 'Documentation',
 | 
			
		||||
        meta: { title: 'Documentation', icon: 'documentation', affix: true }
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: '/guide',
 | 
			
		||||
    component: 'layout/Layout',
 | 
			
		||||
    redirect: '/guide/index',
 | 
			
		||||
    children: [
 | 
			
		||||
      {
 | 
			
		||||
        path: 'index',
 | 
			
		||||
        component: 'views/guide/index',
 | 
			
		||||
        name: 'Guide',
 | 
			
		||||
        meta: { title: 'Guide', icon: 'guide', noCache: true }
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  }
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
export const asyncRoutes = [
 | 
			
		||||
  {
 | 
			
		||||
    path: '/permission',
 | 
			
		||||
    component: 'layout/Layout',
 | 
			
		||||
    redirect: '/permission/index',
 | 
			
		||||
    alwaysShow: true,
 | 
			
		||||
    meta: {
 | 
			
		||||
      title: 'Permission',
 | 
			
		||||
      icon: 'lock',
 | 
			
		||||
      roles: ['admin', 'editor']
 | 
			
		||||
    },
 | 
			
		||||
    children: [
 | 
			
		||||
      {
 | 
			
		||||
        path: 'page',
 | 
			
		||||
        component: 'views/permission/page',
 | 
			
		||||
        name: 'PagePermission',
 | 
			
		||||
        meta: {
 | 
			
		||||
          title: 'Page Permission',
 | 
			
		||||
          roles: ['admin']
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'directive',
 | 
			
		||||
        component: 'views/permission/directive',
 | 
			
		||||
        name: 'DirectivePermission',
 | 
			
		||||
        meta: {
 | 
			
		||||
          title: 'Directive Permission'
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'role',
 | 
			
		||||
        component: 'views/permission/role',
 | 
			
		||||
        name: 'RolePermission',
 | 
			
		||||
        meta: {
 | 
			
		||||
          title: 'Role Permission',
 | 
			
		||||
          roles: ['admin']
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  {
 | 
			
		||||
    path: '/icon',
 | 
			
		||||
    component: 'layout/Layout',
 | 
			
		||||
    children: [
 | 
			
		||||
      {
 | 
			
		||||
        path: 'index',
 | 
			
		||||
        component: 'views/icons/index',
 | 
			
		||||
        name: 'Icons',
 | 
			
		||||
        meta: { title: 'Icons', icon: 'icon', noCache: true }
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  {
 | 
			
		||||
    path: '/components',
 | 
			
		||||
    component: 'layout/Layout',
 | 
			
		||||
    redirect: 'noRedirect',
 | 
			
		||||
    name: 'ComponentDemo',
 | 
			
		||||
    meta: {
 | 
			
		||||
      title: 'Components',
 | 
			
		||||
      icon: 'component'
 | 
			
		||||
    },
 | 
			
		||||
    children: [
 | 
			
		||||
      {
 | 
			
		||||
        path: 'tinymce',
 | 
			
		||||
        component: 'views/components-demo/tinymce',
 | 
			
		||||
        name: 'TinymceDemo',
 | 
			
		||||
        meta: { title: 'Tinymce' }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'markdown',
 | 
			
		||||
        component: 'views/components-demo/markdown',
 | 
			
		||||
        name: 'MarkdownDemo',
 | 
			
		||||
        meta: { title: 'Markdown' }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'json-editor',
 | 
			
		||||
        component: 'views/components-demo/json-editor',
 | 
			
		||||
        name: 'JsonEditorDemo',
 | 
			
		||||
        meta: { title: 'Json Editor' }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'split-pane',
 | 
			
		||||
        component: 'views/components-demo/split-pane',
 | 
			
		||||
        name: 'SplitpaneDemo',
 | 
			
		||||
        meta: { title: 'SplitPane' }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'avatar-upload',
 | 
			
		||||
        component: 'views/components-demo/avatar-upload',
 | 
			
		||||
        name: 'AvatarUploadDemo',
 | 
			
		||||
        meta: { title: 'Avatar Upload' }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'dropzone',
 | 
			
		||||
        component: 'views/components-demo/dropzone',
 | 
			
		||||
        name: 'DropzoneDemo',
 | 
			
		||||
        meta: { title: 'Dropzone' }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'sticky',
 | 
			
		||||
        component: 'views/components-demo/sticky',
 | 
			
		||||
        name: 'StickyDemo',
 | 
			
		||||
        meta: { title: 'Sticky' }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'count-to',
 | 
			
		||||
        component: 'views/components-demo/count-to',
 | 
			
		||||
        name: 'CountToDemo',
 | 
			
		||||
        meta: { title: 'Count To' }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'mixin',
 | 
			
		||||
        component: 'views/components-demo/mixin',
 | 
			
		||||
        name: 'ComponentMixinDemo',
 | 
			
		||||
        meta: { title: 'componentMixin' }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'back-to-top',
 | 
			
		||||
        component: 'views/components-demo/back-to-top',
 | 
			
		||||
        name: 'BackToTopDemo',
 | 
			
		||||
        meta: { title: 'Back To Top' }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'drag-dialog',
 | 
			
		||||
        component: 'views/components-demo/drag-dialog',
 | 
			
		||||
        name: 'DragDialogDemo',
 | 
			
		||||
        meta: { title: 'Drag Dialog' }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'drag-select',
 | 
			
		||||
        component: 'views/components-demo/drag-select',
 | 
			
		||||
        name: 'DragSelectDemo',
 | 
			
		||||
        meta: { title: 'Drag Select' }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'dnd-list',
 | 
			
		||||
        component: 'views/components-demo/dnd-list',
 | 
			
		||||
        name: 'DndListDemo',
 | 
			
		||||
        meta: { title: 'Dnd List' }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'drag-kanban',
 | 
			
		||||
        component: 'views/components-demo/drag-kanban',
 | 
			
		||||
        name: 'DragKanbanDemo',
 | 
			
		||||
        meta: { title: 'Drag Kanban' }
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: '/charts',
 | 
			
		||||
    component: 'layout/Layout',
 | 
			
		||||
    redirect: 'noRedirect',
 | 
			
		||||
    name: 'Charts',
 | 
			
		||||
    meta: {
 | 
			
		||||
      title: 'Charts',
 | 
			
		||||
      icon: 'chart'
 | 
			
		||||
    },
 | 
			
		||||
    children: [
 | 
			
		||||
      {
 | 
			
		||||
        path: 'keyboard',
 | 
			
		||||
        component: 'views/charts/keyboard',
 | 
			
		||||
        name: 'KeyboardChart',
 | 
			
		||||
        meta: { title: 'Keyboard Chart', noCache: true }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'line',
 | 
			
		||||
        component: 'views/charts/line',
 | 
			
		||||
        name: 'LineChart',
 | 
			
		||||
        meta: { title: 'Line Chart', noCache: true }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'mixchart',
 | 
			
		||||
        component: 'views/charts/mixChart',
 | 
			
		||||
        name: 'MixChart',
 | 
			
		||||
        meta: { title: 'Mix Chart', noCache: true }
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: '/nested',
 | 
			
		||||
    component: 'layout/Layout',
 | 
			
		||||
    redirect: '/nested/menu1/menu1-1',
 | 
			
		||||
    name: 'Nested',
 | 
			
		||||
    meta: {
 | 
			
		||||
      title: 'Nested',
 | 
			
		||||
      icon: 'nested'
 | 
			
		||||
    },
 | 
			
		||||
    children: [
 | 
			
		||||
      {
 | 
			
		||||
        path: 'menu1',
 | 
			
		||||
        component: 'views/nested/menu1/index',
 | 
			
		||||
        name: 'Menu1',
 | 
			
		||||
        meta: { title: 'Menu1' },
 | 
			
		||||
        redirect: '/nested/menu1/menu1-1',
 | 
			
		||||
        children: [
 | 
			
		||||
          {
 | 
			
		||||
            path: 'menu1-1',
 | 
			
		||||
            component: 'views/nested/menu1/menu1-1',
 | 
			
		||||
            name: 'Menu1-1',
 | 
			
		||||
            meta: { title: 'Menu1-1' }
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'menu1-2',
 | 
			
		||||
            component: 'views/nested/menu1/menu1-2',
 | 
			
		||||
            name: 'Menu1-2',
 | 
			
		||||
            redirect: '/nested/menu1/menu1-2/menu1-2-1',
 | 
			
		||||
            meta: { title: 'Menu1-2' },
 | 
			
		||||
            children: [
 | 
			
		||||
              {
 | 
			
		||||
                path: 'menu1-2-1',
 | 
			
		||||
                component: 'views/nested/menu1/menu1-2/menu1-2-1',
 | 
			
		||||
                name: 'Menu1-2-1',
 | 
			
		||||
                meta: { title: 'Menu1-2-1' }
 | 
			
		||||
              },
 | 
			
		||||
              {
 | 
			
		||||
                path: 'menu1-2-2',
 | 
			
		||||
                component: 'views/nested/menu1/menu1-2/menu1-2-2',
 | 
			
		||||
                name: 'Menu1-2-2',
 | 
			
		||||
                meta: { title: 'Menu1-2-2' }
 | 
			
		||||
              }
 | 
			
		||||
            ]
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'menu1-3',
 | 
			
		||||
            component: 'views/nested/menu1/menu1-3',
 | 
			
		||||
            name: 'Menu1-3',
 | 
			
		||||
            meta: { title: 'Menu1-3' }
 | 
			
		||||
          }
 | 
			
		||||
        ]
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'menu2',
 | 
			
		||||
        name: 'Menu2',
 | 
			
		||||
        component: 'views/nested/menu2/index',
 | 
			
		||||
        meta: { title: 'Menu2' }
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  {
 | 
			
		||||
    path: '/example',
 | 
			
		||||
    component: 'layout/Layout',
 | 
			
		||||
    redirect: '/example/list',
 | 
			
		||||
    name: 'Example',
 | 
			
		||||
    meta: {
 | 
			
		||||
      title: 'Example',
 | 
			
		||||
      icon: 'example'
 | 
			
		||||
    },
 | 
			
		||||
    children: [
 | 
			
		||||
      {
 | 
			
		||||
        path: 'create',
 | 
			
		||||
        component: 'views/example/create',
 | 
			
		||||
        name: 'CreateArticle',
 | 
			
		||||
        meta: { title: 'Create Article', icon: 'edit' }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'edit/:id(\\d+)',
 | 
			
		||||
        component: 'views/example/edit',
 | 
			
		||||
        name: 'EditArticle',
 | 
			
		||||
        meta: { title: 'Edit Article', noCache: true },
 | 
			
		||||
        hidden: true
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'list',
 | 
			
		||||
        component: 'views/example/list',
 | 
			
		||||
        name: 'ArticleList',
 | 
			
		||||
        meta: { title: 'Article List', icon: 'list' }
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  {
 | 
			
		||||
    path: '/tab',
 | 
			
		||||
    component: 'layout/Layout',
 | 
			
		||||
    children: [
 | 
			
		||||
      {
 | 
			
		||||
        path: 'index',
 | 
			
		||||
        component: 'views/tab/index',
 | 
			
		||||
        name: 'Tab',
 | 
			
		||||
        meta: { title: 'Tab', icon: 'tab' }
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  {
 | 
			
		||||
    path: '/error',
 | 
			
		||||
    component: 'layout/Layout',
 | 
			
		||||
    redirect: 'noRedirect',
 | 
			
		||||
    name: 'ErrorPages',
 | 
			
		||||
    meta: {
 | 
			
		||||
      title: 'Error Pages',
 | 
			
		||||
      icon: '404'
 | 
			
		||||
    },
 | 
			
		||||
    children: [
 | 
			
		||||
      {
 | 
			
		||||
        path: '401',
 | 
			
		||||
        component: 'views/error-page/401',
 | 
			
		||||
        name: 'Page401',
 | 
			
		||||
        meta: { title: 'Page 401', noCache: true }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: '404',
 | 
			
		||||
        component: 'views/error-page/404',
 | 
			
		||||
        name: 'Page404',
 | 
			
		||||
        meta: { title: 'Page 404', noCache: true }
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  {
 | 
			
		||||
    path: '/error-log',
 | 
			
		||||
    component: 'layout/Layout',
 | 
			
		||||
    redirect: 'noRedirect',
 | 
			
		||||
    children: [
 | 
			
		||||
      {
 | 
			
		||||
        path: 'log',
 | 
			
		||||
        component: 'views/error-log/index',
 | 
			
		||||
        name: 'ErrorLog',
 | 
			
		||||
        meta: { title: 'Error Log', icon: 'bug' }
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  {
 | 
			
		||||
    path: '/excel',
 | 
			
		||||
    component: 'layout/Layout',
 | 
			
		||||
    redirect: '/excel/export-excel',
 | 
			
		||||
    name: 'Excel',
 | 
			
		||||
    meta: {
 | 
			
		||||
      title: 'Excel',
 | 
			
		||||
      icon: 'excel'
 | 
			
		||||
    },
 | 
			
		||||
    children: [
 | 
			
		||||
      {
 | 
			
		||||
        path: 'export-excel',
 | 
			
		||||
        component: 'views/excel/export-excel',
 | 
			
		||||
        name: 'ExportExcel',
 | 
			
		||||
        meta: { title: 'Export Excel' }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'export-selected-excel',
 | 
			
		||||
        component: 'views/excel/select-excel',
 | 
			
		||||
        name: 'SelectExcel',
 | 
			
		||||
        meta: { title: 'Select Excel' }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'export-merge-header',
 | 
			
		||||
        component: 'views/excel/merge-header',
 | 
			
		||||
        name: 'MergeHeader',
 | 
			
		||||
        meta: { title: 'Merge Header' }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'upload-excel',
 | 
			
		||||
        component: 'views/excel/upload-excel',
 | 
			
		||||
        name: 'UploadExcel',
 | 
			
		||||
        meta: { title: 'Upload Excel' }
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  {
 | 
			
		||||
    path: '/zip',
 | 
			
		||||
    component: 'layout/Layout',
 | 
			
		||||
    redirect: '/zip/download',
 | 
			
		||||
    alwaysShow: true,
 | 
			
		||||
    meta: { title: 'Zip', icon: 'zip' },
 | 
			
		||||
    children: [
 | 
			
		||||
      {
 | 
			
		||||
        path: 'download',
 | 
			
		||||
        component: 'views/zip/index',
 | 
			
		||||
        name: 'ExportZip',
 | 
			
		||||
        meta: { title: 'Export Zip' }
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  {
 | 
			
		||||
    path: '/pdf',
 | 
			
		||||
    component: 'layout/Layout',
 | 
			
		||||
    redirect: '/pdf/index',
 | 
			
		||||
    children: [
 | 
			
		||||
      {
 | 
			
		||||
        path: 'index',
 | 
			
		||||
        component: 'views/pdf/index',
 | 
			
		||||
        name: 'PDF',
 | 
			
		||||
        meta: { title: 'PDF', icon: 'pdf' }
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: '/pdf/download',
 | 
			
		||||
    component: 'views/pdf/download',
 | 
			
		||||
    hidden: true
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  {
 | 
			
		||||
    path: '/theme',
 | 
			
		||||
    component: 'layout/Layout',
 | 
			
		||||
    redirect: 'noRedirect',
 | 
			
		||||
    children: [
 | 
			
		||||
      {
 | 
			
		||||
        path: 'index',
 | 
			
		||||
        component: 'views/theme/index',
 | 
			
		||||
        name: 'Theme',
 | 
			
		||||
        meta: { title: 'Theme', icon: 'theme' }
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  {
 | 
			
		||||
    path: '/clipboard',
 | 
			
		||||
    component: 'layout/Layout',
 | 
			
		||||
    redirect: 'noRedirect',
 | 
			
		||||
    children: [
 | 
			
		||||
      {
 | 
			
		||||
        path: 'index',
 | 
			
		||||
        component: 'views/clipboard/index',
 | 
			
		||||
        name: 'ClipboardDemo',
 | 
			
		||||
        meta: { title: 'Clipboard Demo', icon: 'clipboard' }
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  {
 | 
			
		||||
    path: '/i18n',
 | 
			
		||||
    component: 'layout/Layout',
 | 
			
		||||
    children: [
 | 
			
		||||
      {
 | 
			
		||||
        path: 'index',
 | 
			
		||||
        component: 'views/i18n-demo/index',
 | 
			
		||||
        name: 'I18n',
 | 
			
		||||
        meta: { title: 'I18n', icon: 'international' }
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  {
 | 
			
		||||
    path: 'external-link',
 | 
			
		||||
    component: 'layout/Layout',
 | 
			
		||||
    children: [
 | 
			
		||||
      {
 | 
			
		||||
        path: 'https://github.com/PanJiaChen/vue-element-admin',
 | 
			
		||||
        meta: { title: 'External Link', icon: 'link' }
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  { path: '*', redirect: '/404', hidden: true }
 | 
			
		||||
]
 | 
			
		||||
							
								
								
									
										84
									
								
								mock/user.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,84 @@
 | 
			
		||||
 | 
			
		||||
const tokens = {
 | 
			
		||||
  admin: {
 | 
			
		||||
    token: 'admin-token'
 | 
			
		||||
  },
 | 
			
		||||
  editor: {
 | 
			
		||||
    token: 'editor-token'
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const users = {
 | 
			
		||||
  'admin-token': {
 | 
			
		||||
    roles: ['admin'],
 | 
			
		||||
    introduction: 'I am a super administrator',
 | 
			
		||||
    avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
 | 
			
		||||
    name: 'Super Admin'
 | 
			
		||||
  },
 | 
			
		||||
  'editor-token': {
 | 
			
		||||
    roles: ['editor'],
 | 
			
		||||
    introduction: 'Backend Developer bei RedBull',
 | 
			
		||||
    avatar: 'https://lbsfilm.at/media/pages/about/1056669128-1567191147/square-small.jpg',
 | 
			
		||||
    name: 'Lukas Bachschwell'
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default [
 | 
			
		||||
  // user login
 | 
			
		||||
  {
 | 
			
		||||
    url: '/user/login',
 | 
			
		||||
    type: 'post',
 | 
			
		||||
    response: config => {
 | 
			
		||||
      const { username } = config.body
 | 
			
		||||
      const token = tokens[username]
 | 
			
		||||
 | 
			
		||||
      // mock error
 | 
			
		||||
      if (!token) {
 | 
			
		||||
        return {
 | 
			
		||||
          code: 60204,
 | 
			
		||||
          message: 'Account and password are incorrect.'
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return {
 | 
			
		||||
        code: 20000,
 | 
			
		||||
        data: token
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  // get user info
 | 
			
		||||
  {
 | 
			
		||||
    url: '/user/info\.*',
 | 
			
		||||
    type: 'get',
 | 
			
		||||
    response: config => {
 | 
			
		||||
      const { token } = config.query
 | 
			
		||||
      const info = users[token]
 | 
			
		||||
 | 
			
		||||
      // mock error
 | 
			
		||||
      if (!info) {
 | 
			
		||||
        return {
 | 
			
		||||
          code: 50008,
 | 
			
		||||
          message: 'Login failed, unable to get user details.'
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return {
 | 
			
		||||
        code: 20000,
 | 
			
		||||
        data: info
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  // user logout
 | 
			
		||||
  {
 | 
			
		||||
    url: '/user/logout',
 | 
			
		||||
    type: 'post',
 | 
			
		||||
    response: _ => {
 | 
			
		||||
      return {
 | 
			
		||||
        code: 20000,
 | 
			
		||||
        data: 'success'
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
]
 | 
			
		||||
							
								
								
									
										106
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,106 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "join-project",
 | 
			
		||||
  "version": "4.2.1",
 | 
			
		||||
  "description": "Joinbot Vue Admin vased on vue-element-admin by Pan <panfree23@gmail.com>",
 | 
			
		||||
  "author": "Lukas Bachschwell <lukas@lbsfilm.at>",
 | 
			
		||||
  "license": "MIT",
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "dev": "vue-cli-service serve",
 | 
			
		||||
    "build:prod": "vue-cli-service build",
 | 
			
		||||
    "build:stage": "vue-cli-service build --mode staging",
 | 
			
		||||
    "preview": "node build/index.js --preview",
 | 
			
		||||
    "lint": "eslint --ext .js,.vue src",
 | 
			
		||||
    "test:unit": "jest --clearCache && vue-cli-service test:unit",
 | 
			
		||||
    "test:ci": "npm run lint && npm run test:unit",
 | 
			
		||||
    "svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml",
 | 
			
		||||
    "new": "plop"
 | 
			
		||||
  },
 | 
			
		||||
  "husky": {
 | 
			
		||||
    "hooks": {
 | 
			
		||||
      "pre-commit": "lint-staged"
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "lint-staged": {
 | 
			
		||||
    "src/**/*.{js,vue}": [
 | 
			
		||||
      "eslint --fix",
 | 
			
		||||
      "git add"
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  "keywords": [
 | 
			
		||||
    "vue",
 | 
			
		||||
    "admin",
 | 
			
		||||
    "dashboard",
 | 
			
		||||
    "element-ui",
 | 
			
		||||
    "boilerplate",
 | 
			
		||||
    "admin-template",
 | 
			
		||||
    "management-system"
 | 
			
		||||
  ],
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "axios": "0.18.1",
 | 
			
		||||
    "clipboard": "2.0.4",
 | 
			
		||||
    "codemirror": "5.45.0",
 | 
			
		||||
    "driver.js": "0.9.5",
 | 
			
		||||
    "dropzone": "5.5.1",
 | 
			
		||||
    "echarts": "4.2.1",
 | 
			
		||||
    "element-ui": "2.7.0",
 | 
			
		||||
    "file-saver": "2.0.1",
 | 
			
		||||
    "fuse.js": "3.4.4",
 | 
			
		||||
    "js-cookie": "2.2.0",
 | 
			
		||||
    "jsonlint": "1.6.3",
 | 
			
		||||
    "jszip": "3.2.1",
 | 
			
		||||
    "normalize.css": "7.0.0",
 | 
			
		||||
    "nprogress": "0.2.0",
 | 
			
		||||
    "path-to-regexp": "2.4.0",
 | 
			
		||||
    "screenfull": "4.2.0",
 | 
			
		||||
    "showdown": "1.9.0",
 | 
			
		||||
    "sortablejs": "1.8.4",
 | 
			
		||||
    "tui-editor": "1.3.3",
 | 
			
		||||
    "vue": "2.6.10",
 | 
			
		||||
    "vue-count-to": "1.0.13",
 | 
			
		||||
    "vue-router": "3.0.2",
 | 
			
		||||
    "vue-splitpane": "1.0.4",
 | 
			
		||||
    "vuedraggable": "2.20.0",
 | 
			
		||||
    "vuex": "3.1.0",
 | 
			
		||||
    "xlsx": "0.14.1"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@babel/core": "7.0.0",
 | 
			
		||||
    "@babel/register": "7.0.0",
 | 
			
		||||
    "@vue/cli-plugin-babel": "3.5.3",
 | 
			
		||||
    "@vue/cli-plugin-eslint": "^3.9.1",
 | 
			
		||||
    "@vue/cli-plugin-unit-jest": "3.5.3",
 | 
			
		||||
    "@vue/cli-service": "3.5.3",
 | 
			
		||||
    "@vue/test-utils": "1.0.0-beta.29",
 | 
			
		||||
    "autoprefixer": "^9.5.1",
 | 
			
		||||
    "babel-core": "7.0.0-bridge.0",
 | 
			
		||||
    "babel-eslint": "10.0.1",
 | 
			
		||||
    "babel-jest": "23.6.0",
 | 
			
		||||
    "chalk": "2.4.2",
 | 
			
		||||
    "chokidar": "2.1.5",
 | 
			
		||||
    "connect": "3.6.6",
 | 
			
		||||
    "eslint": "5.15.3",
 | 
			
		||||
    "eslint-plugin-vue": "5.2.2",
 | 
			
		||||
    "html-webpack-plugin": "3.2.0",
 | 
			
		||||
    "husky": "1.3.1",
 | 
			
		||||
    "lint-staged": "8.1.5",
 | 
			
		||||
    "mockjs": "1.0.1-beta3",
 | 
			
		||||
    "node-sass": "^4.9.0",
 | 
			
		||||
    "plop": "2.3.0",
 | 
			
		||||
    "runjs": "^4.3.2",
 | 
			
		||||
    "sass-loader": "^7.1.0",
 | 
			
		||||
    "script-ext-html-webpack-plugin": "2.1.3",
 | 
			
		||||
    "script-loader": "0.7.2",
 | 
			
		||||
    "serve-static": "^1.13.2",
 | 
			
		||||
    "svg-sprite-loader": "4.1.3",
 | 
			
		||||
    "svgo": "1.2.0",
 | 
			
		||||
    "vue-template-compiler": "2.6.10"
 | 
			
		||||
  },
 | 
			
		||||
  "engines": {
 | 
			
		||||
    "node": ">=8.9",
 | 
			
		||||
    "npm": ">= 3.0.0"
 | 
			
		||||
  },
 | 
			
		||||
  "browserslist": [
 | 
			
		||||
    "> 1%",
 | 
			
		||||
    "last 2 versions"
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										26
									
								
								plop-templates/component/index.hbs
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,26 @@
 | 
			
		||||
{{#if template}}
 | 
			
		||||
<template>
 | 
			
		||||
  <div />
 | 
			
		||||
</template>
 | 
			
		||||
{{/if}}
 | 
			
		||||
 | 
			
		||||
{{#if script}}
 | 
			
		||||
<script>
 | 
			
		||||
export default {
 | 
			
		||||
  name: '{{ properCase name }}',
 | 
			
		||||
  props: {},
 | 
			
		||||
  data() {
 | 
			
		||||
    return {}
 | 
			
		||||
  },
 | 
			
		||||
  created() {},
 | 
			
		||||
  mounted() {},
 | 
			
		||||
  methods: {}
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
{{/if}}
 | 
			
		||||
 | 
			
		||||
{{#if style}}
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
 | 
			
		||||
</style>
 | 
			
		||||
{{/if}}
 | 
			
		||||
							
								
								
									
										55
									
								
								plop-templates/component/prompt.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,55 @@
 | 
			
		||||
const { notEmpty } = require('../utils.js')
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
  description: 'generate vue component',
 | 
			
		||||
  prompts: [{
 | 
			
		||||
    type: 'input',
 | 
			
		||||
    name: 'name',
 | 
			
		||||
    message: 'component name please',
 | 
			
		||||
    validate: notEmpty('name')
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    type: 'checkbox',
 | 
			
		||||
    name: 'blocks',
 | 
			
		||||
    message: 'Blocks:',
 | 
			
		||||
    choices: [{
 | 
			
		||||
      name: '<template>',
 | 
			
		||||
      value: 'template',
 | 
			
		||||
      checked: true
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      name: '<script>',
 | 
			
		||||
      value: 'script',
 | 
			
		||||
      checked: true
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      name: 'style',
 | 
			
		||||
      value: 'style',
 | 
			
		||||
      checked: true
 | 
			
		||||
    }
 | 
			
		||||
    ],
 | 
			
		||||
    validate(value) {
 | 
			
		||||
      if (value.indexOf('script') === -1 && value.indexOf('template') === -1) {
 | 
			
		||||
        return 'Components require at least a <script> or <template> tag.'
 | 
			
		||||
      }
 | 
			
		||||
      return true
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  ],
 | 
			
		||||
  actions: data => {
 | 
			
		||||
    const name = '{{properCase name}}'
 | 
			
		||||
    const actions = [{
 | 
			
		||||
      type: 'add',
 | 
			
		||||
      path: `src/components/${name}/index.vue`,
 | 
			
		||||
      templateFile: 'plop-templates/component/index.hbs',
 | 
			
		||||
      data: {
 | 
			
		||||
        name: name,
 | 
			
		||||
        template: data.blocks.includes('template'),
 | 
			
		||||
        script: data.blocks.includes('script'),
 | 
			
		||||
        style: data.blocks.includes('style')
 | 
			
		||||
      }
 | 
			
		||||
    }]
 | 
			
		||||
 | 
			
		||||
    return actions
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										9
									
								
								plop-templates/utils.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,9 @@
 | 
			
		||||
exports.notEmpty = name => {
 | 
			
		||||
  return v => {
 | 
			
		||||
    if (!v || v.trim === '') {
 | 
			
		||||
      return `${name} is required`
 | 
			
		||||
    } else {
 | 
			
		||||
      return true
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										26
									
								
								plop-templates/view/index.hbs
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,26 @@
 | 
			
		||||
{{#if template}}
 | 
			
		||||
<template>
 | 
			
		||||
  <div />
 | 
			
		||||
</template>
 | 
			
		||||
{{/if}}
 | 
			
		||||
 | 
			
		||||
{{#if script}}
 | 
			
		||||
<script>
 | 
			
		||||
export default {
 | 
			
		||||
  name: '{{ properCase name }}',
 | 
			
		||||
  props: {},
 | 
			
		||||
  data() {
 | 
			
		||||
    return {}
 | 
			
		||||
  },
 | 
			
		||||
  created() {},
 | 
			
		||||
  mounted() {},
 | 
			
		||||
  methods: {}
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
{{/if}}
 | 
			
		||||
 | 
			
		||||
{{#if style}}
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
 | 
			
		||||
</style>
 | 
			
		||||
{{/if}}
 | 
			
		||||
							
								
								
									
										55
									
								
								plop-templates/view/prompt.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,55 @@
 | 
			
		||||
const { notEmpty } = require('../utils.js')
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
  description: 'generate a view',
 | 
			
		||||
  prompts: [{
 | 
			
		||||
    type: 'input',
 | 
			
		||||
    name: 'name',
 | 
			
		||||
    message: 'view name please',
 | 
			
		||||
    validate: notEmpty('name')
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    type: 'checkbox',
 | 
			
		||||
    name: 'blocks',
 | 
			
		||||
    message: 'Blocks:',
 | 
			
		||||
    choices: [{
 | 
			
		||||
      name: '<template>',
 | 
			
		||||
      value: 'template',
 | 
			
		||||
      checked: true
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      name: '<script>',
 | 
			
		||||
      value: 'script',
 | 
			
		||||
      checked: true
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      name: 'style',
 | 
			
		||||
      value: 'style',
 | 
			
		||||
      checked: true
 | 
			
		||||
    }
 | 
			
		||||
    ],
 | 
			
		||||
    validate(value) {
 | 
			
		||||
      if (value.indexOf('script') === -1 && value.indexOf('template') === -1) {
 | 
			
		||||
        return 'View require at least a <script> or <template> tag.'
 | 
			
		||||
      }
 | 
			
		||||
      return true
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  ],
 | 
			
		||||
  actions: data => {
 | 
			
		||||
    const name = '{{name}}'
 | 
			
		||||
    const actions = [{
 | 
			
		||||
      type: 'add',
 | 
			
		||||
      path: `src/views/${name}/index.vue`,
 | 
			
		||||
      templateFile: 'plop-templates/view/index.hbs',
 | 
			
		||||
      data: {
 | 
			
		||||
        name: name,
 | 
			
		||||
        template: data.blocks.includes('template'),
 | 
			
		||||
        script: data.blocks.includes('script'),
 | 
			
		||||
        style: data.blocks.includes('style')
 | 
			
		||||
      }
 | 
			
		||||
    }]
 | 
			
		||||
 | 
			
		||||
    return actions
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										7
									
								
								plopfile.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,7 @@
 | 
			
		||||
const viewGenerator = require('./plop-templates/view/prompt')
 | 
			
		||||
const componentGenerator = require('./plop-templates/component/prompt')
 | 
			
		||||
 | 
			
		||||
module.exports = function(plop) {
 | 
			
		||||
  plop.setGenerator('view', viewGenerator)
 | 
			
		||||
  plop.setGenerator('component', componentGenerator)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										5
									
								
								postcss.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,5 @@
 | 
			
		||||
module.exports = {
 | 
			
		||||
  plugins: {
 | 
			
		||||
    autoprefixer: {}
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								public/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 6.2 KiB  | 
							
								
								
									
										19
									
								
								public/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,19 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html>
 | 
			
		||||
 | 
			
		||||
<head>
 | 
			
		||||
  <meta charset="utf-8">
 | 
			
		||||
  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
 | 
			
		||||
  <meta name="renderer" content="webkit">
 | 
			
		||||
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
 | 
			
		||||
  <script src="http://localhost:3000/assets/modules/channel-web/inject.js"></script>
 | 
			
		||||
  <link rel="icon" href="<%= BASE_URL %>favicon.ico">
 | 
			
		||||
  <title><%= webpackConfig.name %></title>
 | 
			
		||||
</head>
 | 
			
		||||
 | 
			
		||||
<body>
 | 
			
		||||
  <div id="app"></div>
 | 
			
		||||
  <!-- built files will be auto injected -->
 | 
			
		||||
</body>
 | 
			
		||||
 | 
			
		||||
</html>
 | 
			
		||||
							
								
								
									
										11
									
								
								src/App.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,11 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div id="app">
 | 
			
		||||
    <router-view />
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'App'
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
							
								
								
									
										41
									
								
								src/api/article.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,41 @@
 | 
			
		||||
import request from '@/utils/request'
 | 
			
		||||
 | 
			
		||||
export function fetchList(query) {
 | 
			
		||||
  return request({
 | 
			
		||||
    url: '/article/list',
 | 
			
		||||
    method: 'get',
 | 
			
		||||
    params: query
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function fetchArticle(id) {
 | 
			
		||||
  return request({
 | 
			
		||||
    url: '/article/detail',
 | 
			
		||||
    method: 'get',
 | 
			
		||||
    params: { id }
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function fetchPv(pv) {
 | 
			
		||||
  return request({
 | 
			
		||||
    url: '/article/pv',
 | 
			
		||||
    method: 'get',
 | 
			
		||||
    params: { pv }
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function createArticle(data) {
 | 
			
		||||
  return request({
 | 
			
		||||
    url: '/article/create',
 | 
			
		||||
    method: 'post',
 | 
			
		||||
    data
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function updateArticle(data) {
 | 
			
		||||
  return request({
 | 
			
		||||
    url: '/article/update',
 | 
			
		||||
    method: 'post',
 | 
			
		||||
    data
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										8
									
								
								src/api/qiniu.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,8 @@
 | 
			
		||||
import request from '@/utils/request'
 | 
			
		||||
 | 
			
		||||
export function getToken() {
 | 
			
		||||
  return request({
 | 
			
		||||
    url: '/qiniu/upload/token', // 假地址 自行替换
 | 
			
		||||
    method: 'get'
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										17
									
								
								src/api/remote-search.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,17 @@
 | 
			
		||||
import request from '@/utils/request'
 | 
			
		||||
 | 
			
		||||
export function searchUser(name) {
 | 
			
		||||
  return request({
 | 
			
		||||
    url: '/search/user',
 | 
			
		||||
    method: 'get',
 | 
			
		||||
    params: { name }
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function transactionList(query) {
 | 
			
		||||
  return request({
 | 
			
		||||
    url: '/transaction/list',
 | 
			
		||||
    method: 'get',
 | 
			
		||||
    params: query
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										38
									
								
								src/api/role.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,38 @@
 | 
			
		||||
import request from '@/utils/request'
 | 
			
		||||
 | 
			
		||||
export function getRoutes() {
 | 
			
		||||
  return request({
 | 
			
		||||
    url: '/routes',
 | 
			
		||||
    method: 'get'
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getRoles() {
 | 
			
		||||
  return request({
 | 
			
		||||
    url: '/roles',
 | 
			
		||||
    method: 'get'
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function addRole(data) {
 | 
			
		||||
  return request({
 | 
			
		||||
    url: '/role',
 | 
			
		||||
    method: 'post',
 | 
			
		||||
    data
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function updateRole(id, data) {
 | 
			
		||||
  return request({
 | 
			
		||||
    url: `/role/${id}`,
 | 
			
		||||
    method: 'put',
 | 
			
		||||
    data
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function deleteRole(id) {
 | 
			
		||||
  return request({
 | 
			
		||||
    url: `/role/${id}`,
 | 
			
		||||
    method: 'delete'
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										24
									
								
								src/api/user.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,24 @@
 | 
			
		||||
import request from '@/utils/request'
 | 
			
		||||
 | 
			
		||||
export function login(data) {
 | 
			
		||||
  return request({
 | 
			
		||||
    url: '/user/login',
 | 
			
		||||
    method: 'post',
 | 
			
		||||
    data
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getInfo(token) {
 | 
			
		||||
  return request({
 | 
			
		||||
    url: '/user/info',
 | 
			
		||||
    method: 'get',
 | 
			
		||||
    params: { token }
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function logout() {
 | 
			
		||||
  return request({
 | 
			
		||||
    url: '/user/logout',
 | 
			
		||||
    method: 'post'
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								src/assets/401_images/401.gif
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 160 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/404_images/404.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 96 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/404_images/404_cloud.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 4.7 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/custom-theme/fonts/element-icons.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								src/assets/custom-theme/fonts/element-icons.woff
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										1
									
								
								src/assets/custom-theme/index.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								src/assets/logo/join_logo.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 24 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/logo/joinlogosquare.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 10 KiB  | 
							
								
								
									
										111
									
								
								src/components/BackToTop/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,111 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <transition :name="transitionName">
 | 
			
		||||
    <div v-show="visible" :style="customStyle" class="back-to-ceiling" @click="backToTop">
 | 
			
		||||
      <svg width="16" height="16" viewBox="0 0 17 17" xmlns="http://www.w3.org/2000/svg" class="Icon Icon--backToTopArrow" aria-hidden="true" style="height:16px;width:16px"><path d="M12.036 15.59a1 1 0 0 1-.997.995H5.032a.996.996 0 0 1-.997-.996V8.584H1.03c-1.1 0-1.36-.633-.578-1.416L7.33.29a1.003 1.003 0 0 1 1.412 0l6.878 6.88c.782.78.523 1.415-.58 1.415h-3.004v7.004z" /></svg>
 | 
			
		||||
    </div>
 | 
			
		||||
  </transition>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'BackToTop',
 | 
			
		||||
  props: {
 | 
			
		||||
    visibilityHeight: {
 | 
			
		||||
      type: Number,
 | 
			
		||||
      default: 400
 | 
			
		||||
    },
 | 
			
		||||
    backPosition: {
 | 
			
		||||
      type: Number,
 | 
			
		||||
      default: 0
 | 
			
		||||
    },
 | 
			
		||||
    customStyle: {
 | 
			
		||||
      type: Object,
 | 
			
		||||
      default: function() {
 | 
			
		||||
        return {
 | 
			
		||||
          right: '50px',
 | 
			
		||||
          bottom: '50px',
 | 
			
		||||
          width: '40px',
 | 
			
		||||
          height: '40px',
 | 
			
		||||
          'border-radius': '4px',
 | 
			
		||||
          'line-height': '45px',
 | 
			
		||||
          background: '#e7eaf1'
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    transitionName: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      default: 'fade'
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      visible: false,
 | 
			
		||||
      interval: null,
 | 
			
		||||
      isMoving: false
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  mounted() {
 | 
			
		||||
    window.addEventListener('scroll', this.handleScroll)
 | 
			
		||||
  },
 | 
			
		||||
  beforeDestroy() {
 | 
			
		||||
    window.removeEventListener('scroll', this.handleScroll)
 | 
			
		||||
    if (this.interval) {
 | 
			
		||||
      clearInterval(this.interval)
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    handleScroll() {
 | 
			
		||||
      this.visible = window.pageYOffset > this.visibilityHeight
 | 
			
		||||
    },
 | 
			
		||||
    backToTop() {
 | 
			
		||||
      if (this.isMoving) return
 | 
			
		||||
      const start = window.pageYOffset
 | 
			
		||||
      let i = 0
 | 
			
		||||
      this.isMoving = true
 | 
			
		||||
      this.interval = setInterval(() => {
 | 
			
		||||
        const next = Math.floor(this.easeInOutQuad(10 * i, start, -start, 500))
 | 
			
		||||
        if (next <= this.backPosition) {
 | 
			
		||||
          window.scrollTo(0, this.backPosition)
 | 
			
		||||
          clearInterval(this.interval)
 | 
			
		||||
          this.isMoving = false
 | 
			
		||||
        } else {
 | 
			
		||||
          window.scrollTo(0, next)
 | 
			
		||||
        }
 | 
			
		||||
        i++
 | 
			
		||||
      }, 16.7)
 | 
			
		||||
    },
 | 
			
		||||
    easeInOutQuad(t, b, c, d) {
 | 
			
		||||
      if ((t /= d / 2) < 1) return c / 2 * t * t + b
 | 
			
		||||
      return -c / 2 * (--t * (t - 2) - 1) + b
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
.back-to-ceiling {
 | 
			
		||||
  position: fixed;
 | 
			
		||||
  display: inline-block;
 | 
			
		||||
  text-align: center;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.back-to-ceiling:hover {
 | 
			
		||||
  background: #d5dbe7;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.fade-enter-active,
 | 
			
		||||
.fade-leave-active {
 | 
			
		||||
  transition: opacity .5s;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.fade-enter,
 | 
			
		||||
.fade-leave-to {
 | 
			
		||||
  opacity: 0
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.back-to-ceiling .Icon {
 | 
			
		||||
  fill: #9aaabf;
 | 
			
		||||
  background: none;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										82
									
								
								src/components/Breadcrumb/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,82 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <el-breadcrumb class="app-breadcrumb" separator="/">
 | 
			
		||||
    <transition-group name="breadcrumb">
 | 
			
		||||
      <el-breadcrumb-item v-for="(item,index) in levelList" :key="item.path">
 | 
			
		||||
        <span v-if="item.redirect==='noRedirect'||index==levelList.length-1" class="no-redirect">{{ item.meta.title }}</span>
 | 
			
		||||
        <a v-else @click.prevent="handleLink(item)">{{ item.meta.title }}</a>
 | 
			
		||||
      </el-breadcrumb-item>
 | 
			
		||||
    </transition-group>
 | 
			
		||||
  </el-breadcrumb>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import pathToRegexp from 'path-to-regexp'
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      levelList: null
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  watch: {
 | 
			
		||||
    $route(route) {
 | 
			
		||||
      // if you go to the redirect page, do not update the breadcrumbs
 | 
			
		||||
      if (route.path.startsWith('/redirect/')) {
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
      this.getBreadcrumb()
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  created() {
 | 
			
		||||
    this.getBreadcrumb()
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    getBreadcrumb() {
 | 
			
		||||
      // only show routes with meta.title
 | 
			
		||||
      let matched = this.$route.matched.filter(item => item.meta && item.meta.title)
 | 
			
		||||
      const first = matched[0]
 | 
			
		||||
 | 
			
		||||
      if (!this.isDashboard(first)) {
 | 
			
		||||
        matched = [{ path: '/dashboard', meta: { title: 'Dashboard' }}].concat(matched)
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      this.levelList = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
 | 
			
		||||
    },
 | 
			
		||||
    isDashboard(route) {
 | 
			
		||||
      const name = route && route.name
 | 
			
		||||
      if (!name) {
 | 
			
		||||
        return false
 | 
			
		||||
      }
 | 
			
		||||
      return name.trim().toLocaleLowerCase() === 'Dashboard'.toLocaleLowerCase()
 | 
			
		||||
    },
 | 
			
		||||
    pathCompile(path) {
 | 
			
		||||
      // To solve this problem https://github.com/PanJiaChen/vue-element-admin/issues/561
 | 
			
		||||
      const { params } = this.$route
 | 
			
		||||
      var toPath = pathToRegexp.compile(path)
 | 
			
		||||
      return toPath(params)
 | 
			
		||||
    },
 | 
			
		||||
    handleLink(item) {
 | 
			
		||||
      const { redirect, path } = item
 | 
			
		||||
      if (redirect) {
 | 
			
		||||
        this.$router.push(redirect)
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
      this.$router.push(this.pathCompile(path))
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.app-breadcrumb.el-breadcrumb {
 | 
			
		||||
  display: inline-block;
 | 
			
		||||
  font-size: 14px;
 | 
			
		||||
  line-height: 50px;
 | 
			
		||||
  margin-left: 8px;
 | 
			
		||||
 | 
			
		||||
  .no-redirect {
 | 
			
		||||
    color: #97a8be;
 | 
			
		||||
    cursor: text;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										155
									
								
								src/components/Charts/Keyboard.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,155 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div :id="id" :class="className" :style="{height:height,width:width}" />
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import echarts from 'echarts'
 | 
			
		||||
import resize from './mixins/resize'
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  mixins: [resize],
 | 
			
		||||
  props: {
 | 
			
		||||
    className: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      default: 'chart'
 | 
			
		||||
    },
 | 
			
		||||
    id: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      default: 'chart'
 | 
			
		||||
    },
 | 
			
		||||
    width: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      default: '200px'
 | 
			
		||||
    },
 | 
			
		||||
    height: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      default: '200px'
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      chart: null
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  mounted() {
 | 
			
		||||
    this.initChart()
 | 
			
		||||
  },
 | 
			
		||||
  beforeDestroy() {
 | 
			
		||||
    if (!this.chart) {
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    this.chart.dispose()
 | 
			
		||||
    this.chart = null
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    initChart() {
 | 
			
		||||
      this.chart = echarts.init(document.getElementById(this.id))
 | 
			
		||||
 | 
			
		||||
      const xAxisData = []
 | 
			
		||||
      const data = []
 | 
			
		||||
      const data2 = []
 | 
			
		||||
      for (let i = 0; i < 50; i++) {
 | 
			
		||||
        xAxisData.push(i)
 | 
			
		||||
        data.push((Math.sin(i / 5) * (i / 5 - 10) + i / 6) * 5)
 | 
			
		||||
        data2.push((Math.sin(i / 5) * (i / 5 + 10) + i / 6) * 3)
 | 
			
		||||
      }
 | 
			
		||||
      this.chart.setOption({
 | 
			
		||||
        backgroundColor: '#08263a',
 | 
			
		||||
        grid: {
 | 
			
		||||
          left: '5%',
 | 
			
		||||
          right: '5%'
 | 
			
		||||
        },
 | 
			
		||||
        xAxis: [{
 | 
			
		||||
          show: false,
 | 
			
		||||
          data: xAxisData
 | 
			
		||||
        }, {
 | 
			
		||||
          show: false,
 | 
			
		||||
          data: xAxisData
 | 
			
		||||
        }],
 | 
			
		||||
        visualMap: {
 | 
			
		||||
          show: false,
 | 
			
		||||
          min: 0,
 | 
			
		||||
          max: 50,
 | 
			
		||||
          dimension: 0,
 | 
			
		||||
          inRange: {
 | 
			
		||||
            color: ['#4a657a', '#308e92', '#b1cfa5', '#f5d69f', '#f5898b', '#ef5055']
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        yAxis: {
 | 
			
		||||
          axisLine: {
 | 
			
		||||
            show: false
 | 
			
		||||
          },
 | 
			
		||||
          axisLabel: {
 | 
			
		||||
            textStyle: {
 | 
			
		||||
              color: '#4a657a'
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
          splitLine: {
 | 
			
		||||
            show: true,
 | 
			
		||||
            lineStyle: {
 | 
			
		||||
              color: '#08263f'
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
          axisTick: {
 | 
			
		||||
            show: false
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        series: [{
 | 
			
		||||
          name: 'back',
 | 
			
		||||
          type: 'bar',
 | 
			
		||||
          data: data2,
 | 
			
		||||
          z: 1,
 | 
			
		||||
          itemStyle: {
 | 
			
		||||
            normal: {
 | 
			
		||||
              opacity: 0.4,
 | 
			
		||||
              barBorderRadius: 5,
 | 
			
		||||
              shadowBlur: 3,
 | 
			
		||||
              shadowColor: '#111'
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }, {
 | 
			
		||||
          name: 'Simulate Shadow',
 | 
			
		||||
          type: 'line',
 | 
			
		||||
          data,
 | 
			
		||||
          z: 2,
 | 
			
		||||
          showSymbol: false,
 | 
			
		||||
          animationDelay: 0,
 | 
			
		||||
          animationEasing: 'linear',
 | 
			
		||||
          animationDuration: 1200,
 | 
			
		||||
          lineStyle: {
 | 
			
		||||
            normal: {
 | 
			
		||||
              color: 'transparent'
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
          areaStyle: {
 | 
			
		||||
            normal: {
 | 
			
		||||
              color: '#08263a',
 | 
			
		||||
              shadowBlur: 50,
 | 
			
		||||
              shadowColor: '#000'
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }, {
 | 
			
		||||
          name: 'front',
 | 
			
		||||
          type: 'bar',
 | 
			
		||||
          data,
 | 
			
		||||
          xAxisIndex: 1,
 | 
			
		||||
          z: 3,
 | 
			
		||||
          itemStyle: {
 | 
			
		||||
            normal: {
 | 
			
		||||
              barBorderRadius: 5
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }],
 | 
			
		||||
        animationEasing: 'elasticOut',
 | 
			
		||||
        animationEasingUpdate: 'elasticOut',
 | 
			
		||||
        animationDelay(idx) {
 | 
			
		||||
          return idx * 20
 | 
			
		||||
        },
 | 
			
		||||
        animationDelayUpdate(idx) {
 | 
			
		||||
          return idx * 20
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
							
								
								
									
										227
									
								
								src/components/Charts/LineMarker.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,227 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div :id="id" :class="className" :style="{height:height,width:width}" />
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import echarts from 'echarts'
 | 
			
		||||
import resize from './mixins/resize'
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  mixins: [resize],
 | 
			
		||||
  props: {
 | 
			
		||||
    className: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      default: 'chart'
 | 
			
		||||
    },
 | 
			
		||||
    id: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      default: 'chart'
 | 
			
		||||
    },
 | 
			
		||||
    width: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      default: '200px'
 | 
			
		||||
    },
 | 
			
		||||
    height: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      default: '200px'
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      chart: null
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  mounted() {
 | 
			
		||||
    this.initChart()
 | 
			
		||||
  },
 | 
			
		||||
  beforeDestroy() {
 | 
			
		||||
    if (!this.chart) {
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    this.chart.dispose()
 | 
			
		||||
    this.chart = null
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    initChart() {
 | 
			
		||||
      this.chart = echarts.init(document.getElementById(this.id))
 | 
			
		||||
 | 
			
		||||
      this.chart.setOption({
 | 
			
		||||
        backgroundColor: '#394056',
 | 
			
		||||
        title: {
 | 
			
		||||
          top: 20,
 | 
			
		||||
          text: 'Requests',
 | 
			
		||||
          textStyle: {
 | 
			
		||||
            fontWeight: 'normal',
 | 
			
		||||
            fontSize: 16,
 | 
			
		||||
            color: '#F1F1F3'
 | 
			
		||||
          },
 | 
			
		||||
          left: '1%'
 | 
			
		||||
        },
 | 
			
		||||
        tooltip: {
 | 
			
		||||
          trigger: 'axis',
 | 
			
		||||
          axisPointer: {
 | 
			
		||||
            lineStyle: {
 | 
			
		||||
              color: '#57617B'
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        legend: {
 | 
			
		||||
          top: 20,
 | 
			
		||||
          icon: 'rect',
 | 
			
		||||
          itemWidth: 14,
 | 
			
		||||
          itemHeight: 5,
 | 
			
		||||
          itemGap: 13,
 | 
			
		||||
          data: ['CMCC', 'CTCC', 'CUCC'],
 | 
			
		||||
          right: '4%',
 | 
			
		||||
          textStyle: {
 | 
			
		||||
            fontSize: 12,
 | 
			
		||||
            color: '#F1F1F3'
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        grid: {
 | 
			
		||||
          top: 100,
 | 
			
		||||
          left: '2%',
 | 
			
		||||
          right: '2%',
 | 
			
		||||
          bottom: '2%',
 | 
			
		||||
          containLabel: true
 | 
			
		||||
        },
 | 
			
		||||
        xAxis: [{
 | 
			
		||||
          type: 'category',
 | 
			
		||||
          boundaryGap: false,
 | 
			
		||||
          axisLine: {
 | 
			
		||||
            lineStyle: {
 | 
			
		||||
              color: '#57617B'
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
          data: ['13:00', '13:05', '13:10', '13:15', '13:20', '13:25', '13:30', '13:35', '13:40', '13:45', '13:50', '13:55']
 | 
			
		||||
        }],
 | 
			
		||||
        yAxis: [{
 | 
			
		||||
          type: 'value',
 | 
			
		||||
          name: '(%)',
 | 
			
		||||
          axisTick: {
 | 
			
		||||
            show: false
 | 
			
		||||
          },
 | 
			
		||||
          axisLine: {
 | 
			
		||||
            lineStyle: {
 | 
			
		||||
              color: '#57617B'
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
          axisLabel: {
 | 
			
		||||
            margin: 10,
 | 
			
		||||
            textStyle: {
 | 
			
		||||
              fontSize: 14
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
          splitLine: {
 | 
			
		||||
            lineStyle: {
 | 
			
		||||
              color: '#57617B'
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }],
 | 
			
		||||
        series: [{
 | 
			
		||||
          name: 'CMCC',
 | 
			
		||||
          type: 'line',
 | 
			
		||||
          smooth: true,
 | 
			
		||||
          symbol: 'circle',
 | 
			
		||||
          symbolSize: 5,
 | 
			
		||||
          showSymbol: false,
 | 
			
		||||
          lineStyle: {
 | 
			
		||||
            normal: {
 | 
			
		||||
              width: 1
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
          areaStyle: {
 | 
			
		||||
            normal: {
 | 
			
		||||
              color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
 | 
			
		||||
                offset: 0,
 | 
			
		||||
                color: 'rgba(137, 189, 27, 0.3)'
 | 
			
		||||
              }, {
 | 
			
		||||
                offset: 0.8,
 | 
			
		||||
                color: 'rgba(137, 189, 27, 0)'
 | 
			
		||||
              }], false),
 | 
			
		||||
              shadowColor: 'rgba(0, 0, 0, 0.1)',
 | 
			
		||||
              shadowBlur: 10
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
          itemStyle: {
 | 
			
		||||
            normal: {
 | 
			
		||||
              color: 'rgb(137,189,27)',
 | 
			
		||||
              borderColor: 'rgba(137,189,2,0.27)',
 | 
			
		||||
              borderWidth: 12
 | 
			
		||||
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
          data: [220, 182, 191, 134, 150, 120, 110, 125, 145, 122, 165, 122]
 | 
			
		||||
        }, {
 | 
			
		||||
          name: 'CTCC',
 | 
			
		||||
          type: 'line',
 | 
			
		||||
          smooth: true,
 | 
			
		||||
          symbol: 'circle',
 | 
			
		||||
          symbolSize: 5,
 | 
			
		||||
          showSymbol: false,
 | 
			
		||||
          lineStyle: {
 | 
			
		||||
            normal: {
 | 
			
		||||
              width: 1
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
          areaStyle: {
 | 
			
		||||
            normal: {
 | 
			
		||||
              color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
 | 
			
		||||
                offset: 0,
 | 
			
		||||
                color: 'rgba(0, 136, 212, 0.3)'
 | 
			
		||||
              }, {
 | 
			
		||||
                offset: 0.8,
 | 
			
		||||
                color: 'rgba(0, 136, 212, 0)'
 | 
			
		||||
              }], false),
 | 
			
		||||
              shadowColor: 'rgba(0, 0, 0, 0.1)',
 | 
			
		||||
              shadowBlur: 10
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
          itemStyle: {
 | 
			
		||||
            normal: {
 | 
			
		||||
              color: 'rgb(0,136,212)',
 | 
			
		||||
              borderColor: 'rgba(0,136,212,0.2)',
 | 
			
		||||
              borderWidth: 12
 | 
			
		||||
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
          data: [120, 110, 125, 145, 122, 165, 122, 220, 182, 191, 134, 150]
 | 
			
		||||
        }, {
 | 
			
		||||
          name: 'CUCC',
 | 
			
		||||
          type: 'line',
 | 
			
		||||
          smooth: true,
 | 
			
		||||
          symbol: 'circle',
 | 
			
		||||
          symbolSize: 5,
 | 
			
		||||
          showSymbol: false,
 | 
			
		||||
          lineStyle: {
 | 
			
		||||
            normal: {
 | 
			
		||||
              width: 1
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
          areaStyle: {
 | 
			
		||||
            normal: {
 | 
			
		||||
              color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
 | 
			
		||||
                offset: 0,
 | 
			
		||||
                color: 'rgba(219, 50, 51, 0.3)'
 | 
			
		||||
              }, {
 | 
			
		||||
                offset: 0.8,
 | 
			
		||||
                color: 'rgba(219, 50, 51, 0)'
 | 
			
		||||
              }], false),
 | 
			
		||||
              shadowColor: 'rgba(0, 0, 0, 0.1)',
 | 
			
		||||
              shadowBlur: 10
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
          itemStyle: {
 | 
			
		||||
            normal: {
 | 
			
		||||
              color: 'rgb(219,50,51)',
 | 
			
		||||
              borderColor: 'rgba(219,50,51,0.2)',
 | 
			
		||||
              borderWidth: 12
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
          data: [220, 182, 125, 145, 122, 191, 134, 150, 120, 110, 165, 122]
 | 
			
		||||
        }]
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
							
								
								
									
										271
									
								
								src/components/Charts/MixChart.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,271 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div :id="id" :class="className" :style="{height:height,width:width}" />
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import echarts from 'echarts'
 | 
			
		||||
import resize from './mixins/resize'
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  mixins: [resize],
 | 
			
		||||
  props: {
 | 
			
		||||
    className: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      default: 'chart'
 | 
			
		||||
    },
 | 
			
		||||
    id: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      default: 'chart'
 | 
			
		||||
    },
 | 
			
		||||
    width: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      default: '200px'
 | 
			
		||||
    },
 | 
			
		||||
    height: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      default: '200px'
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      chart: null
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  mounted() {
 | 
			
		||||
    this.initChart()
 | 
			
		||||
  },
 | 
			
		||||
  beforeDestroy() {
 | 
			
		||||
    if (!this.chart) {
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    this.chart.dispose()
 | 
			
		||||
    this.chart = null
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    initChart() {
 | 
			
		||||
      this.chart = echarts.init(document.getElementById(this.id))
 | 
			
		||||
      const xData = (function() {
 | 
			
		||||
        const data = []
 | 
			
		||||
        for (let i = 1; i < 13; i++) {
 | 
			
		||||
          data.push(i + 'month')
 | 
			
		||||
        }
 | 
			
		||||
        return data
 | 
			
		||||
      }())
 | 
			
		||||
      this.chart.setOption({
 | 
			
		||||
        backgroundColor: '#344b58',
 | 
			
		||||
        title: {
 | 
			
		||||
          text: 'statistics',
 | 
			
		||||
          x: '20',
 | 
			
		||||
          top: '20',
 | 
			
		||||
          textStyle: {
 | 
			
		||||
            color: '#fff',
 | 
			
		||||
            fontSize: '22'
 | 
			
		||||
          },
 | 
			
		||||
          subtextStyle: {
 | 
			
		||||
            color: '#90979c',
 | 
			
		||||
            fontSize: '16'
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        tooltip: {
 | 
			
		||||
          trigger: 'axis',
 | 
			
		||||
          axisPointer: {
 | 
			
		||||
            textStyle: {
 | 
			
		||||
              color: '#fff'
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        grid: {
 | 
			
		||||
          left: '5%',
 | 
			
		||||
          right: '5%',
 | 
			
		||||
          borderWidth: 0,
 | 
			
		||||
          top: 150,
 | 
			
		||||
          bottom: 95,
 | 
			
		||||
          textStyle: {
 | 
			
		||||
            color: '#fff'
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        legend: {
 | 
			
		||||
          x: '5%',
 | 
			
		||||
          top: '10%',
 | 
			
		||||
          textStyle: {
 | 
			
		||||
            color: '#90979c'
 | 
			
		||||
          },
 | 
			
		||||
          data: ['female', 'male', 'average']
 | 
			
		||||
        },
 | 
			
		||||
        calculable: true,
 | 
			
		||||
        xAxis: [{
 | 
			
		||||
          type: 'category',
 | 
			
		||||
          axisLine: {
 | 
			
		||||
            lineStyle: {
 | 
			
		||||
              color: '#90979c'
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
          splitLine: {
 | 
			
		||||
            show: false
 | 
			
		||||
          },
 | 
			
		||||
          axisTick: {
 | 
			
		||||
            show: false
 | 
			
		||||
          },
 | 
			
		||||
          splitArea: {
 | 
			
		||||
            show: false
 | 
			
		||||
          },
 | 
			
		||||
          axisLabel: {
 | 
			
		||||
            interval: 0
 | 
			
		||||
 | 
			
		||||
          },
 | 
			
		||||
          data: xData
 | 
			
		||||
        }],
 | 
			
		||||
        yAxis: [{
 | 
			
		||||
          type: 'value',
 | 
			
		||||
          splitLine: {
 | 
			
		||||
            show: false
 | 
			
		||||
          },
 | 
			
		||||
          axisLine: {
 | 
			
		||||
            lineStyle: {
 | 
			
		||||
              color: '#90979c'
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
          axisTick: {
 | 
			
		||||
            show: false
 | 
			
		||||
          },
 | 
			
		||||
          axisLabel: {
 | 
			
		||||
            interval: 0
 | 
			
		||||
          },
 | 
			
		||||
          splitArea: {
 | 
			
		||||
            show: false
 | 
			
		||||
          }
 | 
			
		||||
        }],
 | 
			
		||||
        dataZoom: [{
 | 
			
		||||
          show: true,
 | 
			
		||||
          height: 30,
 | 
			
		||||
          xAxisIndex: [
 | 
			
		||||
            0
 | 
			
		||||
          ],
 | 
			
		||||
          bottom: 30,
 | 
			
		||||
          start: 10,
 | 
			
		||||
          end: 80,
 | 
			
		||||
          handleIcon: 'path://M306.1,413c0,2.2-1.8,4-4,4h-59.8c-2.2,0-4-1.8-4-4V200.8c0-2.2,1.8-4,4-4h59.8c2.2,0,4,1.8,4,4V413z',
 | 
			
		||||
          handleSize: '110%',
 | 
			
		||||
          handleStyle: {
 | 
			
		||||
            color: '#d3dee5'
 | 
			
		||||
 | 
			
		||||
          },
 | 
			
		||||
          textStyle: {
 | 
			
		||||
            color: '#fff' },
 | 
			
		||||
          borderColor: '#90979c'
 | 
			
		||||
 | 
			
		||||
        }, {
 | 
			
		||||
          type: 'inside',
 | 
			
		||||
          show: true,
 | 
			
		||||
          height: 15,
 | 
			
		||||
          start: 1,
 | 
			
		||||
          end: 35
 | 
			
		||||
        }],
 | 
			
		||||
        series: [{
 | 
			
		||||
          name: 'female',
 | 
			
		||||
          type: 'bar',
 | 
			
		||||
          stack: 'total',
 | 
			
		||||
          barMaxWidth: 35,
 | 
			
		||||
          barGap: '10%',
 | 
			
		||||
          itemStyle: {
 | 
			
		||||
            normal: {
 | 
			
		||||
              color: 'rgba(255,144,128,1)',
 | 
			
		||||
              label: {
 | 
			
		||||
                show: true,
 | 
			
		||||
                textStyle: {
 | 
			
		||||
                  color: '#fff'
 | 
			
		||||
                },
 | 
			
		||||
                position: 'insideTop',
 | 
			
		||||
                formatter(p) {
 | 
			
		||||
                  return p.value > 0 ? p.value : ''
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
          data: [
 | 
			
		||||
            709,
 | 
			
		||||
            1917,
 | 
			
		||||
            2455,
 | 
			
		||||
            2610,
 | 
			
		||||
            1719,
 | 
			
		||||
            1433,
 | 
			
		||||
            1544,
 | 
			
		||||
            3285,
 | 
			
		||||
            5208,
 | 
			
		||||
            3372,
 | 
			
		||||
            2484,
 | 
			
		||||
            4078
 | 
			
		||||
          ]
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        {
 | 
			
		||||
          name: 'male',
 | 
			
		||||
          type: 'bar',
 | 
			
		||||
          stack: 'total',
 | 
			
		||||
          itemStyle: {
 | 
			
		||||
            normal: {
 | 
			
		||||
              color: 'rgba(0,191,183,1)',
 | 
			
		||||
              barBorderRadius: 0,
 | 
			
		||||
              label: {
 | 
			
		||||
                show: true,
 | 
			
		||||
                position: 'top',
 | 
			
		||||
                formatter(p) {
 | 
			
		||||
                  return p.value > 0 ? p.value : ''
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
          data: [
 | 
			
		||||
            327,
 | 
			
		||||
            1776,
 | 
			
		||||
            507,
 | 
			
		||||
            1200,
 | 
			
		||||
            800,
 | 
			
		||||
            482,
 | 
			
		||||
            204,
 | 
			
		||||
            1390,
 | 
			
		||||
            1001,
 | 
			
		||||
            951,
 | 
			
		||||
            381,
 | 
			
		||||
            220
 | 
			
		||||
          ]
 | 
			
		||||
        }, {
 | 
			
		||||
          name: 'average',
 | 
			
		||||
          type: 'line',
 | 
			
		||||
          stack: 'total',
 | 
			
		||||
          symbolSize: 10,
 | 
			
		||||
          symbol: 'circle',
 | 
			
		||||
          itemStyle: {
 | 
			
		||||
            normal: {
 | 
			
		||||
              color: 'rgba(252,230,48,1)',
 | 
			
		||||
              barBorderRadius: 0,
 | 
			
		||||
              label: {
 | 
			
		||||
                show: true,
 | 
			
		||||
                position: 'top',
 | 
			
		||||
                formatter(p) {
 | 
			
		||||
                  return p.value > 0 ? p.value : ''
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
          data: [
 | 
			
		||||
            1036,
 | 
			
		||||
            3693,
 | 
			
		||||
            2962,
 | 
			
		||||
            3810,
 | 
			
		||||
            2519,
 | 
			
		||||
            1915,
 | 
			
		||||
            1748,
 | 
			
		||||
            4675,
 | 
			
		||||
            6209,
 | 
			
		||||
            4323,
 | 
			
		||||
            2865,
 | 
			
		||||
            4298
 | 
			
		||||
          ]
 | 
			
		||||
        }
 | 
			
		||||
        ]
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
							
								
								
									
										34
									
								
								src/components/Charts/mixins/resize.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,34 @@
 | 
			
		||||
import { debounce } from '@/utils'
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      $_sidebarElm: null
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  mounted() {
 | 
			
		||||
    this.__resizeHandler = debounce(() => {
 | 
			
		||||
      if (this.chart) {
 | 
			
		||||
        this.chart.resize()
 | 
			
		||||
      }
 | 
			
		||||
    }, 100)
 | 
			
		||||
    window.addEventListener('resize', this.__resizeHandler)
 | 
			
		||||
 | 
			
		||||
    this.$_sidebarElm = document.getElementsByClassName('sidebar-container')[0]
 | 
			
		||||
    this.$_sidebarElm && this.$_sidebarElm.addEventListener('transitionend', this.$_sidebarResizeHandler)
 | 
			
		||||
  },
 | 
			
		||||
  beforeDestroy() {
 | 
			
		||||
    window.removeEventListener('resize', this.__resizeHandler)
 | 
			
		||||
 | 
			
		||||
    this.$_sidebarElm && this.$_sidebarElm.removeEventListener('transitionend', this.$_sidebarResizeHandler)
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    // use $_ for mixins properties
 | 
			
		||||
    // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential
 | 
			
		||||
    $_sidebarResizeHandler(e) {
 | 
			
		||||
      if (e.propertyName === 'width') {
 | 
			
		||||
        this.__resizeHandler()
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										166
									
								
								src/components/DndList/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,166 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="dndList">
 | 
			
		||||
    <div :style="{width:width1}" class="dndList-list">
 | 
			
		||||
      <h3>{{ list1Title }}</h3>
 | 
			
		||||
      <draggable :set-data="setData" :list="list1" group="article" class="dragArea">
 | 
			
		||||
        <div v-for="element in list1" :key="element.id" class="list-complete-item">
 | 
			
		||||
          <div class="list-complete-item-handle">
 | 
			
		||||
            {{ element.id }}[{{ element.author }}] {{ element.title }}
 | 
			
		||||
          </div>
 | 
			
		||||
          <div style="position:absolute;right:0px;">
 | 
			
		||||
            <span style="float: right ;margin-top: -20px;margin-right:5px;" @click="deleteEle(element)">
 | 
			
		||||
              <i style="color:#ff4949" class="el-icon-delete" />
 | 
			
		||||
            </span>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </draggable>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div :style="{width:width2}" class="dndList-list">
 | 
			
		||||
      <h3>{{ list2Title }}</h3>
 | 
			
		||||
      <draggable :list="list2" group="article" class="dragArea">
 | 
			
		||||
        <div v-for="element in list2" :key="element.id" class="list-complete-item">
 | 
			
		||||
          <div class="list-complete-item-handle2" @click="pushEle(element)">
 | 
			
		||||
            {{ element.id }} [{{ element.author }}] {{ element.title }}
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </draggable>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import draggable from 'vuedraggable'
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'DndList',
 | 
			
		||||
  components: { draggable },
 | 
			
		||||
  props: {
 | 
			
		||||
    list1: {
 | 
			
		||||
      type: Array,
 | 
			
		||||
      default() {
 | 
			
		||||
        return []
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    list2: {
 | 
			
		||||
      type: Array,
 | 
			
		||||
      default() {
 | 
			
		||||
        return []
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    list1Title: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      default: 'list1'
 | 
			
		||||
    },
 | 
			
		||||
    list2Title: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      default: 'list2'
 | 
			
		||||
    },
 | 
			
		||||
    width1: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      default: '48%'
 | 
			
		||||
    },
 | 
			
		||||
    width2: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      default: '48%'
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    isNotInList1(v) {
 | 
			
		||||
      return this.list1.every(k => v.id !== k.id)
 | 
			
		||||
    },
 | 
			
		||||
    isNotInList2(v) {
 | 
			
		||||
      return this.list2.every(k => v.id !== k.id)
 | 
			
		||||
    },
 | 
			
		||||
    deleteEle(ele) {
 | 
			
		||||
      for (const item of this.list1) {
 | 
			
		||||
        if (item.id === ele.id) {
 | 
			
		||||
          const index = this.list1.indexOf(item)
 | 
			
		||||
          this.list1.splice(index, 1)
 | 
			
		||||
          break
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      if (this.isNotInList2(ele)) {
 | 
			
		||||
        this.list2.unshift(ele)
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    pushEle(ele) {
 | 
			
		||||
      for (const item of this.list2) {
 | 
			
		||||
        if (item.id === ele.id) {
 | 
			
		||||
          const index = this.list2.indexOf(item)
 | 
			
		||||
          this.list2.splice(index, 1)
 | 
			
		||||
          break
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      if (this.isNotInList1(ele)) {
 | 
			
		||||
        this.list1.push(ele)
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    setData(dataTransfer) {
 | 
			
		||||
      // to avoid Firefox bug
 | 
			
		||||
      // Detail see : https://github.com/RubaXa/Sortable/issues/1012
 | 
			
		||||
      dataTransfer.setData('Text', '')
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.dndList {
 | 
			
		||||
  background: #fff;
 | 
			
		||||
  padding-bottom: 40px;
 | 
			
		||||
  &:after {
 | 
			
		||||
    content: "";
 | 
			
		||||
    display: table;
 | 
			
		||||
    clear: both;
 | 
			
		||||
  }
 | 
			
		||||
  .dndList-list {
 | 
			
		||||
    float: left;
 | 
			
		||||
    padding-bottom: 30px;
 | 
			
		||||
    &:first-of-type {
 | 
			
		||||
      margin-right: 2%;
 | 
			
		||||
    }
 | 
			
		||||
    .dragArea {
 | 
			
		||||
      margin-top: 15px;
 | 
			
		||||
      min-height: 50px;
 | 
			
		||||
      padding-bottom: 30px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.list-complete-item {
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  position: relative;
 | 
			
		||||
  font-size: 14px;
 | 
			
		||||
  padding: 5px 12px;
 | 
			
		||||
  margin-top: 4px;
 | 
			
		||||
  border: 1px solid #bfcbd9;
 | 
			
		||||
  transition: all 1s;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.list-complete-item-handle {
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  text-overflow: ellipsis;
 | 
			
		||||
  white-space: nowrap;
 | 
			
		||||
  margin-right: 50px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.list-complete-item-handle2 {
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  text-overflow: ellipsis;
 | 
			
		||||
  white-space: nowrap;
 | 
			
		||||
  margin-right: 20px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.list-complete-item.sortable-chosen {
 | 
			
		||||
  background: #4AB7BD;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.list-complete-item.sortable-ghost {
 | 
			
		||||
  background: #30B08F;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.list-complete-enter,
 | 
			
		||||
.list-complete-leave-active {
 | 
			
		||||
  opacity: 0;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										61
									
								
								src/components/DragSelect/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,61 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <el-select ref="dragSelect" v-model="selectVal" v-bind="$attrs" class="drag-select" multiple v-on="$listeners">
 | 
			
		||||
    <slot />
 | 
			
		||||
  </el-select>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import Sortable from 'sortablejs'
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'DragSelect',
 | 
			
		||||
  props: {
 | 
			
		||||
    value: {
 | 
			
		||||
      type: Array,
 | 
			
		||||
      required: true
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    selectVal: {
 | 
			
		||||
      get() {
 | 
			
		||||
        return [...this.value]
 | 
			
		||||
      },
 | 
			
		||||
      set(val) {
 | 
			
		||||
        this.$emit('input', [...val])
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  mounted() {
 | 
			
		||||
    this.setSort()
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    setSort() {
 | 
			
		||||
      const el = this.$refs.dragSelect.$el.querySelectorAll('.el-select__tags > span')[0]
 | 
			
		||||
      this.sortable = Sortable.create(el, {
 | 
			
		||||
        ghostClass: 'sortable-ghost', // Class name for the drop placeholder,
 | 
			
		||||
        setData: function(dataTransfer) {
 | 
			
		||||
          dataTransfer.setData('Text', '')
 | 
			
		||||
          // to avoid Firefox bug
 | 
			
		||||
          // Detail see : https://github.com/RubaXa/Sortable/issues/1012
 | 
			
		||||
        },
 | 
			
		||||
        onEnd: evt => {
 | 
			
		||||
          const targetRow = this.value.splice(evt.oldIndex, 1)[0]
 | 
			
		||||
          this.value.splice(evt.newIndex, 0, targetRow)
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
.drag-select >>> .sortable-ghost {
 | 
			
		||||
  opacity: .8;
 | 
			
		||||
  color: #fff!important;
 | 
			
		||||
  background: #42b983!important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.drag-select >>> .el-tag {
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										297
									
								
								src/components/Dropzone/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,297 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div :id="id" :ref="id" :action="url" class="dropzone">
 | 
			
		||||
    <input type="file" name="file">
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import Dropzone from 'dropzone'
 | 
			
		||||
import 'dropzone/dist/dropzone.css'
 | 
			
		||||
// import { getToken } from 'api/qiniu';
 | 
			
		||||
 | 
			
		||||
Dropzone.autoDiscover = false
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  props: {
 | 
			
		||||
    id: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      required: true
 | 
			
		||||
    },
 | 
			
		||||
    url: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      required: true
 | 
			
		||||
    },
 | 
			
		||||
    clickable: {
 | 
			
		||||
      type: Boolean,
 | 
			
		||||
      default: true
 | 
			
		||||
    },
 | 
			
		||||
    defaultMsg: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      default: '上传图片'
 | 
			
		||||
    },
 | 
			
		||||
    acceptedFiles: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      default: ''
 | 
			
		||||
    },
 | 
			
		||||
    thumbnailHeight: {
 | 
			
		||||
      type: Number,
 | 
			
		||||
      default: 200
 | 
			
		||||
    },
 | 
			
		||||
    thumbnailWidth: {
 | 
			
		||||
      type: Number,
 | 
			
		||||
      default: 200
 | 
			
		||||
    },
 | 
			
		||||
    showRemoveLink: {
 | 
			
		||||
      type: Boolean,
 | 
			
		||||
      default: true
 | 
			
		||||
    },
 | 
			
		||||
    maxFilesize: {
 | 
			
		||||
      type: Number,
 | 
			
		||||
      default: 2
 | 
			
		||||
    },
 | 
			
		||||
    maxFiles: {
 | 
			
		||||
      type: Number,
 | 
			
		||||
      default: 3
 | 
			
		||||
    },
 | 
			
		||||
    autoProcessQueue: {
 | 
			
		||||
      type: Boolean,
 | 
			
		||||
      default: true
 | 
			
		||||
    },
 | 
			
		||||
    useCustomDropzoneOptions: {
 | 
			
		||||
      type: Boolean,
 | 
			
		||||
      default: false
 | 
			
		||||
    },
 | 
			
		||||
    defaultImg: {
 | 
			
		||||
      default: '',
 | 
			
		||||
      type: [String, Array]
 | 
			
		||||
    },
 | 
			
		||||
    couldPaste: {
 | 
			
		||||
      type: Boolean,
 | 
			
		||||
      default: false
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      dropzone: '',
 | 
			
		||||
      initOnce: true
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  watch: {
 | 
			
		||||
    defaultImg(val) {
 | 
			
		||||
      if (val.length === 0) {
 | 
			
		||||
        this.initOnce = false
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
      if (!this.initOnce) return
 | 
			
		||||
      this.initImages(val)
 | 
			
		||||
      this.initOnce = false
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  mounted() {
 | 
			
		||||
    const element = document.getElementById(this.id)
 | 
			
		||||
    const vm = this
 | 
			
		||||
    this.dropzone = new Dropzone(element, {
 | 
			
		||||
      clickable: this.clickable,
 | 
			
		||||
      thumbnailWidth: this.thumbnailWidth,
 | 
			
		||||
      thumbnailHeight: this.thumbnailHeight,
 | 
			
		||||
      maxFiles: this.maxFiles,
 | 
			
		||||
      maxFilesize: this.maxFilesize,
 | 
			
		||||
      dictRemoveFile: 'Remove',
 | 
			
		||||
      addRemoveLinks: this.showRemoveLink,
 | 
			
		||||
      acceptedFiles: this.acceptedFiles,
 | 
			
		||||
      autoProcessQueue: this.autoProcessQueue,
 | 
			
		||||
      dictDefaultMessage: '<i style="margin-top: 3em;display: inline-block" class="material-icons">' + this.defaultMsg + '</i><br>Drop files here to upload',
 | 
			
		||||
      dictMaxFilesExceeded: '只能一个图',
 | 
			
		||||
      previewTemplate: '<div class="dz-preview dz-file-preview">  <div class="dz-image" style="width:' + this.thumbnailWidth + 'px;height:' + this.thumbnailHeight + 'px" ><img style="width:' + this.thumbnailWidth + 'px;height:' + this.thumbnailHeight + 'px" data-dz-thumbnail /></div>  <div class="dz-details"><div class="dz-size"><span data-dz-size></span></div> <div class="dz-progress"><span class="dz-upload" data-dz-uploadprogress></span></div>  <div class="dz-error-message"><span data-dz-errormessage></span></div>  <div class="dz-success-mark"> <i class="material-icons">done</i> </div>  <div class="dz-error-mark"><i class="material-icons">error</i></div></div>',
 | 
			
		||||
      init() {
 | 
			
		||||
        const val = vm.defaultImg
 | 
			
		||||
        if (!val) return
 | 
			
		||||
        if (Array.isArray(val)) {
 | 
			
		||||
          if (val.length === 0) return
 | 
			
		||||
          val.map((v, i) => {
 | 
			
		||||
            const mockFile = { name: 'name' + i, size: 12345, url: v }
 | 
			
		||||
            this.options.addedfile.call(this, mockFile)
 | 
			
		||||
            this.options.thumbnail.call(this, mockFile, v)
 | 
			
		||||
            mockFile.previewElement.classList.add('dz-success')
 | 
			
		||||
            mockFile.previewElement.classList.add('dz-complete')
 | 
			
		||||
            vm.initOnce = false
 | 
			
		||||
            return true
 | 
			
		||||
          })
 | 
			
		||||
        } else {
 | 
			
		||||
          const mockFile = { name: 'name', size: 12345, url: val }
 | 
			
		||||
          this.options.addedfile.call(this, mockFile)
 | 
			
		||||
          this.options.thumbnail.call(this, mockFile, val)
 | 
			
		||||
          mockFile.previewElement.classList.add('dz-success')
 | 
			
		||||
          mockFile.previewElement.classList.add('dz-complete')
 | 
			
		||||
          vm.initOnce = false
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      accept: (file, done) => {
 | 
			
		||||
        /* 七牛*/
 | 
			
		||||
        // const token = this.$store.getters.token;
 | 
			
		||||
        // getToken(token).then(response => {
 | 
			
		||||
        //   file.token = response.data.qiniu_token;
 | 
			
		||||
        //   file.key = response.data.qiniu_key;
 | 
			
		||||
        //   file.url = response.data.qiniu_url;
 | 
			
		||||
        //   done();
 | 
			
		||||
        // })
 | 
			
		||||
        done()
 | 
			
		||||
      },
 | 
			
		||||
      sending: (file, xhr, formData) => {
 | 
			
		||||
        // formData.append('token', file.token);
 | 
			
		||||
        // formData.append('key', file.key);
 | 
			
		||||
        vm.initOnce = false
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    if (this.couldPaste) {
 | 
			
		||||
      document.addEventListener('paste', this.pasteImg)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.dropzone.on('success', file => {
 | 
			
		||||
      vm.$emit('dropzone-success', file, vm.dropzone.element)
 | 
			
		||||
    })
 | 
			
		||||
    this.dropzone.on('addedfile', file => {
 | 
			
		||||
      vm.$emit('dropzone-fileAdded', file)
 | 
			
		||||
    })
 | 
			
		||||
    this.dropzone.on('removedfile', file => {
 | 
			
		||||
      vm.$emit('dropzone-removedFile', file)
 | 
			
		||||
    })
 | 
			
		||||
    this.dropzone.on('error', (file, error, xhr) => {
 | 
			
		||||
      vm.$emit('dropzone-error', file, error, xhr)
 | 
			
		||||
    })
 | 
			
		||||
    this.dropzone.on('successmultiple', (file, error, xhr) => {
 | 
			
		||||
      vm.$emit('dropzone-successmultiple', file, error, xhr)
 | 
			
		||||
    })
 | 
			
		||||
  },
 | 
			
		||||
  destroyed() {
 | 
			
		||||
    document.removeEventListener('paste', this.pasteImg)
 | 
			
		||||
    this.dropzone.destroy()
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    removeAllFiles() {
 | 
			
		||||
      this.dropzone.removeAllFiles(true)
 | 
			
		||||
    },
 | 
			
		||||
    processQueue() {
 | 
			
		||||
      this.dropzone.processQueue()
 | 
			
		||||
    },
 | 
			
		||||
    pasteImg(event) {
 | 
			
		||||
      const items = (event.clipboardData || event.originalEvent.clipboardData).items
 | 
			
		||||
      if (items[0].kind === 'file') {
 | 
			
		||||
        this.dropzone.addFile(items[0].getAsFile())
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    initImages(val) {
 | 
			
		||||
      if (!val) return
 | 
			
		||||
      if (Array.isArray(val)) {
 | 
			
		||||
        val.map((v, i) => {
 | 
			
		||||
          const mockFile = { name: 'name' + i, size: 12345, url: v }
 | 
			
		||||
          this.dropzone.options.addedfile.call(this.dropzone, mockFile)
 | 
			
		||||
          this.dropzone.options.thumbnail.call(this.dropzone, mockFile, v)
 | 
			
		||||
          mockFile.previewElement.classList.add('dz-success')
 | 
			
		||||
          mockFile.previewElement.classList.add('dz-complete')
 | 
			
		||||
          return true
 | 
			
		||||
        })
 | 
			
		||||
      } else {
 | 
			
		||||
        const mockFile = { name: 'name', size: 12345, url: val }
 | 
			
		||||
        this.dropzone.options.addedfile.call(this.dropzone, mockFile)
 | 
			
		||||
        this.dropzone.options.thumbnail.call(this.dropzone, mockFile, val)
 | 
			
		||||
        mockFile.previewElement.classList.add('dz-success')
 | 
			
		||||
        mockFile.previewElement.classList.add('dz-complete')
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
    .dropzone {
 | 
			
		||||
        border: 2px solid #E5E5E5;
 | 
			
		||||
        font-family: 'Roboto', sans-serif;
 | 
			
		||||
        color: #777;
 | 
			
		||||
        transition: background-color .2s linear;
 | 
			
		||||
        padding: 5px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .dropzone:hover {
 | 
			
		||||
        background-color: #F6F6F6;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    i {
 | 
			
		||||
        color: #CCC;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .dropzone .dz-image img {
 | 
			
		||||
        width: 100%;
 | 
			
		||||
        height: 100%;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .dropzone input[name='file'] {
 | 
			
		||||
        display: none;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .dropzone .dz-preview .dz-image {
 | 
			
		||||
        border-radius: 0px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .dropzone .dz-preview:hover .dz-image img {
 | 
			
		||||
        transform: none;
 | 
			
		||||
        filter: none;
 | 
			
		||||
        width: 100%;
 | 
			
		||||
        height: 100%;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .dropzone .dz-preview .dz-details {
 | 
			
		||||
        bottom: 0px;
 | 
			
		||||
        top: 0px;
 | 
			
		||||
        color: white;
 | 
			
		||||
        background-color: rgba(33, 150, 243, 0.8);
 | 
			
		||||
        transition: opacity .2s linear;
 | 
			
		||||
        text-align: left;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .dropzone .dz-preview .dz-details .dz-filename span, .dropzone .dz-preview .dz-details .dz-size span {
 | 
			
		||||
        background-color: transparent;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .dropzone .dz-preview .dz-details .dz-filename:not(:hover) span {
 | 
			
		||||
        border: none;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .dropzone .dz-preview .dz-details .dz-filename:hover span {
 | 
			
		||||
        background-color: transparent;
 | 
			
		||||
        border: none;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .dropzone .dz-preview .dz-remove {
 | 
			
		||||
        position: absolute;
 | 
			
		||||
        z-index: 30;
 | 
			
		||||
        color: white;
 | 
			
		||||
        margin-left: 15px;
 | 
			
		||||
        padding: 10px;
 | 
			
		||||
        top: inherit;
 | 
			
		||||
        bottom: 15px;
 | 
			
		||||
        border: 2px white solid;
 | 
			
		||||
        text-decoration: none;
 | 
			
		||||
        text-transform: uppercase;
 | 
			
		||||
        font-size: 0.8rem;
 | 
			
		||||
        font-weight: 800;
 | 
			
		||||
        letter-spacing: 1.1px;
 | 
			
		||||
        opacity: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .dropzone .dz-preview:hover .dz-remove {
 | 
			
		||||
        opacity: 1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .dropzone .dz-preview .dz-success-mark, .dropzone .dz-preview .dz-error-mark {
 | 
			
		||||
        margin-left: -40px;
 | 
			
		||||
        margin-top: -50px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .dropzone .dz-preview .dz-success-mark i, .dropzone .dz-preview .dz-error-mark i {
 | 
			
		||||
        color: white;
 | 
			
		||||
        font-size: 5rem;
 | 
			
		||||
    }
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										78
									
								
								src/components/ErrorLog/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,78 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div v-if="errorLogs.length>0">
 | 
			
		||||
    <el-badge :is-dot="true" style="line-height: 25px;margin-top: -5px;" @click.native="dialogTableVisible=true">
 | 
			
		||||
      <el-button style="padding: 8px 10px;" size="small" type="danger">
 | 
			
		||||
        <svg-icon icon-class="bug" />
 | 
			
		||||
      </el-button>
 | 
			
		||||
    </el-badge>
 | 
			
		||||
 | 
			
		||||
    <el-dialog :visible.sync="dialogTableVisible" width="80%" append-to-body>
 | 
			
		||||
      <div slot="title">
 | 
			
		||||
        <span style="padding-right: 10px;">Error Log</span>
 | 
			
		||||
        <el-button size="mini" type="primary" icon="el-icon-delete" @click="clearAll">Clear All</el-button>
 | 
			
		||||
      </div>
 | 
			
		||||
      <el-table :data="errorLogs" border>
 | 
			
		||||
        <el-table-column label="Message">
 | 
			
		||||
          <template slot-scope="{row}">
 | 
			
		||||
            <div>
 | 
			
		||||
              <span class="message-title">Msg:</span>
 | 
			
		||||
              <el-tag type="danger">
 | 
			
		||||
                {{ row.err.message }}
 | 
			
		||||
              </el-tag>
 | 
			
		||||
            </div>
 | 
			
		||||
            <br>
 | 
			
		||||
            <div>
 | 
			
		||||
              <span class="message-title" style="padding-right: 10px;">Info: </span>
 | 
			
		||||
              <el-tag type="warning">
 | 
			
		||||
                {{ row.vm.$vnode.tag }} error in {{ row.info }}
 | 
			
		||||
              </el-tag>
 | 
			
		||||
            </div>
 | 
			
		||||
            <br>
 | 
			
		||||
            <div>
 | 
			
		||||
              <span class="message-title" style="padding-right: 16px;">Url: </span>
 | 
			
		||||
              <el-tag type="success">
 | 
			
		||||
                {{ row.url }}
 | 
			
		||||
              </el-tag>
 | 
			
		||||
            </div>
 | 
			
		||||
          </template>
 | 
			
		||||
        </el-table-column>
 | 
			
		||||
        <el-table-column label="Stack">
 | 
			
		||||
          <template slot-scope="scope">
 | 
			
		||||
            {{ scope.row.err.stack }}
 | 
			
		||||
          </template>
 | 
			
		||||
        </el-table-column>
 | 
			
		||||
      </el-table>
 | 
			
		||||
    </el-dialog>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'ErrorLog',
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      dialogTableVisible: false
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    errorLogs() {
 | 
			
		||||
      return this.$store.getters.errorLogs
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    clearAll() {
 | 
			
		||||
      this.dialogTableVisible = false
 | 
			
		||||
      this.$store.dispatch('errorLog/clearErrorLog')
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
.message-title {
 | 
			
		||||
  font-size: 16px;
 | 
			
		||||
  color: #333;
 | 
			
		||||
  font-weight: bold;
 | 
			
		||||
  padding-right: 8px;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										54
									
								
								src/components/GithubCorner/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,54 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <a href="https://github.com/PanJiaChen/vue-element-admin" target="_blank" class="github-corner" aria-label="View source on Github">
 | 
			
		||||
    <svg
 | 
			
		||||
      width="80"
 | 
			
		||||
      height="80"
 | 
			
		||||
      viewBox="0 0 250 250"
 | 
			
		||||
      style="fill:#40c9c6; color:#fff;"
 | 
			
		||||
      aria-hidden="true"
 | 
			
		||||
    >
 | 
			
		||||
      <path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z" />
 | 
			
		||||
      <path
 | 
			
		||||
        d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2"
 | 
			
		||||
        fill="currentColor"
 | 
			
		||||
        style="transform-origin: 130px 106px;"
 | 
			
		||||
        class="octo-arm"
 | 
			
		||||
      />
 | 
			
		||||
      <path
 | 
			
		||||
        d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z"
 | 
			
		||||
        fill="currentColor"
 | 
			
		||||
        class="octo-body"
 | 
			
		||||
      />
 | 
			
		||||
    </svg>
 | 
			
		||||
  </a>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
.github-corner:hover .octo-arm {
 | 
			
		||||
  animation: octocat-wave 560ms ease-in-out
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@keyframes octocat-wave {
 | 
			
		||||
  0%,
 | 
			
		||||
  100% {
 | 
			
		||||
    transform: rotate(0)
 | 
			
		||||
  }
 | 
			
		||||
  20%,
 | 
			
		||||
  60% {
 | 
			
		||||
    transform: rotate(-25deg)
 | 
			
		||||
  }
 | 
			
		||||
  40%,
 | 
			
		||||
  80% {
 | 
			
		||||
    transform: rotate(10deg)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media (max-width:500px) {
 | 
			
		||||
  .github-corner:hover .octo-arm {
 | 
			
		||||
    animation: none
 | 
			
		||||
  }
 | 
			
		||||
  .github-corner .octo-arm {
 | 
			
		||||
    animation: octocat-wave 560ms ease-in-out
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										44
									
								
								src/components/Hamburger/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,44 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div style="padding: 0 15px;" @click="toggleClick">
 | 
			
		||||
    <svg
 | 
			
		||||
      :class="{'is-active':isActive}"
 | 
			
		||||
      class="hamburger"
 | 
			
		||||
      viewBox="0 0 1024 1024"
 | 
			
		||||
      xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
      width="64"
 | 
			
		||||
      height="64"
 | 
			
		||||
    >
 | 
			
		||||
      <path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z" />
 | 
			
		||||
    </svg>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'Hamburger',
 | 
			
		||||
  props: {
 | 
			
		||||
    isActive: {
 | 
			
		||||
      type: Boolean,
 | 
			
		||||
      default: false
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    toggleClick() {
 | 
			
		||||
      this.$emit('toggleClick')
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
.hamburger {
 | 
			
		||||
  display: inline-block;
 | 
			
		||||
  vertical-align: middle;
 | 
			
		||||
  width: 20px;
 | 
			
		||||
  height: 20px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.hamburger.is-active {
 | 
			
		||||
  transform: rotate(180deg);
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										180
									
								
								src/components/HeaderSearch/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,180 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div :class="{'show':show}" class="header-search">
 | 
			
		||||
    <svg-icon class-name="search-icon" icon-class="search" @click.stop="click" />
 | 
			
		||||
    <el-select
 | 
			
		||||
      ref="headerSearchSelect"
 | 
			
		||||
      v-model="search"
 | 
			
		||||
      :remote-method="querySearch"
 | 
			
		||||
      filterable
 | 
			
		||||
      default-first-option
 | 
			
		||||
      remote
 | 
			
		||||
      placeholder="Search"
 | 
			
		||||
      class="header-search-select"
 | 
			
		||||
      @change="change"
 | 
			
		||||
    >
 | 
			
		||||
      <el-option v-for="item in options" :key="item.path" :value="item" :label="item.title.join(' > ')" />
 | 
			
		||||
    </el-select>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
// fuse is a lightweight fuzzy-search module
 | 
			
		||||
// make search results more in line with expectations
 | 
			
		||||
import Fuse from 'fuse.js'
 | 
			
		||||
import path from 'path'
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'HeaderSearch',
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      search: '',
 | 
			
		||||
      options: [],
 | 
			
		||||
      searchPool: [],
 | 
			
		||||
      show: false,
 | 
			
		||||
      fuse: undefined
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    routes() {
 | 
			
		||||
      return this.$store.getters.permission_routes
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  watch: {
 | 
			
		||||
    routes() {
 | 
			
		||||
      this.searchPool = this.generateRoutes(this.routes)
 | 
			
		||||
    },
 | 
			
		||||
    searchPool(list) {
 | 
			
		||||
      this.initFuse(list)
 | 
			
		||||
    },
 | 
			
		||||
    show(value) {
 | 
			
		||||
      if (value) {
 | 
			
		||||
        document.body.addEventListener('click', this.close)
 | 
			
		||||
      } else {
 | 
			
		||||
        document.body.removeEventListener('click', this.close)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  mounted() {
 | 
			
		||||
    this.searchPool = this.generateRoutes(this.routes)
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    click() {
 | 
			
		||||
      this.show = !this.show
 | 
			
		||||
      if (this.show) {
 | 
			
		||||
        this.$refs.headerSearchSelect && this.$refs.headerSearchSelect.focus()
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    close() {
 | 
			
		||||
      this.$refs.headerSearchSelect && this.$refs.headerSearchSelect.blur()
 | 
			
		||||
      this.options = []
 | 
			
		||||
      this.show = false
 | 
			
		||||
    },
 | 
			
		||||
    change(val) {
 | 
			
		||||
      this.$router.push(val.path)
 | 
			
		||||
      this.search = ''
 | 
			
		||||
      this.options = []
 | 
			
		||||
      this.$nextTick(() => {
 | 
			
		||||
        this.show = false
 | 
			
		||||
      })
 | 
			
		||||
    },
 | 
			
		||||
    initFuse(list) {
 | 
			
		||||
      this.fuse = new Fuse(list, {
 | 
			
		||||
        shouldSort: true,
 | 
			
		||||
        threshold: 0.4,
 | 
			
		||||
        location: 0,
 | 
			
		||||
        distance: 100,
 | 
			
		||||
        maxPatternLength: 32,
 | 
			
		||||
        minMatchCharLength: 1,
 | 
			
		||||
        keys: [{
 | 
			
		||||
          name: 'title',
 | 
			
		||||
          weight: 0.7
 | 
			
		||||
        }, {
 | 
			
		||||
          name: 'path',
 | 
			
		||||
          weight: 0.3
 | 
			
		||||
        }]
 | 
			
		||||
      })
 | 
			
		||||
    },
 | 
			
		||||
    // Filter out the routes that can be displayed in the sidebar
 | 
			
		||||
    // And generate the internationalized title
 | 
			
		||||
    generateRoutes(routes, basePath = '/', prefixTitle = []) {
 | 
			
		||||
      let res = []
 | 
			
		||||
 | 
			
		||||
      for (const router of routes) {
 | 
			
		||||
        // skip hidden router
 | 
			
		||||
        if (router.hidden) { continue }
 | 
			
		||||
 | 
			
		||||
        const data = {
 | 
			
		||||
          path: path.resolve(basePath, router.path),
 | 
			
		||||
          title: [...prefixTitle]
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (router.meta && router.meta.title) {
 | 
			
		||||
          data.title = [...data.title, router.meta.title]
 | 
			
		||||
 | 
			
		||||
          if (router.redirect !== 'noRedirect') {
 | 
			
		||||
            // only push the routes with title
 | 
			
		||||
            // special case: need to exclude parent router without redirect
 | 
			
		||||
            res.push(data)
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // recursive child routes
 | 
			
		||||
        if (router.children) {
 | 
			
		||||
          const tempRoutes = this.generateRoutes(router.children, data.path, data.title)
 | 
			
		||||
          if (tempRoutes.length >= 1) {
 | 
			
		||||
            res = [...res, ...tempRoutes]
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      return res
 | 
			
		||||
    },
 | 
			
		||||
    querySearch(query) {
 | 
			
		||||
      if (query !== '') {
 | 
			
		||||
        this.options = this.fuse.search(query)
 | 
			
		||||
      } else {
 | 
			
		||||
        this.options = []
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.header-search {
 | 
			
		||||
  font-size: 0 !important;
 | 
			
		||||
 | 
			
		||||
  .search-icon {
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    font-size: 18px;
 | 
			
		||||
    vertical-align: middle;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .header-search-select {
 | 
			
		||||
    font-size: 18px;
 | 
			
		||||
    transition: width 0.2s;
 | 
			
		||||
    width: 0;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
    background: transparent;
 | 
			
		||||
    border-radius: 0;
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
    vertical-align: middle;
 | 
			
		||||
 | 
			
		||||
    /deep/ .el-input__inner {
 | 
			
		||||
      border-radius: 0;
 | 
			
		||||
      border: 0;
 | 
			
		||||
      padding-left: 0;
 | 
			
		||||
      padding-right: 0;
 | 
			
		||||
      box-shadow: none !important;
 | 
			
		||||
      border-bottom: 1px solid #d9d9d9;
 | 
			
		||||
      vertical-align: middle;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &.show {
 | 
			
		||||
    .header-search-select {
 | 
			
		||||
      width: 210px;
 | 
			
		||||
      margin-left: 10px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										1778
									
								
								src/components/ImageCropper/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										19
									
								
								src/components/ImageCropper/utils/data2blob.js
									
									
									
									
									
										Executable file
									
								
							
							
						
						@@ -0,0 +1,19 @@
 | 
			
		||||
/**
 | 
			
		||||
 * database64文件格式转换为2进制
 | 
			
		||||
 *
 | 
			
		||||
 * @param  {[String]} data dataURL 的格式为 “data:image/png;base64,****”,逗号之前都是一些说明性的文字,我们只需要逗号之后的就行了
 | 
			
		||||
 * @param  {[String]} mime [description]
 | 
			
		||||
 * @return {[blob]}      [description]
 | 
			
		||||
 */
 | 
			
		||||
export default function(data, mime) {
 | 
			
		||||
  data = data.split(',')[1]
 | 
			
		||||
  data = window.atob(data)
 | 
			
		||||
  var ia = new Uint8Array(data.length)
 | 
			
		||||
  for (var i = 0; i < data.length; i++) {
 | 
			
		||||
    ia[i] = data.charCodeAt(i)
 | 
			
		||||
  }
 | 
			
		||||
  // canvas.toDataURL 返回的默认格式就是 image/png
 | 
			
		||||
  return new Blob([ia], {
 | 
			
		||||
    type: mime
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										39
									
								
								src/components/ImageCropper/utils/effectRipple.js
									
									
									
									
									
										Executable file
									
								
							
							
						
						@@ -0,0 +1,39 @@
 | 
			
		||||
/**
 | 
			
		||||
 * 点击波纹效果
 | 
			
		||||
 *
 | 
			
		||||
 * @param  {[event]} e        [description]
 | 
			
		||||
 * @param  {[Object]} arg_opts [description]
 | 
			
		||||
 * @return {[bollean]}          [description]
 | 
			
		||||
 */
 | 
			
		||||
export default function(e, arg_opts) {
 | 
			
		||||
  var opts = Object.assign({
 | 
			
		||||
    ele: e.target, // 波纹作用元素
 | 
			
		||||
    type: 'hit', // hit点击位置扩散center中心点扩展
 | 
			
		||||
    bgc: 'rgba(0, 0, 0, 0.15)' // 波纹颜色
 | 
			
		||||
  }, arg_opts)
 | 
			
		||||
  var target = opts.ele
 | 
			
		||||
  if (target) {
 | 
			
		||||
    var rect = target.getBoundingClientRect()
 | 
			
		||||
    var ripple = target.querySelector('.e-ripple')
 | 
			
		||||
    if (!ripple) {
 | 
			
		||||
      ripple = document.createElement('span')
 | 
			
		||||
      ripple.className = 'e-ripple'
 | 
			
		||||
      ripple.style.height = ripple.style.width = Math.max(rect.width, rect.height) + 'px'
 | 
			
		||||
      target.appendChild(ripple)
 | 
			
		||||
    } else {
 | 
			
		||||
      ripple.className = 'e-ripple'
 | 
			
		||||
    }
 | 
			
		||||
    switch (opts.type) {
 | 
			
		||||
      case 'center':
 | 
			
		||||
        ripple.style.top = (rect.height / 2 - ripple.offsetHeight / 2) + 'px'
 | 
			
		||||
        ripple.style.left = (rect.width / 2 - ripple.offsetWidth / 2) + 'px'
 | 
			
		||||
        break
 | 
			
		||||
      default:
 | 
			
		||||
        ripple.style.top = (e.pageY - rect.top - ripple.offsetHeight / 2 - document.body.scrollTop) + 'px'
 | 
			
		||||
        ripple.style.left = (e.pageX - rect.left - ripple.offsetWidth / 2 - document.body.scrollLeft) + 'px'
 | 
			
		||||
    }
 | 
			
		||||
    ripple.style.backgroundColor = opts.bgc
 | 
			
		||||
    ripple.className = 'e-ripple z-active'
 | 
			
		||||
    return false
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										232
									
								
								src/components/ImageCropper/utils/language.js
									
									
									
									
									
										Executable file
									
								
							
							
						
						@@ -0,0 +1,232 @@
 | 
			
		||||
export default {
 | 
			
		||||
  zh: {
 | 
			
		||||
    hint: '点击,或拖动图片至此处',
 | 
			
		||||
    loading: '正在上传……',
 | 
			
		||||
    noSupported: '浏览器不支持该功能,请使用IE10以上或其他现在浏览器!',
 | 
			
		||||
    success: '上传成功',
 | 
			
		||||
    fail: '图片上传失败',
 | 
			
		||||
    preview: '头像预览',
 | 
			
		||||
    btn: {
 | 
			
		||||
      off: '取消',
 | 
			
		||||
      close: '关闭',
 | 
			
		||||
      back: '上一步',
 | 
			
		||||
      save: '保存'
 | 
			
		||||
    },
 | 
			
		||||
    error: {
 | 
			
		||||
      onlyImg: '仅限图片格式',
 | 
			
		||||
      outOfSize: '单文件大小不能超过 ',
 | 
			
		||||
      lowestPx: '图片最低像素为(宽*高):'
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  'zh-tw': {
 | 
			
		||||
    hint: '點擊,或拖動圖片至此處',
 | 
			
		||||
    loading: '正在上傳……',
 | 
			
		||||
    noSupported: '瀏覽器不支持該功能,請使用IE10以上或其他現代瀏覽器!',
 | 
			
		||||
    success: '上傳成功',
 | 
			
		||||
    fail: '圖片上傳失敗',
 | 
			
		||||
    preview: '頭像預覽',
 | 
			
		||||
    btn: {
 | 
			
		||||
      off: '取消',
 | 
			
		||||
      close: '關閉',
 | 
			
		||||
      back: '上一步',
 | 
			
		||||
      save: '保存'
 | 
			
		||||
    },
 | 
			
		||||
    error: {
 | 
			
		||||
      onlyImg: '僅限圖片格式',
 | 
			
		||||
      outOfSize: '單文件大小不能超過 ',
 | 
			
		||||
      lowestPx: '圖片最低像素為(寬*高):'
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  en: {
 | 
			
		||||
    hint: 'Click or drag the file here to upload',
 | 
			
		||||
    loading: 'Uploading…',
 | 
			
		||||
    noSupported: 'Browser is not supported, please use IE10+ or other browsers',
 | 
			
		||||
    success: 'Upload success',
 | 
			
		||||
    fail: 'Upload failed',
 | 
			
		||||
    preview: 'Preview',
 | 
			
		||||
    btn: {
 | 
			
		||||
      off: 'Cancel',
 | 
			
		||||
      close: 'Close',
 | 
			
		||||
      back: 'Back',
 | 
			
		||||
      save: 'Save'
 | 
			
		||||
    },
 | 
			
		||||
    error: {
 | 
			
		||||
      onlyImg: 'Image only',
 | 
			
		||||
      outOfSize: 'Image exceeds size limit: ',
 | 
			
		||||
      lowestPx: 'Image\'s size is too low. Expected at least: '
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  ro: {
 | 
			
		||||
    hint: 'Atinge sau trage fișierul aici',
 | 
			
		||||
    loading: 'Se încarcă',
 | 
			
		||||
    noSupported: 'Browser-ul tău nu suportă acest feature. Te rugăm încearcă cu alt browser.',
 | 
			
		||||
    success: 'S-a încărcat cu succes',
 | 
			
		||||
    fail: 'A apărut o problemă la încărcare',
 | 
			
		||||
    preview: 'Previzualizează',
 | 
			
		||||
 | 
			
		||||
    btn: {
 | 
			
		||||
      off: 'Anulează',
 | 
			
		||||
      close: 'Închide',
 | 
			
		||||
      back: 'Înapoi',
 | 
			
		||||
      save: 'Salvează'
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    error: {
 | 
			
		||||
      onlyImg: 'Doar imagini',
 | 
			
		||||
      outOfSize: 'Imaginea depășește limita de: ',
 | 
			
		||||
      loewstPx: 'Imaginea este prea mică; Minim: '
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  ru: {
 | 
			
		||||
    hint: 'Нажмите, или перетащите файл в это окно',
 | 
			
		||||
    loading: 'Загружаю……',
 | 
			
		||||
    noSupported: 'Ваш браузер не поддерживается, пожалуйста, используйте IE10 + или другие браузеры',
 | 
			
		||||
    success: 'Загрузка выполнена успешно',
 | 
			
		||||
    fail: 'Ошибка загрузки',
 | 
			
		||||
    preview: 'Предпросмотр',
 | 
			
		||||
    btn: {
 | 
			
		||||
      off: 'Отменить',
 | 
			
		||||
      close: 'Закрыть',
 | 
			
		||||
      back: 'Назад',
 | 
			
		||||
      save: 'Сохранить'
 | 
			
		||||
    },
 | 
			
		||||
    error: {
 | 
			
		||||
      onlyImg: 'Только изображения',
 | 
			
		||||
      outOfSize: 'Изображение превышает предельный размер: ',
 | 
			
		||||
      lowestPx: 'Минимальный размер изображения: '
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  'pt-br': {
 | 
			
		||||
    hint: 'Clique ou arraste o arquivo aqui para carregar',
 | 
			
		||||
    loading: 'Carregando…',
 | 
			
		||||
    noSupported: 'Browser não suportado, use o IE10+ ou outro browser',
 | 
			
		||||
    success: 'Sucesso ao carregar imagem',
 | 
			
		||||
    fail: 'Falha ao carregar imagem',
 | 
			
		||||
    preview: 'Pré-visualizar',
 | 
			
		||||
    btn: {
 | 
			
		||||
      off: 'Cancelar',
 | 
			
		||||
      close: 'Fechar',
 | 
			
		||||
      back: 'Voltar',
 | 
			
		||||
      save: 'Salvar'
 | 
			
		||||
    },
 | 
			
		||||
    error: {
 | 
			
		||||
      onlyImg: 'Apenas imagens',
 | 
			
		||||
      outOfSize: 'A imagem excede o limite de tamanho: ',
 | 
			
		||||
      lowestPx: 'O tamanho da imagem é muito pequeno. Tamanho mínimo: '
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  fr: {
 | 
			
		||||
    hint: 'Cliquez ou glissez le fichier ici.',
 | 
			
		||||
    loading: 'Téléchargement…',
 | 
			
		||||
    noSupported: 'Votre navigateur n\'est pas supporté. Utilisez IE10 + ou un autre navigateur s\'il vous plaît.',
 | 
			
		||||
    success: 'Téléchargement réussit',
 | 
			
		||||
    fail: 'Téléchargement echoué',
 | 
			
		||||
    preview: 'Aperçu',
 | 
			
		||||
    btn: {
 | 
			
		||||
      off: 'Annuler',
 | 
			
		||||
      close: 'Fermer',
 | 
			
		||||
      back: 'Retour',
 | 
			
		||||
      save: 'Enregistrer'
 | 
			
		||||
    },
 | 
			
		||||
    error: {
 | 
			
		||||
      onlyImg: 'Image uniquement',
 | 
			
		||||
      outOfSize: 'L\'image sélectionnée dépasse la taille maximum: ',
 | 
			
		||||
      lowestPx: 'L\'image sélectionnée est trop petite. Dimensions attendues: '
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  nl: {
 | 
			
		||||
    hint: 'Klik hier of sleep een afbeelding in dit vlak',
 | 
			
		||||
    loading: 'Uploaden…',
 | 
			
		||||
    noSupported: 'Je browser wordt helaas niet ondersteund. Gebruik IE10+ of een andere browser.',
 | 
			
		||||
    success: 'Upload succesvol',
 | 
			
		||||
    fail: 'Upload mislukt',
 | 
			
		||||
    preview: 'Voorbeeld',
 | 
			
		||||
    btn: {
 | 
			
		||||
      off: 'Annuleren',
 | 
			
		||||
      close: 'Sluiten',
 | 
			
		||||
      back: 'Terug',
 | 
			
		||||
      save: 'Opslaan'
 | 
			
		||||
    },
 | 
			
		||||
    error: {
 | 
			
		||||
      onlyImg: 'Alleen afbeeldingen',
 | 
			
		||||
      outOfSize: 'De afbeelding is groter dan: ',
 | 
			
		||||
      lowestPx: 'De afbeelding is te klein! Minimale afmetingen: '
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  tr: {
 | 
			
		||||
    hint: 'Tıkla veya yüklemek istediğini buraya sürükle',
 | 
			
		||||
    loading: 'Yükleniyor…',
 | 
			
		||||
    noSupported: 'Tarayıcı desteklenmiyor, lütfen IE10+ veya farklı tarayıcı kullanın',
 | 
			
		||||
    success: 'Yükleme başarılı',
 | 
			
		||||
    fail: 'Yüklemede hata oluştu',
 | 
			
		||||
    preview: 'Önizle',
 | 
			
		||||
    btn: {
 | 
			
		||||
      off: 'İptal',
 | 
			
		||||
      close: 'Kapat',
 | 
			
		||||
      back: 'Geri',
 | 
			
		||||
      save: 'Kaydet'
 | 
			
		||||
    },
 | 
			
		||||
    error: {
 | 
			
		||||
      onlyImg: 'Sadece resim',
 | 
			
		||||
      outOfSize: 'Resim yükleme limitini aşıyor: ',
 | 
			
		||||
      lowestPx: 'Resmin boyutu çok küçük. En az olması gereken: '
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  'es-MX': {
 | 
			
		||||
    hint: 'Selecciona o arrastra una imagen',
 | 
			
		||||
    loading: 'Subiendo...',
 | 
			
		||||
    noSupported: 'Tu navegador no es soportado, porfavor usa IE10+ u otros navegadores mas recientes',
 | 
			
		||||
    success: 'Subido exitosamente',
 | 
			
		||||
    fail: 'Sucedió un error',
 | 
			
		||||
    preview: 'Vista previa',
 | 
			
		||||
    btn: {
 | 
			
		||||
      off: 'Cancelar',
 | 
			
		||||
      close: 'Cerrar',
 | 
			
		||||
      back: 'Atras',
 | 
			
		||||
      save: 'Guardar'
 | 
			
		||||
    },
 | 
			
		||||
    error: {
 | 
			
		||||
      onlyImg: 'Unicamente imagenes',
 | 
			
		||||
      outOfSize: 'La imagen excede el tamaño maximo:',
 | 
			
		||||
      lowestPx: 'La imagen es demasiado pequeño. Se espera por lo menos:'
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  de: {
 | 
			
		||||
    hint: 'Klick hier oder zieh eine Datei hier rein zum Hochladen',
 | 
			
		||||
    loading: 'Hochladen…',
 | 
			
		||||
    noSupported: 'Browser wird nicht unterstützt, bitte verwende IE10+ oder andere Browser',
 | 
			
		||||
    success: 'Upload erfolgreich',
 | 
			
		||||
    fail: 'Upload fehlgeschlagen',
 | 
			
		||||
    preview: 'Vorschau',
 | 
			
		||||
    btn: {
 | 
			
		||||
      off: 'Abbrechen',
 | 
			
		||||
      close: 'Schließen',
 | 
			
		||||
      back: 'Zurück',
 | 
			
		||||
      save: 'Speichern'
 | 
			
		||||
    },
 | 
			
		||||
    error: {
 | 
			
		||||
      onlyImg: 'Nur Bilder',
 | 
			
		||||
      outOfSize: 'Das Bild ist zu groß: ',
 | 
			
		||||
      lowestPx: 'Das Bild ist zu klein. Mindestens: '
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  ja: {
 | 
			
		||||
    hint: 'クリック・ドラッグしてファイルをアップロード',
 | 
			
		||||
    loading: 'アップロード中...',
 | 
			
		||||
    noSupported: 'このブラウザは対応されていません。IE10+かその他の主要ブラウザをお使いください。',
 | 
			
		||||
    success: 'アップロード成功',
 | 
			
		||||
    fail: 'アップロード失敗',
 | 
			
		||||
    preview: 'プレビュー',
 | 
			
		||||
    btn: {
 | 
			
		||||
      off: 'キャンセル',
 | 
			
		||||
      close: '閉じる',
 | 
			
		||||
      back: '戻る',
 | 
			
		||||
      save: '保存'
 | 
			
		||||
    },
 | 
			
		||||
    error: {
 | 
			
		||||
      onlyImg: '画像のみ',
 | 
			
		||||
      outOfSize: '画像サイズが上限を超えています。上限: ',
 | 
			
		||||
      lowestPx: '画像が小さすぎます。最小サイズ: '
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										7
									
								
								src/components/ImageCropper/utils/mimes.js
									
									
									
									
									
										Executable file
									
								
							
							
						
						@@ -0,0 +1,7 @@
 | 
			
		||||
export default {
 | 
			
		||||
  'jpg': 'image/jpeg',
 | 
			
		||||
  'png': 'image/png',
 | 
			
		||||
  'gif': 'image/gif',
 | 
			
		||||
  'svg': 'image/svg+xml',
 | 
			
		||||
  'psd': 'image/photoshop'
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										72
									
								
								src/components/JsonEditor/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,72 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="json-editor">
 | 
			
		||||
    <textarea ref="textarea" />
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import CodeMirror from 'codemirror'
 | 
			
		||||
import 'codemirror/addon/lint/lint.css'
 | 
			
		||||
import 'codemirror/lib/codemirror.css'
 | 
			
		||||
import 'codemirror/theme/rubyblue.css'
 | 
			
		||||
require('script-loader!jsonlint')
 | 
			
		||||
import 'codemirror/mode/javascript/javascript'
 | 
			
		||||
import 'codemirror/addon/lint/lint'
 | 
			
		||||
import 'codemirror/addon/lint/json-lint'
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'JsonEditor',
 | 
			
		||||
  /* eslint-disable vue/require-prop-types */
 | 
			
		||||
  props: ['value'],
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      jsonEditor: false
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  watch: {
 | 
			
		||||
    value(value) {
 | 
			
		||||
      const editorValue = this.jsonEditor.getValue()
 | 
			
		||||
      if (value !== editorValue) {
 | 
			
		||||
        this.jsonEditor.setValue(JSON.stringify(this.value, null, 2))
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  mounted() {
 | 
			
		||||
    this.jsonEditor = CodeMirror.fromTextArea(this.$refs.textarea, {
 | 
			
		||||
      lineNumbers: true,
 | 
			
		||||
      mode: 'application/json',
 | 
			
		||||
      gutters: ['CodeMirror-lint-markers'],
 | 
			
		||||
      theme: 'rubyblue',
 | 
			
		||||
      lint: true
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    this.jsonEditor.setValue(JSON.stringify(this.value, null, 2))
 | 
			
		||||
    this.jsonEditor.on('change', cm => {
 | 
			
		||||
      this.$emit('changed', cm.getValue())
 | 
			
		||||
      this.$emit('input', cm.getValue())
 | 
			
		||||
    })
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    getValue() {
 | 
			
		||||
      return this.jsonEditor.getValue()
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
.json-editor{
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  position: relative;
 | 
			
		||||
}
 | 
			
		||||
.json-editor >>> .CodeMirror {
 | 
			
		||||
  height: auto;
 | 
			
		||||
  min-height: 300px;
 | 
			
		||||
}
 | 
			
		||||
.json-editor >>> .CodeMirror-scroll{
 | 
			
		||||
  min-height: 300px;
 | 
			
		||||
}
 | 
			
		||||
.json-editor >>> .cm-s-rubyblue span.cm-string {
 | 
			
		||||
  color: #F08047;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										99
									
								
								src/components/Kanban/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,99 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="board-column">
 | 
			
		||||
    <div class="board-column-header">
 | 
			
		||||
      {{ headerText }}
 | 
			
		||||
    </div>
 | 
			
		||||
    <draggable
 | 
			
		||||
      :list="list"
 | 
			
		||||
      v-bind="$attrs"
 | 
			
		||||
      class="board-column-content"
 | 
			
		||||
      :set-data="setData"
 | 
			
		||||
    >
 | 
			
		||||
      <div v-for="element in list" :key="element.id" class="board-item">
 | 
			
		||||
        {{ element.name }} {{ element.id }}
 | 
			
		||||
      </div>
 | 
			
		||||
    </draggable>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import draggable from 'vuedraggable'
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'DragKanbanDemo',
 | 
			
		||||
  components: {
 | 
			
		||||
    draggable
 | 
			
		||||
  },
 | 
			
		||||
  props: {
 | 
			
		||||
    headerText: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      default: 'Header'
 | 
			
		||||
    },
 | 
			
		||||
    options: {
 | 
			
		||||
      type: Object,
 | 
			
		||||
      default() {
 | 
			
		||||
        return {}
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    list: {
 | 
			
		||||
      type: Array,
 | 
			
		||||
      default() {
 | 
			
		||||
        return []
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    setData(dataTransfer) {
 | 
			
		||||
      // to avoid Firefox bug
 | 
			
		||||
      // Detail see : https://github.com/RubaXa/Sortable/issues/1012
 | 
			
		||||
      dataTransfer.setData('Text', '')
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.board-column {
 | 
			
		||||
  min-width: 300px;
 | 
			
		||||
  min-height: 100px;
 | 
			
		||||
  height: auto;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  background: #f0f0f0;
 | 
			
		||||
  border-radius: 3px;
 | 
			
		||||
 | 
			
		||||
  .board-column-header {
 | 
			
		||||
    height: 50px;
 | 
			
		||||
    line-height: 50px;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
    padding: 0 20px;
 | 
			
		||||
    text-align: center;
 | 
			
		||||
    background: #333;
 | 
			
		||||
    color: #fff;
 | 
			
		||||
    border-radius: 3px 3px 0 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .board-column-content {
 | 
			
		||||
    height: auto;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
    border: 10px solid transparent;
 | 
			
		||||
    min-height: 60px;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    justify-content: flex-start;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
 | 
			
		||||
    .board-item {
 | 
			
		||||
      cursor: pointer;
 | 
			
		||||
      width: 100%;
 | 
			
		||||
      height: 64px;
 | 
			
		||||
      margin: 5px 0;
 | 
			
		||||
      background-color: #fff;
 | 
			
		||||
      text-align: left;
 | 
			
		||||
      line-height: 54px;
 | 
			
		||||
      padding: 5px 10px;
 | 
			
		||||
      box-sizing: border-box;
 | 
			
		||||
      box-shadow: 0px 1px 3px 0 rgba(0, 0, 0, 0.2);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										360
									
								
								src/components/MDinput/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,360 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div :class="computedClasses" class="material-input__component">
 | 
			
		||||
    <div :class="{iconClass:icon}">
 | 
			
		||||
      <i v-if="icon" :class="['el-icon-' + icon]" class="el-input__icon material-input__icon" />
 | 
			
		||||
      <input
 | 
			
		||||
        v-if="type === 'email'"
 | 
			
		||||
        v-model="currentValue"
 | 
			
		||||
        :name="name"
 | 
			
		||||
        :placeholder="fillPlaceHolder"
 | 
			
		||||
        :readonly="readonly"
 | 
			
		||||
        :disabled="disabled"
 | 
			
		||||
        :autocomplete="autoComplete"
 | 
			
		||||
        :required="required"
 | 
			
		||||
        type="email"
 | 
			
		||||
        class="material-input"
 | 
			
		||||
        @focus="handleMdFocus"
 | 
			
		||||
        @blur="handleMdBlur"
 | 
			
		||||
        @input="handleModelInput"
 | 
			
		||||
      >
 | 
			
		||||
      <input
 | 
			
		||||
        v-if="type === 'url'"
 | 
			
		||||
        v-model="currentValue"
 | 
			
		||||
        :name="name"
 | 
			
		||||
        :placeholder="fillPlaceHolder"
 | 
			
		||||
        :readonly="readonly"
 | 
			
		||||
        :disabled="disabled"
 | 
			
		||||
        :autocomplete="autoComplete"
 | 
			
		||||
        :required="required"
 | 
			
		||||
        type="url"
 | 
			
		||||
        class="material-input"
 | 
			
		||||
        @focus="handleMdFocus"
 | 
			
		||||
        @blur="handleMdBlur"
 | 
			
		||||
        @input="handleModelInput"
 | 
			
		||||
      >
 | 
			
		||||
      <input
 | 
			
		||||
        v-if="type === 'number'"
 | 
			
		||||
        v-model="currentValue"
 | 
			
		||||
        :name="name"
 | 
			
		||||
        :placeholder="fillPlaceHolder"
 | 
			
		||||
        :step="step"
 | 
			
		||||
        :readonly="readonly"
 | 
			
		||||
        :disabled="disabled"
 | 
			
		||||
        :autocomplete="autoComplete"
 | 
			
		||||
        :max="max"
 | 
			
		||||
        :min="min"
 | 
			
		||||
        :minlength="minlength"
 | 
			
		||||
        :maxlength="maxlength"
 | 
			
		||||
        :required="required"
 | 
			
		||||
        type="number"
 | 
			
		||||
        class="material-input"
 | 
			
		||||
        @focus="handleMdFocus"
 | 
			
		||||
        @blur="handleMdBlur"
 | 
			
		||||
        @input="handleModelInput"
 | 
			
		||||
      >
 | 
			
		||||
      <input
 | 
			
		||||
        v-if="type === 'password'"
 | 
			
		||||
        v-model="currentValue"
 | 
			
		||||
        :name="name"
 | 
			
		||||
        :placeholder="fillPlaceHolder"
 | 
			
		||||
        :readonly="readonly"
 | 
			
		||||
        :disabled="disabled"
 | 
			
		||||
        :autocomplete="autoComplete"
 | 
			
		||||
        :max="max"
 | 
			
		||||
        :min="min"
 | 
			
		||||
        :required="required"
 | 
			
		||||
        type="password"
 | 
			
		||||
        class="material-input"
 | 
			
		||||
        @focus="handleMdFocus"
 | 
			
		||||
        @blur="handleMdBlur"
 | 
			
		||||
        @input="handleModelInput"
 | 
			
		||||
      >
 | 
			
		||||
      <input
 | 
			
		||||
        v-if="type === 'tel'"
 | 
			
		||||
        v-model="currentValue"
 | 
			
		||||
        :name="name"
 | 
			
		||||
        :placeholder="fillPlaceHolder"
 | 
			
		||||
        :readonly="readonly"
 | 
			
		||||
        :disabled="disabled"
 | 
			
		||||
        :autocomplete="autoComplete"
 | 
			
		||||
        :required="required"
 | 
			
		||||
        type="tel"
 | 
			
		||||
        class="material-input"
 | 
			
		||||
        @focus="handleMdFocus"
 | 
			
		||||
        @blur="handleMdBlur"
 | 
			
		||||
        @input="handleModelInput"
 | 
			
		||||
      >
 | 
			
		||||
      <input
 | 
			
		||||
        v-if="type === 'text'"
 | 
			
		||||
        v-model="currentValue"
 | 
			
		||||
        :name="name"
 | 
			
		||||
        :placeholder="fillPlaceHolder"
 | 
			
		||||
        :readonly="readonly"
 | 
			
		||||
        :disabled="disabled"
 | 
			
		||||
        :autocomplete="autoComplete"
 | 
			
		||||
        :minlength="minlength"
 | 
			
		||||
        :maxlength="maxlength"
 | 
			
		||||
        :required="required"
 | 
			
		||||
        type="text"
 | 
			
		||||
        class="material-input"
 | 
			
		||||
        @focus="handleMdFocus"
 | 
			
		||||
        @blur="handleMdBlur"
 | 
			
		||||
        @input="handleModelInput"
 | 
			
		||||
      >
 | 
			
		||||
      <span class="material-input-bar" />
 | 
			
		||||
      <label class="material-label">
 | 
			
		||||
        <slot />
 | 
			
		||||
      </label>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
// source:https://github.com/wemake-services/vue-material-input/blob/master/src/components/MaterialInput.vue
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'MdInput',
 | 
			
		||||
  props: {
 | 
			
		||||
    /* eslint-disable */
 | 
			
		||||
    icon: String,
 | 
			
		||||
    name: String,
 | 
			
		||||
    type: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      default: 'text'
 | 
			
		||||
    },
 | 
			
		||||
    value: [String, Number],
 | 
			
		||||
    placeholder: String,
 | 
			
		||||
    readonly: Boolean,
 | 
			
		||||
    disabled: Boolean,
 | 
			
		||||
    min: String,
 | 
			
		||||
    max: String,
 | 
			
		||||
    step: String,
 | 
			
		||||
    minlength: Number,
 | 
			
		||||
    maxlength: Number,
 | 
			
		||||
    required: {
 | 
			
		||||
      type: Boolean,
 | 
			
		||||
      default: true
 | 
			
		||||
    },
 | 
			
		||||
    autoComplete: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      default: 'off'
 | 
			
		||||
    },
 | 
			
		||||
    validateEvent: {
 | 
			
		||||
      type: Boolean,
 | 
			
		||||
      default: true
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      currentValue: this.value,
 | 
			
		||||
      focus: false,
 | 
			
		||||
      fillPlaceHolder: null
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    computedClasses() {
 | 
			
		||||
      return {
 | 
			
		||||
        'material--active': this.focus,
 | 
			
		||||
        'material--disabled': this.disabled,
 | 
			
		||||
        'material--raised': Boolean(this.focus || this.currentValue) // has value
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  watch: {
 | 
			
		||||
    value(newValue) {
 | 
			
		||||
      this.currentValue = newValue
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    handleModelInput(event) {
 | 
			
		||||
      const value = event.target.value
 | 
			
		||||
      this.$emit('input', value)
 | 
			
		||||
      if (this.$parent.$options.componentName === 'ElFormItem') {
 | 
			
		||||
        if (this.validateEvent) {
 | 
			
		||||
          this.$parent.$emit('el.form.change', [value])
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      this.$emit('change', value)
 | 
			
		||||
    },
 | 
			
		||||
    handleMdFocus(event) {
 | 
			
		||||
      this.focus = true
 | 
			
		||||
      this.$emit('focus', event)
 | 
			
		||||
      if (this.placeholder && this.placeholder !== '') {
 | 
			
		||||
        this.fillPlaceHolder = this.placeholder
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    handleMdBlur(event) {
 | 
			
		||||
      this.focus = false
 | 
			
		||||
      this.$emit('blur', event)
 | 
			
		||||
      this.fillPlaceHolder = null
 | 
			
		||||
      if (this.$parent.$options.componentName === 'ElFormItem') {
 | 
			
		||||
        if (this.validateEvent) {
 | 
			
		||||
          this.$parent.$emit('el.form.blur', [this.currentValue])
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
  // Fonts:
 | 
			
		||||
  $font-size-base: 16px;
 | 
			
		||||
  $font-size-small: 18px;
 | 
			
		||||
  $font-size-smallest: 12px;
 | 
			
		||||
  $font-weight-normal: normal;
 | 
			
		||||
  $font-weight-bold: bold;
 | 
			
		||||
  $apixel: 1px;
 | 
			
		||||
  // Utils
 | 
			
		||||
  $spacer: 12px;
 | 
			
		||||
  $transition: 0.2s ease all;
 | 
			
		||||
  $index: 0px;
 | 
			
		||||
  $index-has-icon: 30px;
 | 
			
		||||
  // Theme:
 | 
			
		||||
  $color-white: white;
 | 
			
		||||
  $color-grey: #9E9E9E;
 | 
			
		||||
  $color-grey-light: #E0E0E0;
 | 
			
		||||
  $color-blue: #2196F3;
 | 
			
		||||
  $color-red: #F44336;
 | 
			
		||||
  $color-black: black;
 | 
			
		||||
  // Base clases:
 | 
			
		||||
  %base-bar-pseudo {
 | 
			
		||||
    content: '';
 | 
			
		||||
    height: 1px;
 | 
			
		||||
    width: 0;
 | 
			
		||||
    bottom: 0;
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    transition: $transition;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Mixins:
 | 
			
		||||
  @mixin slided-top() {
 | 
			
		||||
    top: - ($font-size-base + $spacer);
 | 
			
		||||
    left: 0;
 | 
			
		||||
    font-size: $font-size-base;
 | 
			
		||||
    font-weight: $font-weight-bold;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Component:
 | 
			
		||||
  .material-input__component {
 | 
			
		||||
    margin-top: 36px;
 | 
			
		||||
    position: relative;
 | 
			
		||||
    * {
 | 
			
		||||
      box-sizing: border-box;
 | 
			
		||||
    }
 | 
			
		||||
    .iconClass {
 | 
			
		||||
      .material-input__icon {
 | 
			
		||||
        position: absolute;
 | 
			
		||||
        left: 0;
 | 
			
		||||
        line-height: $font-size-base;
 | 
			
		||||
        color: $color-blue;
 | 
			
		||||
        top: $spacer;
 | 
			
		||||
        width: $index-has-icon;
 | 
			
		||||
        height: $font-size-base;
 | 
			
		||||
        font-size: $font-size-base;
 | 
			
		||||
        font-weight: $font-weight-normal;
 | 
			
		||||
        pointer-events: none;
 | 
			
		||||
      }
 | 
			
		||||
      .material-label {
 | 
			
		||||
        left: $index-has-icon;
 | 
			
		||||
      }
 | 
			
		||||
      .material-input {
 | 
			
		||||
        text-indent: $index-has-icon;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    .material-input {
 | 
			
		||||
      font-size: $font-size-base;
 | 
			
		||||
      padding: $spacer $spacer $spacer - $apixel * 10 $spacer / 2;
 | 
			
		||||
      display: block;
 | 
			
		||||
      width: 100%;
 | 
			
		||||
      border: none;
 | 
			
		||||
      line-height: 1;
 | 
			
		||||
      border-radius: 0;
 | 
			
		||||
      &:focus {
 | 
			
		||||
        outline: none;
 | 
			
		||||
        border: none;
 | 
			
		||||
        border-bottom: 1px solid transparent; // fixes the height issue
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    .material-label {
 | 
			
		||||
      font-weight: $font-weight-normal;
 | 
			
		||||
      position: absolute;
 | 
			
		||||
      pointer-events: none;
 | 
			
		||||
      left: $index;
 | 
			
		||||
      top: 0;
 | 
			
		||||
      transition: $transition;
 | 
			
		||||
      font-size: $font-size-small;
 | 
			
		||||
    }
 | 
			
		||||
    .material-input-bar {
 | 
			
		||||
      position: relative;
 | 
			
		||||
      display: block;
 | 
			
		||||
      width: 100%;
 | 
			
		||||
      &:before {
 | 
			
		||||
        @extend %base-bar-pseudo;
 | 
			
		||||
        left: 50%;
 | 
			
		||||
      }
 | 
			
		||||
      &:after {
 | 
			
		||||
        @extend %base-bar-pseudo;
 | 
			
		||||
        right: 50%;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    // Disabled state:
 | 
			
		||||
    &.material--disabled {
 | 
			
		||||
      .material-input {
 | 
			
		||||
        border-bottom-style: dashed;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    // Raised state:
 | 
			
		||||
    &.material--raised {
 | 
			
		||||
      .material-label {
 | 
			
		||||
        @include slided-top();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    // Active state:
 | 
			
		||||
    &.material--active {
 | 
			
		||||
      .material-input-bar {
 | 
			
		||||
        &:before,
 | 
			
		||||
        &:after {
 | 
			
		||||
          width: 50%;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .material-input__component {
 | 
			
		||||
    background: $color-white;
 | 
			
		||||
    .material-input {
 | 
			
		||||
      background: none;
 | 
			
		||||
      color: $color-black;
 | 
			
		||||
      text-indent: $index;
 | 
			
		||||
      border-bottom: 1px solid $color-grey-light;
 | 
			
		||||
    }
 | 
			
		||||
    .material-label {
 | 
			
		||||
      color: $color-grey;
 | 
			
		||||
    }
 | 
			
		||||
    .material-input-bar {
 | 
			
		||||
      &:before,
 | 
			
		||||
      &:after {
 | 
			
		||||
        background: $color-blue;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    // Active state:
 | 
			
		||||
    &.material--active {
 | 
			
		||||
      .material-label {
 | 
			
		||||
        color: $color-blue;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    // Errors:
 | 
			
		||||
    &.material--has-errors {
 | 
			
		||||
      &.material--active .material-label {
 | 
			
		||||
        color: $color-red;
 | 
			
		||||
      }
 | 
			
		||||
      .material-input-bar {
 | 
			
		||||
        &:before,
 | 
			
		||||
        &:after {
 | 
			
		||||
          background: transparent;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										31
									
								
								src/components/MarkdownEditor/default-options.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,31 @@
 | 
			
		||||
// doc: https://nhnent.github.io/tui.editor/api/latest/ToastUIEditor.html#ToastUIEditor
 | 
			
		||||
export default {
 | 
			
		||||
  minHeight: '200px',
 | 
			
		||||
  previewStyle: 'vertical',
 | 
			
		||||
  useCommandShortcut: true,
 | 
			
		||||
  useDefaultHTMLSanitizer: true,
 | 
			
		||||
  usageStatistics: false,
 | 
			
		||||
  hideModeSwitch: false,
 | 
			
		||||
  toolbarItems: [
 | 
			
		||||
    'heading',
 | 
			
		||||
    'bold',
 | 
			
		||||
    'italic',
 | 
			
		||||
    'strike',
 | 
			
		||||
    'divider',
 | 
			
		||||
    'hr',
 | 
			
		||||
    'quote',
 | 
			
		||||
    'divider',
 | 
			
		||||
    'ul',
 | 
			
		||||
    'ol',
 | 
			
		||||
    'task',
 | 
			
		||||
    'indent',
 | 
			
		||||
    'outdent',
 | 
			
		||||
    'divider',
 | 
			
		||||
    'table',
 | 
			
		||||
    'image',
 | 
			
		||||
    'link',
 | 
			
		||||
    'divider',
 | 
			
		||||
    'code',
 | 
			
		||||
    'codeblock'
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										118
									
								
								src/components/MarkdownEditor/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,118 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div :id="id" />
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
// deps for editor
 | 
			
		||||
import 'codemirror/lib/codemirror.css' // codemirror
 | 
			
		||||
import 'tui-editor/dist/tui-editor.css' // editor ui
 | 
			
		||||
import 'tui-editor/dist/tui-editor-contents.css' // editor content
 | 
			
		||||
 | 
			
		||||
import Editor from 'tui-editor'
 | 
			
		||||
import defaultOptions from './default-options'
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'MarkdownEditor',
 | 
			
		||||
  props: {
 | 
			
		||||
    value: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      default: ''
 | 
			
		||||
    },
 | 
			
		||||
    id: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      required: false,
 | 
			
		||||
      default() {
 | 
			
		||||
        return 'markdown-editor-' + +new Date() + ((Math.random() * 1000).toFixed(0) + '')
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    options: {
 | 
			
		||||
      type: Object,
 | 
			
		||||
      default() {
 | 
			
		||||
        return defaultOptions
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    mode: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      default: 'markdown'
 | 
			
		||||
    },
 | 
			
		||||
    height: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      required: false,
 | 
			
		||||
      default: '300px'
 | 
			
		||||
    },
 | 
			
		||||
    language: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      required: false,
 | 
			
		||||
      default: 'en_US' // https://github.com/nhnent/tui.editor/tree/master/src/js/langs
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      editor: null
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    editorOptions() {
 | 
			
		||||
      const options = Object.assign({}, defaultOptions, this.options)
 | 
			
		||||
      options.initialEditType = this.mode
 | 
			
		||||
      options.height = this.height
 | 
			
		||||
      options.language = this.language
 | 
			
		||||
      return options
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  watch: {
 | 
			
		||||
    value(newValue, preValue) {
 | 
			
		||||
      if (newValue !== preValue && newValue !== this.editor.getValue()) {
 | 
			
		||||
        this.editor.setValue(newValue)
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    language(val) {
 | 
			
		||||
      this.destroyEditor()
 | 
			
		||||
      this.initEditor()
 | 
			
		||||
    },
 | 
			
		||||
    height(newValue) {
 | 
			
		||||
      this.editor.height(newValue)
 | 
			
		||||
    },
 | 
			
		||||
    mode(newValue) {
 | 
			
		||||
      this.editor.changeMode(newValue)
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  mounted() {
 | 
			
		||||
    this.initEditor()
 | 
			
		||||
  },
 | 
			
		||||
  destroyed() {
 | 
			
		||||
    this.destroyEditor()
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    initEditor() {
 | 
			
		||||
      this.editor = new Editor({
 | 
			
		||||
        el: document.getElementById(this.id),
 | 
			
		||||
        ...this.editorOptions
 | 
			
		||||
      })
 | 
			
		||||
      if (this.value) {
 | 
			
		||||
        this.editor.setValue(this.value)
 | 
			
		||||
      }
 | 
			
		||||
      this.editor.on('change', () => {
 | 
			
		||||
        this.$emit('input', this.editor.getValue())
 | 
			
		||||
      })
 | 
			
		||||
    },
 | 
			
		||||
    destroyEditor() {
 | 
			
		||||
      if (!this.editor) return
 | 
			
		||||
      this.editor.off('change')
 | 
			
		||||
      this.editor.remove()
 | 
			
		||||
    },
 | 
			
		||||
    setValue(value) {
 | 
			
		||||
      this.editor.setValue(value)
 | 
			
		||||
    },
 | 
			
		||||
    getValue() {
 | 
			
		||||
      return this.editor.getValue()
 | 
			
		||||
    },
 | 
			
		||||
    setHtml(value) {
 | 
			
		||||
      this.editor.setHtml(value)
 | 
			
		||||
    },
 | 
			
		||||
    getHtml() {
 | 
			
		||||
      return this.editor.getHtml()
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
							
								
								
									
										101
									
								
								src/components/Pagination/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,101 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div :class="{'hidden':hidden}" class="pagination-container">
 | 
			
		||||
    <el-pagination
 | 
			
		||||
      :background="background"
 | 
			
		||||
      :current-page.sync="currentPage"
 | 
			
		||||
      :page-size.sync="pageSize"
 | 
			
		||||
      :layout="layout"
 | 
			
		||||
      :page-sizes="pageSizes"
 | 
			
		||||
      :total="total"
 | 
			
		||||
      v-bind="$attrs"
 | 
			
		||||
      @size-change="handleSizeChange"
 | 
			
		||||
      @current-change="handleCurrentChange"
 | 
			
		||||
    />
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import { scrollTo } from '@/utils/scroll-to'
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'Pagination',
 | 
			
		||||
  props: {
 | 
			
		||||
    total: {
 | 
			
		||||
      required: true,
 | 
			
		||||
      type: Number
 | 
			
		||||
    },
 | 
			
		||||
    page: {
 | 
			
		||||
      type: Number,
 | 
			
		||||
      default: 1
 | 
			
		||||
    },
 | 
			
		||||
    limit: {
 | 
			
		||||
      type: Number,
 | 
			
		||||
      default: 20
 | 
			
		||||
    },
 | 
			
		||||
    pageSizes: {
 | 
			
		||||
      type: Array,
 | 
			
		||||
      default() {
 | 
			
		||||
        return [10, 20, 30, 50]
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    layout: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      default: 'total, sizes, prev, pager, next, jumper'
 | 
			
		||||
    },
 | 
			
		||||
    background: {
 | 
			
		||||
      type: Boolean,
 | 
			
		||||
      default: true
 | 
			
		||||
    },
 | 
			
		||||
    autoScroll: {
 | 
			
		||||
      type: Boolean,
 | 
			
		||||
      default: true
 | 
			
		||||
    },
 | 
			
		||||
    hidden: {
 | 
			
		||||
      type: Boolean,
 | 
			
		||||
      default: false
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    currentPage: {
 | 
			
		||||
      get() {
 | 
			
		||||
        return this.page
 | 
			
		||||
      },
 | 
			
		||||
      set(val) {
 | 
			
		||||
        this.$emit('update:page', val)
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    pageSize: {
 | 
			
		||||
      get() {
 | 
			
		||||
        return this.limit
 | 
			
		||||
      },
 | 
			
		||||
      set(val) {
 | 
			
		||||
        this.$emit('update:limit', val)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    handleSizeChange(val) {
 | 
			
		||||
      this.$emit('pagination', { page: this.currentPage, limit: val })
 | 
			
		||||
      if (this.autoScroll) {
 | 
			
		||||
        scrollTo(0, 800)
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    handleCurrentChange(val) {
 | 
			
		||||
      this.$emit('pagination', { page: val, limit: this.pageSize })
 | 
			
		||||
      if (this.autoScroll) {
 | 
			
		||||
        scrollTo(0, 800)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
.pagination-container {
 | 
			
		||||
  background: #fff;
 | 
			
		||||
  padding: 32px 16px;
 | 
			
		||||
}
 | 
			
		||||
.pagination-container.hidden {
 | 
			
		||||
  display: none;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										142
									
								
								src/components/PanThumb/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,142 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div :style="{zIndex:zIndex,height:height,width:width}" class="pan-item">
 | 
			
		||||
    <div class="pan-info">
 | 
			
		||||
      <div class="pan-info-roles-container">
 | 
			
		||||
        <slot />
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <!-- eslint-disable-next-line -->
 | 
			
		||||
    <div :style="{backgroundImage: `url(${image})`}" class="pan-thumb"></div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'PanThumb',
 | 
			
		||||
  props: {
 | 
			
		||||
    image: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      required: true
 | 
			
		||||
    },
 | 
			
		||||
    zIndex: {
 | 
			
		||||
      type: Number,
 | 
			
		||||
      default: 1
 | 
			
		||||
    },
 | 
			
		||||
    width: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      default: '150px'
 | 
			
		||||
    },
 | 
			
		||||
    height: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      default: '150px'
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
.pan-item {
 | 
			
		||||
  width: 200px;
 | 
			
		||||
  height: 200px;
 | 
			
		||||
  border-radius: 50%;
 | 
			
		||||
  display: inline-block;
 | 
			
		||||
  position: relative;
 | 
			
		||||
  cursor: default;
 | 
			
		||||
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.pan-info-roles-container {
 | 
			
		||||
  padding: 20px;
 | 
			
		||||
  text-align: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.pan-thumb {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  background-position: center center;
 | 
			
		||||
  background-size: cover;
 | 
			
		||||
  border-radius: 50%;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  transform-origin: 95% 40%;
 | 
			
		||||
  transition: all 0.3s ease-in-out;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* .pan-thumb:after {
 | 
			
		||||
  content: '';
 | 
			
		||||
  width: 8px;
 | 
			
		||||
  height: 8px;
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  border-radius: 50%;
 | 
			
		||||
  top: 40%;
 | 
			
		||||
  left: 95%;
 | 
			
		||||
  margin: -4px 0 0 -4px;
 | 
			
		||||
  background: radial-gradient(ellipse at center, rgba(14, 14, 14, 1) 0%, rgba(125, 126, 125, 1) 100%);
 | 
			
		||||
  box-shadow: 0 0 1px rgba(255, 255, 255, 0.9);
 | 
			
		||||
} */
 | 
			
		||||
 | 
			
		||||
.pan-info {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  width: inherit;
 | 
			
		||||
  height: inherit;
 | 
			
		||||
  border-radius: 50%;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  box-shadow: inset 0 0 0 5px rgba(0, 0, 0, 0.05);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.pan-info h3 {
 | 
			
		||||
  color: #fff;
 | 
			
		||||
  text-transform: uppercase;
 | 
			
		||||
  position: relative;
 | 
			
		||||
  letter-spacing: 2px;
 | 
			
		||||
  font-size: 18px;
 | 
			
		||||
  margin: 0 60px;
 | 
			
		||||
  padding: 22px 0 0 0;
 | 
			
		||||
  height: 85px;
 | 
			
		||||
  font-family: 'Open Sans', Arial, sans-serif;
 | 
			
		||||
  text-shadow: 0 0 1px #fff, 0 1px 2px rgba(0, 0, 0, 0.3);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.pan-info p {
 | 
			
		||||
  color: #fff;
 | 
			
		||||
  padding: 10px 5px;
 | 
			
		||||
  font-style: italic;
 | 
			
		||||
  margin: 0 30px;
 | 
			
		||||
  font-size: 12px;
 | 
			
		||||
  border-top: 1px solid rgba(255, 255, 255, 0.5);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.pan-info p a {
 | 
			
		||||
  display: block;
 | 
			
		||||
  color: #333;
 | 
			
		||||
  width: 80px;
 | 
			
		||||
  height: 80px;
 | 
			
		||||
  background: rgba(255, 255, 255, 0.3);
 | 
			
		||||
  border-radius: 50%;
 | 
			
		||||
  color: #fff;
 | 
			
		||||
  font-style: normal;
 | 
			
		||||
  font-weight: 700;
 | 
			
		||||
  text-transform: uppercase;
 | 
			
		||||
  font-size: 9px;
 | 
			
		||||
  letter-spacing: 1px;
 | 
			
		||||
  padding-top: 24px;
 | 
			
		||||
  margin: 7px auto 0;
 | 
			
		||||
  font-family: 'Open Sans', Arial, sans-serif;
 | 
			
		||||
  opacity: 0;
 | 
			
		||||
  transition: transform 0.3s ease-in-out 0.2s, opacity 0.3s ease-in-out 0.2s, background 0.2s linear 0s;
 | 
			
		||||
  transform: translateX(60px) rotate(90deg);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.pan-info p a:hover {
 | 
			
		||||
  background: rgba(255, 255, 255, 0.5);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.pan-item:hover .pan-thumb {
 | 
			
		||||
  transform: rotate(-110deg);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.pan-item:hover .pan-info p a {
 | 
			
		||||
  opacity: 1;
 | 
			
		||||
  transform: translateX(0px) rotate(0deg);
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										145
									
								
								src/components/RightPanel/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,145 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div ref="rightPanel" :class="{show:show}" class="rightPanel-container">
 | 
			
		||||
    <div class="rightPanel-background" />
 | 
			
		||||
    <div class="rightPanel">
 | 
			
		||||
      <div class="handle-button" :style="{'top':buttonTop+'px','background-color':theme}" @click="show=!show">
 | 
			
		||||
        <i :class="show?'el-icon-close':'el-icon-setting'" />
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="rightPanel-items">
 | 
			
		||||
        <slot />
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import { addClass, removeClass } from '@/utils'
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'RightPanel',
 | 
			
		||||
  props: {
 | 
			
		||||
    clickNotClose: {
 | 
			
		||||
      default: false,
 | 
			
		||||
      type: Boolean
 | 
			
		||||
    },
 | 
			
		||||
    buttonTop: {
 | 
			
		||||
      default: 250,
 | 
			
		||||
      type: Number
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      show: false
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    theme() {
 | 
			
		||||
      return this.$store.state.settings.theme
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  watch: {
 | 
			
		||||
    show(value) {
 | 
			
		||||
      if (value && !this.clickNotClose) {
 | 
			
		||||
        this.addEventClick()
 | 
			
		||||
      }
 | 
			
		||||
      if (value) {
 | 
			
		||||
        addClass(document.body, 'showRightPanel')
 | 
			
		||||
      } else {
 | 
			
		||||
        removeClass(document.body, 'showRightPanel')
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  mounted() {
 | 
			
		||||
    this.insertToBody()
 | 
			
		||||
  },
 | 
			
		||||
  beforeDestroy() {
 | 
			
		||||
    const elx = this.$refs.rightPanel
 | 
			
		||||
    elx.remove()
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    addEventClick() {
 | 
			
		||||
      window.addEventListener('click', this.closeSidebar)
 | 
			
		||||
    },
 | 
			
		||||
    closeSidebar(evt) {
 | 
			
		||||
      const parent = evt.target.closest('.rightPanel')
 | 
			
		||||
      if (!parent) {
 | 
			
		||||
        this.show = false
 | 
			
		||||
        window.removeEventListener('click', this.closeSidebar)
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    insertToBody() {
 | 
			
		||||
      const elx = this.$refs.rightPanel
 | 
			
		||||
      const body = document.querySelector('body')
 | 
			
		||||
      body.insertBefore(elx, body.firstChild)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
.showRightPanel {
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  position: relative;
 | 
			
		||||
  width: calc(100% - 15px);
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.rightPanel-background {
 | 
			
		||||
  position: fixed;
 | 
			
		||||
  top: 0;
 | 
			
		||||
  left: 0;
 | 
			
		||||
  opacity: 0;
 | 
			
		||||
  transition: opacity .3s cubic-bezier(.7, .3, .1, 1);
 | 
			
		||||
  background: rgba(0, 0, 0, .2);
 | 
			
		||||
  z-index: -1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.rightPanel {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  max-width: 260px;
 | 
			
		||||
  height: 100vh;
 | 
			
		||||
  position: fixed;
 | 
			
		||||
  top: 0;
 | 
			
		||||
  right: 0;
 | 
			
		||||
  box-shadow: 0px 0px 15px 0px rgba(0, 0, 0, .05);
 | 
			
		||||
  transition: all .25s cubic-bezier(.7, .3, .1, 1);
 | 
			
		||||
  transform: translate(100%);
 | 
			
		||||
  background: #fff;
 | 
			
		||||
  z-index: 40000;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.show {
 | 
			
		||||
  transition: all .3s cubic-bezier(.7, .3, .1, 1);
 | 
			
		||||
 | 
			
		||||
  .rightPanel-background {
 | 
			
		||||
    z-index: 20000;
 | 
			
		||||
    opacity: 1;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .rightPanel {
 | 
			
		||||
    transform: translate(0);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.handle-button {
 | 
			
		||||
  width: 48px;
 | 
			
		||||
  height: 48px;
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  left: -48px;
 | 
			
		||||
  text-align: center;
 | 
			
		||||
  font-size: 24px;
 | 
			
		||||
  border-radius: 6px 0 0 6px !important;
 | 
			
		||||
  z-index: 0;
 | 
			
		||||
  pointer-events: auto;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  color: #fff;
 | 
			
		||||
  line-height: 48px;
 | 
			
		||||
  i {
 | 
			
		||||
    font-size: 24px;
 | 
			
		||||
    line-height: 48px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										60
									
								
								src/components/Screenfull/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,60 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div>
 | 
			
		||||
    <svg-icon :icon-class="isFullscreen?'exit-fullscreen':'fullscreen'" @click="click" />
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import screenfull from 'screenfull'
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'Screenfull',
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      isFullscreen: false
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  mounted() {
 | 
			
		||||
    this.init()
 | 
			
		||||
  },
 | 
			
		||||
  beforeDestroy() {
 | 
			
		||||
    this.destroy()
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    click() {
 | 
			
		||||
      if (!screenfull.enabled) {
 | 
			
		||||
        this.$message({
 | 
			
		||||
          message: 'you browser can not work',
 | 
			
		||||
          type: 'warning'
 | 
			
		||||
        })
 | 
			
		||||
        return false
 | 
			
		||||
      }
 | 
			
		||||
      screenfull.toggle()
 | 
			
		||||
    },
 | 
			
		||||
    change() {
 | 
			
		||||
      this.isFullscreen = screenfull.isFullscreen
 | 
			
		||||
    },
 | 
			
		||||
    init() {
 | 
			
		||||
      if (screenfull.enabled) {
 | 
			
		||||
        screenfull.on('change', this.change)
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    destroy() {
 | 
			
		||||
      if (screenfull.enabled) {
 | 
			
		||||
        screenfull.off('change', this.change)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
.screenfull-svg {
 | 
			
		||||
  display: inline-block;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  fill: #5a5e66;;
 | 
			
		||||
  width: 20px;
 | 
			
		||||
  height: 20px;
 | 
			
		||||
  vertical-align: 10px;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										100
									
								
								src/components/Share/DropdownMenu.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,100 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div :class="{active:isActive}" class="share-dropdown-menu">
 | 
			
		||||
    <div class="share-dropdown-menu-wrapper">
 | 
			
		||||
      <span class="share-dropdown-menu-title" @click.self="clickTitle">{{ title }}</span>
 | 
			
		||||
      <div v-for="(item,index) of items" :key="index" class="share-dropdown-menu-item">
 | 
			
		||||
        <a v-if="item.href" :href="item.href" target="_blank">{{ item.title }}</a>
 | 
			
		||||
        <span v-else>{{ item.title }}</span>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
export default {
 | 
			
		||||
  props: {
 | 
			
		||||
    items: {
 | 
			
		||||
      type: Array,
 | 
			
		||||
      default: function() {
 | 
			
		||||
        return []
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    title: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      default: 'vue'
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      isActive: false
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    clickTitle() {
 | 
			
		||||
      this.isActive = !this.isActive
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" >
 | 
			
		||||
$n: 9; //和items.length 相同
 | 
			
		||||
$t: .1s;
 | 
			
		||||
.share-dropdown-menu {
 | 
			
		||||
  width: 250px;
 | 
			
		||||
  position: relative;
 | 
			
		||||
  z-index: 1;
 | 
			
		||||
  &-title {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    display: block;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    background: black;
 | 
			
		||||
    color: white;
 | 
			
		||||
    height: 60px;
 | 
			
		||||
    line-height: 60px;
 | 
			
		||||
    font-size: 20px;
 | 
			
		||||
    text-align: center;
 | 
			
		||||
    z-index: 2;
 | 
			
		||||
    transform: translate3d(0,0,0);
 | 
			
		||||
  }
 | 
			
		||||
  &-wrapper {
 | 
			
		||||
    position: relative;
 | 
			
		||||
  }
 | 
			
		||||
  &-item {
 | 
			
		||||
    text-align: center;
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    background: #e0e0e0;
 | 
			
		||||
    line-height: 60px;
 | 
			
		||||
    height: 60px;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    font-size: 20px;
 | 
			
		||||
    opacity: 1;
 | 
			
		||||
    transition: transform 0.28s ease;
 | 
			
		||||
    &:hover {
 | 
			
		||||
      background: black;
 | 
			
		||||
      color: white;
 | 
			
		||||
    }
 | 
			
		||||
    @for $i from 1 through $n {
 | 
			
		||||
      &:nth-of-type(#{$i}) {
 | 
			
		||||
        z-index: -1;
 | 
			
		||||
        transition-delay: $i*$t;
 | 
			
		||||
        transform: translate3d(0, -60px, 0);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  &.active {
 | 
			
		||||
    .share-dropdown-menu-wrapper {
 | 
			
		||||
      z-index: 1;
 | 
			
		||||
    }
 | 
			
		||||
    .share-dropdown-menu-item {
 | 
			
		||||
      @for $i from 1 through $n {
 | 
			
		||||
        &:nth-of-type(#{$i}) {
 | 
			
		||||
          transition-delay: ($n - $i)*$t;
 | 
			
		||||
          transform: translate3d(0, ($i - 1)*60px, 0);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										57
									
								
								src/components/SizeSelect/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,57 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <el-dropdown trigger="click" @command="handleSetSize">
 | 
			
		||||
    <div>
 | 
			
		||||
      <svg-icon class-name="size-icon" icon-class="size" />
 | 
			
		||||
    </div>
 | 
			
		||||
    <el-dropdown-menu slot="dropdown">
 | 
			
		||||
      <el-dropdown-item v-for="item of sizeOptions" :key="item.value" :disabled="size===item.value" :command="item.value">
 | 
			
		||||
        {{
 | 
			
		||||
          item.label }}
 | 
			
		||||
      </el-dropdown-item>
 | 
			
		||||
    </el-dropdown-menu>
 | 
			
		||||
  </el-dropdown>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
export default {
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      sizeOptions: [
 | 
			
		||||
        { label: 'Default', value: 'default' },
 | 
			
		||||
        { label: 'Medium', value: 'medium' },
 | 
			
		||||
        { label: 'Small', value: 'small' },
 | 
			
		||||
        { label: 'Mini', value: 'mini' }
 | 
			
		||||
      ]
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    size() {
 | 
			
		||||
      return this.$store.getters.size
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    handleSetSize(size) {
 | 
			
		||||
      this.$ELEMENT.size = size
 | 
			
		||||
      this.$store.dispatch('app/setSize', size)
 | 
			
		||||
      this.refreshView()
 | 
			
		||||
      this.$message({
 | 
			
		||||
        message: 'Switch Size Success',
 | 
			
		||||
        type: 'success'
 | 
			
		||||
      })
 | 
			
		||||
    },
 | 
			
		||||
    refreshView() {
 | 
			
		||||
      // In order to make the cached page re-rendered
 | 
			
		||||
      this.$store.dispatch('tagsView/delAllCachedViews', this.$route)
 | 
			
		||||
 | 
			
		||||
      const { fullPath } = this.$route
 | 
			
		||||
 | 
			
		||||
      this.$nextTick(() => {
 | 
			
		||||
        this.$router.replace({
 | 
			
		||||
          path: '/redirect' + fullPath
 | 
			
		||||
        })
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
							
								
								
									
										91
									
								
								src/components/Sticky/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,91 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div :style="{height:height+'px',zIndex:zIndex}">
 | 
			
		||||
    <div
 | 
			
		||||
      :class="className"
 | 
			
		||||
      :style="{top:(isSticky ? stickyTop +'px' : ''),zIndex:zIndex,position:position,width:width,height:height+'px'}"
 | 
			
		||||
    >
 | 
			
		||||
      <slot>
 | 
			
		||||
        <div>sticky</div>
 | 
			
		||||
      </slot>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'Sticky',
 | 
			
		||||
  props: {
 | 
			
		||||
    stickyTop: {
 | 
			
		||||
      type: Number,
 | 
			
		||||
      default: 0
 | 
			
		||||
    },
 | 
			
		||||
    zIndex: {
 | 
			
		||||
      type: Number,
 | 
			
		||||
      default: 1
 | 
			
		||||
    },
 | 
			
		||||
    className: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      default: ''
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      active: false,
 | 
			
		||||
      position: '',
 | 
			
		||||
      width: undefined,
 | 
			
		||||
      height: undefined,
 | 
			
		||||
      isSticky: false
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  mounted() {
 | 
			
		||||
    this.height = this.$el.getBoundingClientRect().height
 | 
			
		||||
    window.addEventListener('scroll', this.handleScroll)
 | 
			
		||||
    window.addEventListener('resize', this.handleResize)
 | 
			
		||||
  },
 | 
			
		||||
  activated() {
 | 
			
		||||
    this.handleScroll()
 | 
			
		||||
  },
 | 
			
		||||
  destroyed() {
 | 
			
		||||
    window.removeEventListener('scroll', this.handleScroll)
 | 
			
		||||
    window.removeEventListener('resize', this.handleResize)
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    sticky() {
 | 
			
		||||
      if (this.active) {
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
      this.position = 'fixed'
 | 
			
		||||
      this.active = true
 | 
			
		||||
      this.width = this.width + 'px'
 | 
			
		||||
      this.isSticky = true
 | 
			
		||||
    },
 | 
			
		||||
    handleReset() {
 | 
			
		||||
      if (!this.active) {
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
      this.reset()
 | 
			
		||||
    },
 | 
			
		||||
    reset() {
 | 
			
		||||
      this.position = ''
 | 
			
		||||
      this.width = 'auto'
 | 
			
		||||
      this.active = false
 | 
			
		||||
      this.isSticky = false
 | 
			
		||||
    },
 | 
			
		||||
    handleScroll() {
 | 
			
		||||
      const width = this.$el.getBoundingClientRect().width
 | 
			
		||||
      this.width = width || 'auto'
 | 
			
		||||
      const offsetTop = this.$el.getBoundingClientRect().top
 | 
			
		||||
      if (offsetTop < this.stickyTop) {
 | 
			
		||||
        this.sticky()
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
      this.handleReset()
 | 
			
		||||
    },
 | 
			
		||||
    handleResize() {
 | 
			
		||||
      if (this.isSticky) {
 | 
			
		||||
        this.width = this.$el.getBoundingClientRect().width + 'px'
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
							
								
								
									
										62
									
								
								src/components/SvgIcon/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,62 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div v-if="isExternal" :style="styleExternalIcon" class="svg-external-icon svg-icon" v-on="$listeners" />
 | 
			
		||||
  <svg v-else :class="svgClass" aria-hidden="true" v-on="$listeners">
 | 
			
		||||
    <use :href="iconName" />
 | 
			
		||||
  </svg>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
// doc: https://panjiachen.github.io/vue-element-admin-site/feature/component/svg-icon.html#usage
 | 
			
		||||
import { isExternal } from '@/utils/validate'
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'SvgIcon',
 | 
			
		||||
  props: {
 | 
			
		||||
    iconClass: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      required: true
 | 
			
		||||
    },
 | 
			
		||||
    className: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      default: ''
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    isExternal() {
 | 
			
		||||
      return isExternal(this.iconClass)
 | 
			
		||||
    },
 | 
			
		||||
    iconName() {
 | 
			
		||||
      return `#icon-${this.iconClass}`
 | 
			
		||||
    },
 | 
			
		||||
    svgClass() {
 | 
			
		||||
      if (this.className) {
 | 
			
		||||
        return 'svg-icon ' + this.className
 | 
			
		||||
      } else {
 | 
			
		||||
        return 'svg-icon'
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    styleExternalIcon() {
 | 
			
		||||
      return {
 | 
			
		||||
        mask: `url(${this.iconClass}) no-repeat 50% 50%`,
 | 
			
		||||
        '-webkit-mask': `url(${this.iconClass}) no-repeat 50% 50%`
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
.svg-icon {
 | 
			
		||||
  width: 1em;
 | 
			
		||||
  height: 1em;
 | 
			
		||||
  vertical-align: -0.15em;
 | 
			
		||||
  fill: currentColor;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.svg-external-icon {
 | 
			
		||||
  background-color: currentColor;
 | 
			
		||||
  mask-size: cover!important;
 | 
			
		||||
  display: inline-block;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										113
									
								
								src/components/TextHoverEffect/Mallki.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,113 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <a :class="className" class="link--mallki" href="#">
 | 
			
		||||
    {{ text }}
 | 
			
		||||
    <span :data-letters="text" />
 | 
			
		||||
    <span :data-letters="text" />
 | 
			
		||||
  </a>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
export default {
 | 
			
		||||
  props: {
 | 
			
		||||
    className: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      default: ''
 | 
			
		||||
    },
 | 
			
		||||
    text: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      default: 'vue-element-admin'
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
/* Mallki */
 | 
			
		||||
 | 
			
		||||
.link--mallki {
 | 
			
		||||
  font-weight: 800;
 | 
			
		||||
  color: #4dd9d5;
 | 
			
		||||
  font-family: 'Dosis', sans-serif;
 | 
			
		||||
  -webkit-transition: color 0.5s 0.25s;
 | 
			
		||||
  transition: color 0.5s 0.25s;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  position: relative;
 | 
			
		||||
  display: inline-block;
 | 
			
		||||
  line-height: 1;
 | 
			
		||||
  outline: none;
 | 
			
		||||
  text-decoration: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.link--mallki:hover {
 | 
			
		||||
  -webkit-transition: none;
 | 
			
		||||
  transition: none;
 | 
			
		||||
  color: transparent;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.link--mallki::before {
 | 
			
		||||
  content: '';
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 6px;
 | 
			
		||||
  margin: -3px 0 0 0;
 | 
			
		||||
  background: #3888fa;
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  left: 0;
 | 
			
		||||
  top: 50%;
 | 
			
		||||
  -webkit-transform: translate3d(-100%, 0, 0);
 | 
			
		||||
  transform: translate3d(-100%, 0, 0);
 | 
			
		||||
  -webkit-transition: -webkit-transform 0.4s;
 | 
			
		||||
  transition: transform 0.4s;
 | 
			
		||||
  -webkit-transition-timing-function: cubic-bezier(0.7, 0, 0.3, 1);
 | 
			
		||||
  transition-timing-function: cubic-bezier(0.7, 0, 0.3, 1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.link--mallki:hover::before {
 | 
			
		||||
  -webkit-transform: translate3d(100%, 0, 0);
 | 
			
		||||
  transform: translate3d(100%, 0, 0);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.link--mallki span {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  height: 50%;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  left: 0;
 | 
			
		||||
  top: 0;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.link--mallki span::before {
 | 
			
		||||
  content: attr(data-letters);
 | 
			
		||||
  color: red;
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  left: 0;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  color: #3888fa;
 | 
			
		||||
  -webkit-transition: -webkit-transform 0.5s;
 | 
			
		||||
  transition: transform 0.5s;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.link--mallki span:nth-child(2) {
 | 
			
		||||
  top: 50%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.link--mallki span:first-child::before {
 | 
			
		||||
  top: 0;
 | 
			
		||||
  -webkit-transform: translate3d(0, 100%, 0);
 | 
			
		||||
  transform: translate3d(0, 100%, 0);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.link--mallki span:nth-child(2)::before {
 | 
			
		||||
  bottom: 0;
 | 
			
		||||
  -webkit-transform: translate3d(0, -100%, 0);
 | 
			
		||||
  transform: translate3d(0, -100%, 0);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.link--mallki:hover span::before {
 | 
			
		||||
  -webkit-transition-delay: 0.3s;
 | 
			
		||||
  transition-delay: 0.3s;
 | 
			
		||||
  -webkit-transform: translate3d(0, 0, 0);
 | 
			
		||||
  transform: translate3d(0, 0, 0);
 | 
			
		||||
  -webkit-transition-timing-function: cubic-bezier(0.2, 1, 0.3, 1);
 | 
			
		||||
  transition-timing-function: cubic-bezier(0.2, 1, 0.3, 1);
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										175
									
								
								src/components/ThemePicker/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,175 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <el-color-picker
 | 
			
		||||
    v-model="theme"
 | 
			
		||||
    :predefine="['#409EFF', '#1890ff', '#304156','#212121','#11a983', '#13c2c2', '#6959CD', '#f5222d', ]"
 | 
			
		||||
    class="theme-picker"
 | 
			
		||||
    popper-class="theme-picker-dropdown"
 | 
			
		||||
  />
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
const version = require('element-ui/package.json').version // element-ui version from node_modules
 | 
			
		||||
const ORIGINAL_THEME = '#409EFF' // default color
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      chalk: '', // content of theme-chalk css
 | 
			
		||||
      theme: ''
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    defaultTheme() {
 | 
			
		||||
      return this.$store.state.settings.theme
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  watch: {
 | 
			
		||||
    defaultTheme: {
 | 
			
		||||
      handler: function(val, oldVal) {
 | 
			
		||||
        this.theme = val
 | 
			
		||||
      },
 | 
			
		||||
      immediate: true
 | 
			
		||||
    },
 | 
			
		||||
    async theme(val) {
 | 
			
		||||
      const oldVal = this.chalk ? this.theme : ORIGINAL_THEME
 | 
			
		||||
      if (typeof val !== 'string') return
 | 
			
		||||
      const themeCluster = this.getThemeCluster(val.replace('#', ''))
 | 
			
		||||
      const originalCluster = this.getThemeCluster(oldVal.replace('#', ''))
 | 
			
		||||
      console.log(themeCluster, originalCluster)
 | 
			
		||||
 | 
			
		||||
      const $message = this.$message({
 | 
			
		||||
        message: '  Compiling the theme',
 | 
			
		||||
        customClass: 'theme-message',
 | 
			
		||||
        type: 'success',
 | 
			
		||||
        duration: 0,
 | 
			
		||||
        iconClass: 'el-icon-loading'
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      const getHandler = (variable, id) => {
 | 
			
		||||
        return () => {
 | 
			
		||||
          const originalCluster = this.getThemeCluster(ORIGINAL_THEME.replace('#', ''))
 | 
			
		||||
          const newStyle = this.updateStyle(this[variable], originalCluster, themeCluster)
 | 
			
		||||
 | 
			
		||||
          let styleTag = document.getElementById(id)
 | 
			
		||||
          if (!styleTag) {
 | 
			
		||||
            styleTag = document.createElement('style')
 | 
			
		||||
            styleTag.setAttribute('id', id)
 | 
			
		||||
            document.head.appendChild(styleTag)
 | 
			
		||||
          }
 | 
			
		||||
          styleTag.innerText = newStyle
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (!this.chalk) {
 | 
			
		||||
        const url = `https://unpkg.com/element-ui@${version}/lib/theme-chalk/index.css`
 | 
			
		||||
        await this.getCSSString(url, 'chalk')
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const chalkHandler = getHandler('chalk', 'chalk-style')
 | 
			
		||||
 | 
			
		||||
      chalkHandler()
 | 
			
		||||
 | 
			
		||||
      const styles = [].slice.call(document.querySelectorAll('style'))
 | 
			
		||||
        .filter(style => {
 | 
			
		||||
          const text = style.innerText
 | 
			
		||||
          return new RegExp(oldVal, 'i').test(text) && !/Chalk Variables/.test(text)
 | 
			
		||||
        })
 | 
			
		||||
      styles.forEach(style => {
 | 
			
		||||
        const { innerText } = style
 | 
			
		||||
        if (typeof innerText !== 'string') return
 | 
			
		||||
        style.innerText = this.updateStyle(innerText, originalCluster, themeCluster)
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      this.$emit('change', val)
 | 
			
		||||
 | 
			
		||||
      $message.close()
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  methods: {
 | 
			
		||||
    updateStyle(style, oldCluster, newCluster) {
 | 
			
		||||
      let newStyle = style
 | 
			
		||||
      oldCluster.forEach((color, index) => {
 | 
			
		||||
        newStyle = newStyle.replace(new RegExp(color, 'ig'), newCluster[index])
 | 
			
		||||
      })
 | 
			
		||||
      return newStyle
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    getCSSString(url, variable) {
 | 
			
		||||
      return new Promise(resolve => {
 | 
			
		||||
        const xhr = new XMLHttpRequest()
 | 
			
		||||
        xhr.onreadystatechange = () => {
 | 
			
		||||
          if (xhr.readyState === 4 && xhr.status === 200) {
 | 
			
		||||
            this[variable] = xhr.responseText.replace(/@font-face{[^}]+}/, '')
 | 
			
		||||
            resolve()
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        xhr.open('GET', url)
 | 
			
		||||
        xhr.send()
 | 
			
		||||
      })
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    getThemeCluster(theme) {
 | 
			
		||||
      const tintColor = (color, tint) => {
 | 
			
		||||
        let red = parseInt(color.slice(0, 2), 16)
 | 
			
		||||
        let green = parseInt(color.slice(2, 4), 16)
 | 
			
		||||
        let blue = parseInt(color.slice(4, 6), 16)
 | 
			
		||||
 | 
			
		||||
        if (tint === 0) { // when primary color is in its rgb space
 | 
			
		||||
          return [red, green, blue].join(',')
 | 
			
		||||
        } else {
 | 
			
		||||
          red += Math.round(tint * (255 - red))
 | 
			
		||||
          green += Math.round(tint * (255 - green))
 | 
			
		||||
          blue += Math.round(tint * (255 - blue))
 | 
			
		||||
 | 
			
		||||
          red = red.toString(16)
 | 
			
		||||
          green = green.toString(16)
 | 
			
		||||
          blue = blue.toString(16)
 | 
			
		||||
 | 
			
		||||
          return `#${red}${green}${blue}`
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const shadeColor = (color, shade) => {
 | 
			
		||||
        let red = parseInt(color.slice(0, 2), 16)
 | 
			
		||||
        let green = parseInt(color.slice(2, 4), 16)
 | 
			
		||||
        let blue = parseInt(color.slice(4, 6), 16)
 | 
			
		||||
 | 
			
		||||
        red = Math.round((1 - shade) * red)
 | 
			
		||||
        green = Math.round((1 - shade) * green)
 | 
			
		||||
        blue = Math.round((1 - shade) * blue)
 | 
			
		||||
 | 
			
		||||
        red = red.toString(16)
 | 
			
		||||
        green = green.toString(16)
 | 
			
		||||
        blue = blue.toString(16)
 | 
			
		||||
 | 
			
		||||
        return `#${red}${green}${blue}`
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const clusters = [theme]
 | 
			
		||||
      for (let i = 0; i <= 9; i++) {
 | 
			
		||||
        clusters.push(tintColor(theme, Number((i / 10).toFixed(2))))
 | 
			
		||||
      }
 | 
			
		||||
      clusters.push(shadeColor(theme, 0.1))
 | 
			
		||||
      return clusters
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
.theme-message,
 | 
			
		||||
.theme-picker-dropdown {
 | 
			
		||||
  z-index: 99999 !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.theme-picker .el-color-picker__trigger {
 | 
			
		||||
  height: 26px !important;
 | 
			
		||||
  width: 26px !important;
 | 
			
		||||
  padding: 2px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.theme-picker-dropdown .el-color-dropdown__link-btn {
 | 
			
		||||
  display: none;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										111
									
								
								src/components/Tinymce/components/EditorImage.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,111 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="upload-container">
 | 
			
		||||
    <el-button :style="{background:color,borderColor:color}" icon="el-icon-upload" size="mini" type="primary" @click=" dialogVisible=true">
 | 
			
		||||
      upload
 | 
			
		||||
    </el-button>
 | 
			
		||||
    <el-dialog :visible.sync="dialogVisible">
 | 
			
		||||
      <el-upload
 | 
			
		||||
        :multiple="true"
 | 
			
		||||
        :file-list="fileList"
 | 
			
		||||
        :show-file-list="true"
 | 
			
		||||
        :on-remove="handleRemove"
 | 
			
		||||
        :on-success="handleSuccess"
 | 
			
		||||
        :before-upload="beforeUpload"
 | 
			
		||||
        class="editor-slide-upload"
 | 
			
		||||
        action="https://httpbin.org/post"
 | 
			
		||||
        list-type="picture-card"
 | 
			
		||||
      >
 | 
			
		||||
        <el-button size="small" type="primary">
 | 
			
		||||
          Click upload
 | 
			
		||||
        </el-button>
 | 
			
		||||
      </el-upload>
 | 
			
		||||
      <el-button @click="dialogVisible = false">
 | 
			
		||||
        Cancel
 | 
			
		||||
      </el-button>
 | 
			
		||||
      <el-button type="primary" @click="handleSubmit">
 | 
			
		||||
        Confirm
 | 
			
		||||
      </el-button>
 | 
			
		||||
    </el-dialog>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
// import { getToken } from 'api/qiniu'
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'EditorSlideUpload',
 | 
			
		||||
  props: {
 | 
			
		||||
    color: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      default: '#1890ff'
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      dialogVisible: false,
 | 
			
		||||
      listObj: {},
 | 
			
		||||
      fileList: []
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    checkAllSuccess() {
 | 
			
		||||
      return Object.keys(this.listObj).every(item => this.listObj[item].hasSuccess)
 | 
			
		||||
    },
 | 
			
		||||
    handleSubmit() {
 | 
			
		||||
      const arr = Object.keys(this.listObj).map(v => this.listObj[v])
 | 
			
		||||
      if (!this.checkAllSuccess()) {
 | 
			
		||||
        this.$message('Please wait for all images to be uploaded successfully. If there is a network problem, please refresh the page and upload again!')
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
      this.$emit('successCBK', arr)
 | 
			
		||||
      this.listObj = {}
 | 
			
		||||
      this.fileList = []
 | 
			
		||||
      this.dialogVisible = false
 | 
			
		||||
    },
 | 
			
		||||
    handleSuccess(response, file) {
 | 
			
		||||
      const uid = file.uid
 | 
			
		||||
      const objKeyArr = Object.keys(this.listObj)
 | 
			
		||||
      for (let i = 0, len = objKeyArr.length; i < len; i++) {
 | 
			
		||||
        if (this.listObj[objKeyArr[i]].uid === uid) {
 | 
			
		||||
          this.listObj[objKeyArr[i]].url = response.files.file
 | 
			
		||||
          this.listObj[objKeyArr[i]].hasSuccess = true
 | 
			
		||||
          return
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    handleRemove(file) {
 | 
			
		||||
      const uid = file.uid
 | 
			
		||||
      const objKeyArr = Object.keys(this.listObj)
 | 
			
		||||
      for (let i = 0, len = objKeyArr.length; i < len; i++) {
 | 
			
		||||
        if (this.listObj[objKeyArr[i]].uid === uid) {
 | 
			
		||||
          delete this.listObj[objKeyArr[i]]
 | 
			
		||||
          return
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    beforeUpload(file) {
 | 
			
		||||
      const _self = this
 | 
			
		||||
      const _URL = window.URL || window.webkitURL
 | 
			
		||||
      const fileName = file.uid
 | 
			
		||||
      this.listObj[fileName] = {}
 | 
			
		||||
      return new Promise((resolve, reject) => {
 | 
			
		||||
        const img = new Image()
 | 
			
		||||
        img.src = _URL.createObjectURL(file)
 | 
			
		||||
        img.onload = function() {
 | 
			
		||||
          _self.listObj[fileName] = { hasSuccess: false, uid: file.uid, width: this.width, height: this.height }
 | 
			
		||||
        }
 | 
			
		||||
        resolve(true)
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.editor-slide-upload {
 | 
			
		||||
  margin-bottom: 20px;
 | 
			
		||||
  /deep/ .el-upload--picture-card {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										59
									
								
								src/components/Tinymce/dynamicLoadScript.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,59 @@
 | 
			
		||||
let callbacks = []
 | 
			
		||||
 | 
			
		||||
function loadedTinymce() {
 | 
			
		||||
  // to fixed https://github.com/PanJiaChen/vue-element-admin/issues/2144
 | 
			
		||||
  // check is successfully downloaded script
 | 
			
		||||
  return window.tinymce
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const dynamicLoadScript = (src, callback) => {
 | 
			
		||||
  const existingScript = document.getElementById(src)
 | 
			
		||||
  const cb = callback || function() {}
 | 
			
		||||
 | 
			
		||||
  if (!existingScript) {
 | 
			
		||||
    const script = document.createElement('script')
 | 
			
		||||
    script.src = src // src url for the third-party library being loaded.
 | 
			
		||||
    script.id = src
 | 
			
		||||
    document.body.appendChild(script)
 | 
			
		||||
    callbacks.push(cb)
 | 
			
		||||
    const onEnd = 'onload' in script ? stdOnEnd : ieOnEnd
 | 
			
		||||
    onEnd(script)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (existingScript && cb) {
 | 
			
		||||
    if (loadedTinymce()) {
 | 
			
		||||
      cb(null, existingScript)
 | 
			
		||||
    } else {
 | 
			
		||||
      callbacks.push(cb)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function stdOnEnd(script) {
 | 
			
		||||
    script.onload = function() {
 | 
			
		||||
      // this.onload = null here is necessary
 | 
			
		||||
      // because even IE9 works not like others
 | 
			
		||||
      this.onerror = this.onload = null
 | 
			
		||||
      for (const cb of callbacks) {
 | 
			
		||||
        cb(null, script)
 | 
			
		||||
      }
 | 
			
		||||
      callbacks = null
 | 
			
		||||
    }
 | 
			
		||||
    script.onerror = function() {
 | 
			
		||||
      this.onerror = this.onload = null
 | 
			
		||||
      cb(new Error('Failed to load ' + src), script)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function ieOnEnd(script) {
 | 
			
		||||
    script.onreadystatechange = function() {
 | 
			
		||||
      if (this.readyState !== 'complete' && this.readyState !== 'loaded') return
 | 
			
		||||
      this.onreadystatechange = null
 | 
			
		||||
      for (const cb of callbacks) {
 | 
			
		||||
        cb(null, script) // there is no way to catch loading errors in IE8
 | 
			
		||||
      }
 | 
			
		||||
      callbacks = null
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default dynamicLoadScript
 | 
			
		||||
							
								
								
									
										237
									
								
								src/components/Tinymce/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,237 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div :class="{fullscreen:fullscreen}" class="tinymce-container" :style="{width:containerWidth}">
 | 
			
		||||
    <textarea :id="tinymceId" class="tinymce-textarea" />
 | 
			
		||||
    <div class="editor-custom-btn-container">
 | 
			
		||||
      <editorImage color="#1890ff" class="editor-upload-btn" @successCBK="imageSuccessCBK" />
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
/**
 | 
			
		||||
 * docs:
 | 
			
		||||
 * https://panjiachen.github.io/vue-element-admin-site/feature/component/rich-editor.html#tinymce
 | 
			
		||||
 */
 | 
			
		||||
import editorImage from './components/EditorImage'
 | 
			
		||||
import plugins from './plugins'
 | 
			
		||||
import toolbar from './toolbar'
 | 
			
		||||
import load from './dynamicLoadScript'
 | 
			
		||||
 | 
			
		||||
// why use this cdn, detail see https://github.com/PanJiaChen/tinymce-all-in-one
 | 
			
		||||
const tinymceCDN = 'https://cdn.jsdelivr.net/npm/tinymce-all-in-one@4.9.3/tinymce.min.js'
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'Tinymce',
 | 
			
		||||
  components: { editorImage },
 | 
			
		||||
  props: {
 | 
			
		||||
    id: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      default: function() {
 | 
			
		||||
        return 'vue-tinymce-' + +new Date() + ((Math.random() * 1000).toFixed(0) + '')
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    value: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      default: ''
 | 
			
		||||
    },
 | 
			
		||||
    toolbar: {
 | 
			
		||||
      type: Array,
 | 
			
		||||
      required: false,
 | 
			
		||||
      default() {
 | 
			
		||||
        return []
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    menubar: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      default: 'file edit insert view format table'
 | 
			
		||||
    },
 | 
			
		||||
    height: {
 | 
			
		||||
      type: [Number, String],
 | 
			
		||||
      required: false,
 | 
			
		||||
      default: 360
 | 
			
		||||
    },
 | 
			
		||||
    width: {
 | 
			
		||||
      type: [Number, String],
 | 
			
		||||
      required: false,
 | 
			
		||||
      default: 'auto'
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      hasChange: false,
 | 
			
		||||
      hasInit: false,
 | 
			
		||||
      tinymceId: this.id,
 | 
			
		||||
      fullscreen: false,
 | 
			
		||||
      languageTypeList: {
 | 
			
		||||
        'en': 'en',
 | 
			
		||||
        'zh': 'zh_CN',
 | 
			
		||||
        'es': 'es_MX',
 | 
			
		||||
        'ja': 'ja'
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    containerWidth() {
 | 
			
		||||
      const width = this.width
 | 
			
		||||
      if (/^[\d]+(\.[\d]+)?$/.test(width)) { // matches `100`, `'100'`
 | 
			
		||||
        return `${width}px`
 | 
			
		||||
      }
 | 
			
		||||
      return width
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  watch: {
 | 
			
		||||
    value(val) {
 | 
			
		||||
      if (!this.hasChange && this.hasInit) {
 | 
			
		||||
        this.$nextTick(() =>
 | 
			
		||||
          window.tinymce.get(this.tinymceId).setContent(val || ''))
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  mounted() {
 | 
			
		||||
    this.init()
 | 
			
		||||
  },
 | 
			
		||||
  activated() {
 | 
			
		||||
    if (window.tinymce) {
 | 
			
		||||
      this.initTinymce()
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  deactivated() {
 | 
			
		||||
    this.destroyTinymce()
 | 
			
		||||
  },
 | 
			
		||||
  destroyed() {
 | 
			
		||||
    this.destroyTinymce()
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    init() {
 | 
			
		||||
      // dynamic load tinymce from cdn
 | 
			
		||||
      load(tinymceCDN, (err) => {
 | 
			
		||||
        if (err) {
 | 
			
		||||
          this.$message.error(err.message)
 | 
			
		||||
          return
 | 
			
		||||
        }
 | 
			
		||||
        this.initTinymce()
 | 
			
		||||
      })
 | 
			
		||||
    },
 | 
			
		||||
    initTinymce() {
 | 
			
		||||
      const _this = this
 | 
			
		||||
      window.tinymce.init({
 | 
			
		||||
        selector: `#${this.tinymceId}`,
 | 
			
		||||
        language: this.languageTypeList['en'],
 | 
			
		||||
        height: this.height,
 | 
			
		||||
        body_class: 'panel-body ',
 | 
			
		||||
        object_resizing: false,
 | 
			
		||||
        toolbar: this.toolbar.length > 0 ? this.toolbar : toolbar,
 | 
			
		||||
        menubar: this.menubar,
 | 
			
		||||
        plugins: plugins,
 | 
			
		||||
        end_container_on_empty_block: true,
 | 
			
		||||
        powerpaste_word_import: 'clean',
 | 
			
		||||
        code_dialog_height: 450,
 | 
			
		||||
        code_dialog_width: 1000,
 | 
			
		||||
        advlist_bullet_styles: 'square',
 | 
			
		||||
        advlist_number_styles: 'default',
 | 
			
		||||
        imagetools_cors_hosts: ['www.tinymce.com', 'codepen.io'],
 | 
			
		||||
        default_link_target: '_blank',
 | 
			
		||||
        link_title: false,
 | 
			
		||||
        nonbreaking_force_tab: true, // inserting nonbreaking space   need Nonbreaking Space Plugin
 | 
			
		||||
        init_instance_callback: editor => {
 | 
			
		||||
          if (_this.value) {
 | 
			
		||||
            editor.setContent(_this.value)
 | 
			
		||||
          }
 | 
			
		||||
          _this.hasInit = true
 | 
			
		||||
          editor.on('NodeChange Change KeyUp SetContent', () => {
 | 
			
		||||
            this.hasChange = true
 | 
			
		||||
            this.$emit('input', editor.getContent())
 | 
			
		||||
          })
 | 
			
		||||
        },
 | 
			
		||||
        setup(editor) {
 | 
			
		||||
          editor.on('FullscreenStateChanged', (e) => {
 | 
			
		||||
            _this.fullscreen = e.state
 | 
			
		||||
          })
 | 
			
		||||
        }
 | 
			
		||||
        // 整合七牛上传
 | 
			
		||||
        // images_dataimg_filter(img) {
 | 
			
		||||
        //   setTimeout(() => {
 | 
			
		||||
        //     const $image = $(img);
 | 
			
		||||
        //     $image.removeAttr('width');
 | 
			
		||||
        //     $image.removeAttr('height');
 | 
			
		||||
        //     if ($image[0].height && $image[0].width) {
 | 
			
		||||
        //       $image.attr('data-wscntype', 'image');
 | 
			
		||||
        //       $image.attr('data-wscnh', $image[0].height);
 | 
			
		||||
        //       $image.attr('data-wscnw', $image[0].width);
 | 
			
		||||
        //       $image.addClass('wscnph');
 | 
			
		||||
        //     }
 | 
			
		||||
        //   }, 0);
 | 
			
		||||
        //   return img
 | 
			
		||||
        // },
 | 
			
		||||
        // images_upload_handler(blobInfo, success, failure, progress) {
 | 
			
		||||
        //   progress(0);
 | 
			
		||||
        //   const token = _this.$store.getters.token;
 | 
			
		||||
        //   getToken(token).then(response => {
 | 
			
		||||
        //     const url = response.data.qiniu_url;
 | 
			
		||||
        //     const formData = new FormData();
 | 
			
		||||
        //     formData.append('token', response.data.qiniu_token);
 | 
			
		||||
        //     formData.append('key', response.data.qiniu_key);
 | 
			
		||||
        //     formData.append('file', blobInfo.blob(), url);
 | 
			
		||||
        //     upload(formData).then(() => {
 | 
			
		||||
        //       success(url);
 | 
			
		||||
        //       progress(100);
 | 
			
		||||
        //     })
 | 
			
		||||
        //   }).catch(err => {
 | 
			
		||||
        //     failure('出现未知问题,刷新页面,或者联系程序员')
 | 
			
		||||
        //     console.log(err);
 | 
			
		||||
        //   });
 | 
			
		||||
        // },
 | 
			
		||||
      })
 | 
			
		||||
    },
 | 
			
		||||
    destroyTinymce() {
 | 
			
		||||
      const tinymce = window.tinymce.get(this.tinymceId)
 | 
			
		||||
      if (this.fullscreen) {
 | 
			
		||||
        tinymce.execCommand('mceFullScreen')
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (tinymce) {
 | 
			
		||||
        tinymce.destroy()
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    setContent(value) {
 | 
			
		||||
      window.tinymce.get(this.tinymceId).setContent(value)
 | 
			
		||||
    },
 | 
			
		||||
    getContent() {
 | 
			
		||||
      window.tinymce.get(this.tinymceId).getContent()
 | 
			
		||||
    },
 | 
			
		||||
    imageSuccessCBK(arr) {
 | 
			
		||||
      const _this = this
 | 
			
		||||
      arr.forEach(v => {
 | 
			
		||||
        window.tinymce.get(_this.tinymceId).insertContent(`<img class="wscnph" src="${v.url}" >`)
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
.tinymce-container {
 | 
			
		||||
  position: relative;
 | 
			
		||||
  line-height: normal;
 | 
			
		||||
}
 | 
			
		||||
.tinymce-container>>>.mce-fullscreen {
 | 
			
		||||
  z-index: 10000;
 | 
			
		||||
}
 | 
			
		||||
.tinymce-textarea {
 | 
			
		||||
  visibility: hidden;
 | 
			
		||||
  z-index: -1;
 | 
			
		||||
}
 | 
			
		||||
.editor-custom-btn-container {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  right: 4px;
 | 
			
		||||
  top: 4px;
 | 
			
		||||
  /*z-index: 2005;*/
 | 
			
		||||
}
 | 
			
		||||
.fullscreen .editor-custom-btn-container {
 | 
			
		||||
  z-index: 10000;
 | 
			
		||||
  position: fixed;
 | 
			
		||||
}
 | 
			
		||||
.editor-upload-btn {
 | 
			
		||||
  display: inline-block;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										7
									
								
								src/components/Tinymce/plugins.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,7 @@
 | 
			
		||||
// Any plugins you want to use has to be imported
 | 
			
		||||
// Detail plugins list see https://www.tinymce.com/docs/plugins/
 | 
			
		||||
// Custom builds see https://www.tinymce.com/download/custom-builds/
 | 
			
		||||
 | 
			
		||||
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']
 | 
			
		||||
 | 
			
		||||
export default plugins
 | 
			
		||||
							
								
								
									
										6
									
								
								src/components/Tinymce/toolbar.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,6 @@
 | 
			
		||||
// Here is a list of the toolbar
 | 
			
		||||
// Detail list see https://www.tinymce.com/docs/advanced/editor-control-identifiers/#toolbarcontrols
 | 
			
		||||
 | 
			
		||||
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']
 | 
			
		||||
 | 
			
		||||
export default toolbar
 | 
			
		||||
							
								
								
									
										134
									
								
								src/components/Upload/SingleImage.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,134 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="upload-container">
 | 
			
		||||
    <el-upload
 | 
			
		||||
      :data="dataObj"
 | 
			
		||||
      :multiple="false"
 | 
			
		||||
      :show-file-list="false"
 | 
			
		||||
      :on-success="handleImageSuccess"
 | 
			
		||||
      class="image-uploader"
 | 
			
		||||
      drag
 | 
			
		||||
      action="https://httpbin.org/post"
 | 
			
		||||
    >
 | 
			
		||||
      <i class="el-icon-upload" />
 | 
			
		||||
      <div class="el-upload__text">
 | 
			
		||||
        将文件拖到此处,或<em>点击上传</em>
 | 
			
		||||
      </div>
 | 
			
		||||
    </el-upload>
 | 
			
		||||
    <div class="image-preview">
 | 
			
		||||
      <div v-show="imageUrl.length>1" class="image-preview-wrapper">
 | 
			
		||||
        <img :src="imageUrl+'?imageView2/1/w/200/h/200'">
 | 
			
		||||
        <div class="image-preview-action">
 | 
			
		||||
          <i class="el-icon-delete" @click="rmImage" />
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import { getToken } from '@/api/qiniu'
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'SingleImageUpload',
 | 
			
		||||
  props: {
 | 
			
		||||
    value: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      default: ''
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      tempUrl: '',
 | 
			
		||||
      dataObj: { token: '', key: '' }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    imageUrl() {
 | 
			
		||||
      return this.value
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    rmImage() {
 | 
			
		||||
      this.emitInput('')
 | 
			
		||||
    },
 | 
			
		||||
    emitInput(val) {
 | 
			
		||||
      this.$emit('input', val)
 | 
			
		||||
    },
 | 
			
		||||
    handleImageSuccess() {
 | 
			
		||||
      this.emitInput(this.tempUrl)
 | 
			
		||||
    },
 | 
			
		||||
    beforeUpload() {
 | 
			
		||||
      const _self = this
 | 
			
		||||
      return new Promise((resolve, reject) => {
 | 
			
		||||
        getToken().then(response => {
 | 
			
		||||
          const key = response.data.qiniu_key
 | 
			
		||||
          const token = response.data.qiniu_token
 | 
			
		||||
          _self._data.dataObj.token = token
 | 
			
		||||
          _self._data.dataObj.key = key
 | 
			
		||||
          this.tempUrl = response.data.qiniu_url
 | 
			
		||||
          resolve(true)
 | 
			
		||||
        }).catch(err => {
 | 
			
		||||
          console.log(err)
 | 
			
		||||
          reject(false)
 | 
			
		||||
        })
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
    @import "~@/styles/mixin.scss";
 | 
			
		||||
    .upload-container {
 | 
			
		||||
        width: 100%;
 | 
			
		||||
        position: relative;
 | 
			
		||||
        @include clearfix;
 | 
			
		||||
        .image-uploader {
 | 
			
		||||
            width: 60%;
 | 
			
		||||
            float: left;
 | 
			
		||||
        }
 | 
			
		||||
        .image-preview {
 | 
			
		||||
            width: 200px;
 | 
			
		||||
            height: 200px;
 | 
			
		||||
            position: relative;
 | 
			
		||||
            border: 1px dashed #d9d9d9;
 | 
			
		||||
            float: left;
 | 
			
		||||
            margin-left: 50px;
 | 
			
		||||
            .image-preview-wrapper {
 | 
			
		||||
                position: relative;
 | 
			
		||||
                width: 100%;
 | 
			
		||||
                height: 100%;
 | 
			
		||||
                img {
 | 
			
		||||
                    width: 100%;
 | 
			
		||||
                    height: 100%;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            .image-preview-action {
 | 
			
		||||
                position: absolute;
 | 
			
		||||
                width: 100%;
 | 
			
		||||
                height: 100%;
 | 
			
		||||
                left: 0;
 | 
			
		||||
                top: 0;
 | 
			
		||||
                cursor: default;
 | 
			
		||||
                text-align: center;
 | 
			
		||||
                color: #fff;
 | 
			
		||||
                opacity: 0;
 | 
			
		||||
                font-size: 20px;
 | 
			
		||||
                background-color: rgba(0, 0, 0, .5);
 | 
			
		||||
                transition: opacity .3s;
 | 
			
		||||
                cursor: pointer;
 | 
			
		||||
                text-align: center;
 | 
			
		||||
                line-height: 200px;
 | 
			
		||||
                .el-icon-delete {
 | 
			
		||||
                    font-size: 36px;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            &:hover {
 | 
			
		||||
                .image-preview-action {
 | 
			
		||||
                    opacity: 1;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										130
									
								
								src/components/Upload/SingleImage2.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,130 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="singleImageUpload2 upload-container">
 | 
			
		||||
    <el-upload
 | 
			
		||||
      :data="dataObj"
 | 
			
		||||
      :multiple="false"
 | 
			
		||||
      :show-file-list="false"
 | 
			
		||||
      :on-success="handleImageSuccess"
 | 
			
		||||
      class="image-uploader"
 | 
			
		||||
      drag
 | 
			
		||||
      action="https://httpbin.org/post"
 | 
			
		||||
    >
 | 
			
		||||
      <i class="el-icon-upload" />
 | 
			
		||||
      <div class="el-upload__text">
 | 
			
		||||
        Drag或<em>点击上传</em>
 | 
			
		||||
      </div>
 | 
			
		||||
    </el-upload>
 | 
			
		||||
    <div v-show="imageUrl.length>0" class="image-preview">
 | 
			
		||||
      <div v-show="imageUrl.length>1" class="image-preview-wrapper">
 | 
			
		||||
        <img :src="imageUrl">
 | 
			
		||||
        <div class="image-preview-action">
 | 
			
		||||
          <i class="el-icon-delete" @click="rmImage" />
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import { getToken } from '@/api/qiniu'
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'SingleImageUpload2',
 | 
			
		||||
  props: {
 | 
			
		||||
    value: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      default: ''
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      tempUrl: '',
 | 
			
		||||
      dataObj: { token: '', key: '' }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    imageUrl() {
 | 
			
		||||
      return this.value
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    rmImage() {
 | 
			
		||||
      this.emitInput('')
 | 
			
		||||
    },
 | 
			
		||||
    emitInput(val) {
 | 
			
		||||
      this.$emit('input', val)
 | 
			
		||||
    },
 | 
			
		||||
    handleImageSuccess() {
 | 
			
		||||
      this.emitInput(this.tempUrl)
 | 
			
		||||
    },
 | 
			
		||||
    beforeUpload() {
 | 
			
		||||
      const _self = this
 | 
			
		||||
      return new Promise((resolve, reject) => {
 | 
			
		||||
        getToken().then(response => {
 | 
			
		||||
          const key = response.data.qiniu_key
 | 
			
		||||
          const token = response.data.qiniu_token
 | 
			
		||||
          _self._data.dataObj.token = token
 | 
			
		||||
          _self._data.dataObj.key = key
 | 
			
		||||
          this.tempUrl = response.data.qiniu_url
 | 
			
		||||
          resolve(true)
 | 
			
		||||
        }).catch(() => {
 | 
			
		||||
          reject(false)
 | 
			
		||||
        })
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.upload-container {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  position: relative;
 | 
			
		||||
  .image-uploader {
 | 
			
		||||
    height: 100%;
 | 
			
		||||
  }
 | 
			
		||||
  .image-preview {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    left: 0px;
 | 
			
		||||
    top: 0px;
 | 
			
		||||
    border: 1px dashed #d9d9d9;
 | 
			
		||||
    .image-preview-wrapper {
 | 
			
		||||
      position: relative;
 | 
			
		||||
      width: 100%;
 | 
			
		||||
      height: 100%;
 | 
			
		||||
      img {
 | 
			
		||||
        width: 100%;
 | 
			
		||||
        height: 100%;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    .image-preview-action {
 | 
			
		||||
      position: absolute;
 | 
			
		||||
      width: 100%;
 | 
			
		||||
      height: 100%;
 | 
			
		||||
      left: 0;
 | 
			
		||||
      top: 0;
 | 
			
		||||
      cursor: default;
 | 
			
		||||
      text-align: center;
 | 
			
		||||
      color: #fff;
 | 
			
		||||
      opacity: 0;
 | 
			
		||||
      font-size: 20px;
 | 
			
		||||
      background-color: rgba(0, 0, 0, .5);
 | 
			
		||||
      transition: opacity .3s;
 | 
			
		||||
      cursor: pointer;
 | 
			
		||||
      text-align: center;
 | 
			
		||||
      line-height: 200px;
 | 
			
		||||
      .el-icon-delete {
 | 
			
		||||
        font-size: 36px;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    &:hover {
 | 
			
		||||
      .image-preview-action {
 | 
			
		||||
        opacity: 1;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										157
									
								
								src/components/Upload/SingleImage3.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,157 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="upload-container">
 | 
			
		||||
    <el-upload
 | 
			
		||||
      :data="dataObj"
 | 
			
		||||
      :multiple="false"
 | 
			
		||||
      :show-file-list="false"
 | 
			
		||||
      :on-success="handleImageSuccess"
 | 
			
		||||
      class="image-uploader"
 | 
			
		||||
      drag
 | 
			
		||||
      action="https://httpbin.org/post"
 | 
			
		||||
    >
 | 
			
		||||
      <i class="el-icon-upload" />
 | 
			
		||||
      <div class="el-upload__text">
 | 
			
		||||
        将文件拖到此处,或<em>点击上传</em>
 | 
			
		||||
      </div>
 | 
			
		||||
    </el-upload>
 | 
			
		||||
    <div class="image-preview image-app-preview">
 | 
			
		||||
      <div v-show="imageUrl.length>1" class="image-preview-wrapper">
 | 
			
		||||
        <img :src="imageUrl">
 | 
			
		||||
        <div class="image-preview-action">
 | 
			
		||||
          <i class="el-icon-delete" @click="rmImage" />
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="image-preview">
 | 
			
		||||
      <div v-show="imageUrl.length>1" class="image-preview-wrapper">
 | 
			
		||||
        <img :src="imageUrl">
 | 
			
		||||
        <div class="image-preview-action">
 | 
			
		||||
          <i class="el-icon-delete" @click="rmImage" />
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import { getToken } from '@/api/qiniu'
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'SingleImageUpload3',
 | 
			
		||||
  props: {
 | 
			
		||||
    value: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      default: ''
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      tempUrl: '',
 | 
			
		||||
      dataObj: { token: '', key: '' }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    imageUrl() {
 | 
			
		||||
      return this.value
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    rmImage() {
 | 
			
		||||
      this.emitInput('')
 | 
			
		||||
    },
 | 
			
		||||
    emitInput(val) {
 | 
			
		||||
      this.$emit('input', val)
 | 
			
		||||
    },
 | 
			
		||||
    handleImageSuccess(file) {
 | 
			
		||||
      this.emitInput(file.files.file)
 | 
			
		||||
    },
 | 
			
		||||
    beforeUpload() {
 | 
			
		||||
      const _self = this
 | 
			
		||||
      return new Promise((resolve, reject) => {
 | 
			
		||||
        getToken().then(response => {
 | 
			
		||||
          const key = response.data.qiniu_key
 | 
			
		||||
          const token = response.data.qiniu_token
 | 
			
		||||
          _self._data.dataObj.token = token
 | 
			
		||||
          _self._data.dataObj.key = key
 | 
			
		||||
          this.tempUrl = response.data.qiniu_url
 | 
			
		||||
          resolve(true)
 | 
			
		||||
        }).catch(err => {
 | 
			
		||||
          console.log(err)
 | 
			
		||||
          reject(false)
 | 
			
		||||
        })
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
@import "~@/styles/mixin.scss";
 | 
			
		||||
.upload-container {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  position: relative;
 | 
			
		||||
  @include clearfix;
 | 
			
		||||
  .image-uploader {
 | 
			
		||||
    width: 35%;
 | 
			
		||||
    float: left;
 | 
			
		||||
  }
 | 
			
		||||
  .image-preview {
 | 
			
		||||
    width: 200px;
 | 
			
		||||
    height: 200px;
 | 
			
		||||
    position: relative;
 | 
			
		||||
    border: 1px dashed #d9d9d9;
 | 
			
		||||
    float: left;
 | 
			
		||||
    margin-left: 50px;
 | 
			
		||||
    .image-preview-wrapper {
 | 
			
		||||
      position: relative;
 | 
			
		||||
      width: 100%;
 | 
			
		||||
      height: 100%;
 | 
			
		||||
      img {
 | 
			
		||||
        width: 100%;
 | 
			
		||||
        height: 100%;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    .image-preview-action {
 | 
			
		||||
      position: absolute;
 | 
			
		||||
      width: 100%;
 | 
			
		||||
      height: 100%;
 | 
			
		||||
      left: 0;
 | 
			
		||||
      top: 0;
 | 
			
		||||
      cursor: default;
 | 
			
		||||
      text-align: center;
 | 
			
		||||
      color: #fff;
 | 
			
		||||
      opacity: 0;
 | 
			
		||||
      font-size: 20px;
 | 
			
		||||
      background-color: rgba(0, 0, 0, .5);
 | 
			
		||||
      transition: opacity .3s;
 | 
			
		||||
      cursor: pointer;
 | 
			
		||||
      text-align: center;
 | 
			
		||||
      line-height: 200px;
 | 
			
		||||
      .el-icon-delete {
 | 
			
		||||
        font-size: 36px;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    &:hover {
 | 
			
		||||
      .image-preview-action {
 | 
			
		||||
        opacity: 1;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  .image-app-preview {
 | 
			
		||||
    width: 320px;
 | 
			
		||||
    height: 180px;
 | 
			
		||||
    position: relative;
 | 
			
		||||
    border: 1px dashed #d9d9d9;
 | 
			
		||||
    float: left;
 | 
			
		||||
    margin-left: 50px;
 | 
			
		||||
    .app-fake-conver {
 | 
			
		||||
      height: 44px;
 | 
			
		||||
      position: absolute;
 | 
			
		||||
      width: 100%; // background: rgba(0, 0, 0, .1);
 | 
			
		||||
      text-align: center;
 | 
			
		||||
      line-height: 64px;
 | 
			
		||||
      color: #fff;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										138
									
								
								src/components/UploadExcel/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,138 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div>
 | 
			
		||||
    <input ref="excel-upload-input" class="excel-upload-input" type="file" accept=".xlsx, .xls" @change="handleClick">
 | 
			
		||||
    <div class="drop" @drop="handleDrop" @dragover="handleDragover" @dragenter="handleDragover">
 | 
			
		||||
      Drop excel file here or
 | 
			
		||||
      <el-button :loading="loading" style="margin-left:16px;" size="mini" type="primary" @click="handleUpload">
 | 
			
		||||
        Browse
 | 
			
		||||
      </el-button>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import XLSX from 'xlsx'
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  props: {
 | 
			
		||||
    beforeUpload: Function, // eslint-disable-line
 | 
			
		||||
    onSuccess: Function// eslint-disable-line
 | 
			
		||||
  },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      loading: false,
 | 
			
		||||
      excelData: {
 | 
			
		||||
        header: null,
 | 
			
		||||
        results: null
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    generateData({ header, results }) {
 | 
			
		||||
      this.excelData.header = header
 | 
			
		||||
      this.excelData.results = results
 | 
			
		||||
      this.onSuccess && this.onSuccess(this.excelData)
 | 
			
		||||
    },
 | 
			
		||||
    handleDrop(e) {
 | 
			
		||||
      e.stopPropagation()
 | 
			
		||||
      e.preventDefault()
 | 
			
		||||
      if (this.loading) return
 | 
			
		||||
      const files = e.dataTransfer.files
 | 
			
		||||
      if (files.length !== 1) {
 | 
			
		||||
        this.$message.error('Only support uploading one file!')
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
      const rawFile = files[0] // only use files[0]
 | 
			
		||||
 | 
			
		||||
      if (!this.isExcel(rawFile)) {
 | 
			
		||||
        this.$message.error('Only supports upload .xlsx, .xls, .csv suffix files')
 | 
			
		||||
        return false
 | 
			
		||||
      }
 | 
			
		||||
      this.upload(rawFile)
 | 
			
		||||
      e.stopPropagation()
 | 
			
		||||
      e.preventDefault()
 | 
			
		||||
    },
 | 
			
		||||
    handleDragover(e) {
 | 
			
		||||
      e.stopPropagation()
 | 
			
		||||
      e.preventDefault()
 | 
			
		||||
      e.dataTransfer.dropEffect = 'copy'
 | 
			
		||||
    },
 | 
			
		||||
    handleUpload() {
 | 
			
		||||
      this.$refs['excel-upload-input'].click()
 | 
			
		||||
    },
 | 
			
		||||
    handleClick(e) {
 | 
			
		||||
      const files = e.target.files
 | 
			
		||||
      const rawFile = files[0] // only use files[0]
 | 
			
		||||
      if (!rawFile) return
 | 
			
		||||
      this.upload(rawFile)
 | 
			
		||||
    },
 | 
			
		||||
    upload(rawFile) {
 | 
			
		||||
      this.$refs['excel-upload-input'].value = null // fix can't select the same excel
 | 
			
		||||
 | 
			
		||||
      if (!this.beforeUpload) {
 | 
			
		||||
        this.readerData(rawFile)
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
      const before = this.beforeUpload(rawFile)
 | 
			
		||||
      if (before) {
 | 
			
		||||
        this.readerData(rawFile)
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    readerData(rawFile) {
 | 
			
		||||
      this.loading = true
 | 
			
		||||
      return new Promise((resolve, reject) => {
 | 
			
		||||
        const reader = new FileReader()
 | 
			
		||||
        reader.onload = e => {
 | 
			
		||||
          const data = e.target.result
 | 
			
		||||
          const workbook = XLSX.read(data, { type: 'array' })
 | 
			
		||||
          const firstSheetName = workbook.SheetNames[0]
 | 
			
		||||
          const worksheet = workbook.Sheets[firstSheetName]
 | 
			
		||||
          const header = this.getHeaderRow(worksheet)
 | 
			
		||||
          const results = XLSX.utils.sheet_to_json(worksheet)
 | 
			
		||||
          this.generateData({ header, results })
 | 
			
		||||
          this.loading = false
 | 
			
		||||
          resolve()
 | 
			
		||||
        }
 | 
			
		||||
        reader.readAsArrayBuffer(rawFile)
 | 
			
		||||
      })
 | 
			
		||||
    },
 | 
			
		||||
    getHeaderRow(sheet) {
 | 
			
		||||
      const headers = []
 | 
			
		||||
      const range = XLSX.utils.decode_range(sheet['!ref'])
 | 
			
		||||
      let C
 | 
			
		||||
      const R = range.s.r
 | 
			
		||||
      /* start in the first row */
 | 
			
		||||
      for (C = range.s.c; C <= range.e.c; ++C) { /* walk every column in the range */
 | 
			
		||||
        const cell = sheet[XLSX.utils.encode_cell({ c: C, r: R })]
 | 
			
		||||
        /* find the cell in the first row */
 | 
			
		||||
        let hdr = 'UNKNOWN ' + C // <-- replace with your desired default
 | 
			
		||||
        if (cell && cell.t) hdr = XLSX.utils.format_cell(cell)
 | 
			
		||||
        headers.push(hdr)
 | 
			
		||||
      }
 | 
			
		||||
      return headers
 | 
			
		||||
    },
 | 
			
		||||
    isExcel(file) {
 | 
			
		||||
      return /\.(xlsx|xls|csv)$/.test(file.name)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
.excel-upload-input{
 | 
			
		||||
  display: none;
 | 
			
		||||
  z-index: -9999;
 | 
			
		||||
}
 | 
			
		||||
.drop{
 | 
			
		||||
  border: 2px dashed #bbb;
 | 
			
		||||
  width: 600px;
 | 
			
		||||
  height: 160px;
 | 
			
		||||
  line-height: 160px;
 | 
			
		||||
  margin: 0 auto;
 | 
			
		||||
  font-size: 24px;
 | 
			
		||||
  border-radius: 5px;
 | 
			
		||||
  text-align: center;
 | 
			
		||||
  color: #bbb;
 | 
			
		||||
  position: relative;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										49
									
								
								src/directive/clipboard/clipboard.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,49 @@
 | 
			
		||||
// Inspired by https://github.com/Inndy/vue-clipboard2
 | 
			
		||||
const Clipboard = require('clipboard')
 | 
			
		||||
if (!Clipboard) {
 | 
			
		||||
  throw new Error('you should npm install `clipboard` --save at first ')
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  bind(el, binding) {
 | 
			
		||||
    if (binding.arg === 'success') {
 | 
			
		||||
      el._v_clipboard_success = binding.value
 | 
			
		||||
    } else if (binding.arg === 'error') {
 | 
			
		||||
      el._v_clipboard_error = binding.value
 | 
			
		||||
    } else {
 | 
			
		||||
      const clipboard = new Clipboard(el, {
 | 
			
		||||
        text() { return binding.value },
 | 
			
		||||
        action() { return binding.arg === 'cut' ? 'cut' : 'copy' }
 | 
			
		||||
      })
 | 
			
		||||
      clipboard.on('success', e => {
 | 
			
		||||
        const callback = el._v_clipboard_success
 | 
			
		||||
        callback && callback(e) // eslint-disable-line
 | 
			
		||||
      })
 | 
			
		||||
      clipboard.on('error', e => {
 | 
			
		||||
        const callback = el._v_clipboard_error
 | 
			
		||||
        callback && callback(e) // eslint-disable-line
 | 
			
		||||
      })
 | 
			
		||||
      el._v_clipboard = clipboard
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  update(el, binding) {
 | 
			
		||||
    if (binding.arg === 'success') {
 | 
			
		||||
      el._v_clipboard_success = binding.value
 | 
			
		||||
    } else if (binding.arg === 'error') {
 | 
			
		||||
      el._v_clipboard_error = binding.value
 | 
			
		||||
    } else {
 | 
			
		||||
      el._v_clipboard.text = function() { return binding.value }
 | 
			
		||||
      el._v_clipboard.action = function() { return binding.arg === 'cut' ? 'cut' : 'copy' }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  unbind(el, binding) {
 | 
			
		||||
    if (binding.arg === 'success') {
 | 
			
		||||
      delete el._v_clipboard_success
 | 
			
		||||
    } else if (binding.arg === 'error') {
 | 
			
		||||
      delete el._v_clipboard_error
 | 
			
		||||
    } else {
 | 
			
		||||
      el._v_clipboard.destroy()
 | 
			
		||||
      delete el._v_clipboard
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										13
									
								
								src/directive/clipboard/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,13 @@
 | 
			
		||||
import Clipboard from './clipboard'
 | 
			
		||||
 | 
			
		||||
const install = function(Vue) {
 | 
			
		||||
  Vue.directive('Clipboard', Clipboard)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
if (window.Vue) {
 | 
			
		||||
  window.clipboard = Clipboard
 | 
			
		||||
  Vue.use(install); // eslint-disable-line
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Clipboard.install = install
 | 
			
		||||
export default Clipboard
 | 
			
		||||
							
								
								
									
										77
									
								
								src/directive/el-drag-dialog/drag.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,77 @@
 | 
			
		||||
export default {
 | 
			
		||||
  bind(el, binding, vnode) {
 | 
			
		||||
    const dialogHeaderEl = el.querySelector('.el-dialog__header')
 | 
			
		||||
    const dragDom = el.querySelector('.el-dialog')
 | 
			
		||||
    dialogHeaderEl.style.cssText += ';cursor:move;'
 | 
			
		||||
    dragDom.style.cssText += ';top:0px;'
 | 
			
		||||
 | 
			
		||||
    // 获取原有属性 ie dom元素.currentStyle 火狐谷歌 window.getComputedStyle(dom元素, null);
 | 
			
		||||
    const getStyle = (function() {
 | 
			
		||||
      if (window.document.currentStyle) {
 | 
			
		||||
        return (dom, attr) => dom.currentStyle[attr]
 | 
			
		||||
      } else {
 | 
			
		||||
        return (dom, attr) => getComputedStyle(dom, false)[attr]
 | 
			
		||||
      }
 | 
			
		||||
    })()
 | 
			
		||||
 | 
			
		||||
    dialogHeaderEl.onmousedown = (e) => {
 | 
			
		||||
      // 鼠标按下,计算当前元素距离可视区的距离
 | 
			
		||||
      const disX = e.clientX - dialogHeaderEl.offsetLeft
 | 
			
		||||
      const disY = e.clientY - dialogHeaderEl.offsetTop
 | 
			
		||||
 | 
			
		||||
      const dragDomWidth = dragDom.offsetWidth
 | 
			
		||||
      const dragDomHeight = dragDom.offsetHeight
 | 
			
		||||
 | 
			
		||||
      const screenWidth = document.body.clientWidth
 | 
			
		||||
      const screenHeight = document.body.clientHeight
 | 
			
		||||
 | 
			
		||||
      const minDragDomLeft = dragDom.offsetLeft
 | 
			
		||||
      const maxDragDomLeft = screenWidth - dragDom.offsetLeft - dragDomWidth
 | 
			
		||||
 | 
			
		||||
      const minDragDomTop = dragDom.offsetTop
 | 
			
		||||
      const maxDragDomTop = screenHeight - dragDom.offsetTop - dragDomHeight
 | 
			
		||||
 | 
			
		||||
      // 获取到的值带px 正则匹配替换
 | 
			
		||||
      let styL = getStyle(dragDom, 'left')
 | 
			
		||||
      let styT = getStyle(dragDom, 'top')
 | 
			
		||||
 | 
			
		||||
      if (styL.includes('%')) {
 | 
			
		||||
        styL = +document.body.clientWidth * (+styL.replace(/\%/g, '') / 100)
 | 
			
		||||
        styT = +document.body.clientHeight * (+styT.replace(/\%/g, '') / 100)
 | 
			
		||||
      } else {
 | 
			
		||||
        styL = +styL.replace(/\px/g, '')
 | 
			
		||||
        styT = +styT.replace(/\px/g, '')
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      document.onmousemove = function(e) {
 | 
			
		||||
        // 通过事件委托,计算移动的距离
 | 
			
		||||
        let left = e.clientX - disX
 | 
			
		||||
        let top = e.clientY - disY
 | 
			
		||||
 | 
			
		||||
        // 边界处理
 | 
			
		||||
        if (-(left) > minDragDomLeft) {
 | 
			
		||||
          left = -minDragDomLeft
 | 
			
		||||
        } else if (left > maxDragDomLeft) {
 | 
			
		||||
          left = maxDragDomLeft
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (-(top) > minDragDomTop) {
 | 
			
		||||
          top = -minDragDomTop
 | 
			
		||||
        } else if (top > maxDragDomTop) {
 | 
			
		||||
          top = maxDragDomTop
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 移动当前元素
 | 
			
		||||
        dragDom.style.cssText += `;left:${left + styL}px;top:${top + styT}px;`
 | 
			
		||||
 | 
			
		||||
        // emit onDrag event
 | 
			
		||||
        vnode.child.$emit('dragDialog')
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      document.onmouseup = function(e) {
 | 
			
		||||
        document.onmousemove = null
 | 
			
		||||
        document.onmouseup = null
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										13
									
								
								src/directive/el-drag-dialog/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,13 @@
 | 
			
		||||
import drag from './drag'
 | 
			
		||||
 | 
			
		||||
const install = function(Vue) {
 | 
			
		||||
  Vue.directive('el-drag-dialog', drag)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
if (window.Vue) {
 | 
			
		||||
  window['el-drag-dialog'] = drag
 | 
			
		||||
  Vue.use(install); // eslint-disable-line
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
drag.install = install
 | 
			
		||||
export default drag
 | 
			
		||||
							
								
								
									
										41
									
								
								src/directive/el-table/adaptive.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,41 @@
 | 
			
		||||
import { addResizeListener, removeResizeListener } from 'element-ui/src/utils/resize-event'
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * How to use
 | 
			
		||||
 * <el-table height="100px" v-el-height-adaptive-table="{bottomOffset: 30}">...</el-table>
 | 
			
		||||
 * el-table height is must be set
 | 
			
		||||
 * bottomOffset: 30(default)   // The height of the table from the bottom of the page.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
const doResize = (el, binding, vnode) => {
 | 
			
		||||
  const { componentInstance: $table } = vnode
 | 
			
		||||
 | 
			
		||||
  const { value } = binding
 | 
			
		||||
 | 
			
		||||
  if (!$table.height) {
 | 
			
		||||
    throw new Error(`el-$table must set the height. Such as height='100px'`)
 | 
			
		||||
  }
 | 
			
		||||
  const bottomOffset = (value && value.bottomOffset) || 30
 | 
			
		||||
 | 
			
		||||
  if (!$table) return
 | 
			
		||||
 | 
			
		||||
  const height = window.innerHeight - el.getBoundingClientRect().top - bottomOffset
 | 
			
		||||
  $table.layout.setHeight(height)
 | 
			
		||||
  $table.doLayout()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  bind(el, binding, vnode) {
 | 
			
		||||
    el.resizeListener = () => {
 | 
			
		||||
      doResize(el, binding, vnode)
 | 
			
		||||
    }
 | 
			
		||||
    // parameter 1 is must be "Element" type
 | 
			
		||||
    addResizeListener(window.document.body, el.resizeListener)
 | 
			
		||||
  },
 | 
			
		||||
  inserted(el, binding, vnode) {
 | 
			
		||||
    doResize(el, binding, vnode)
 | 
			
		||||
  },
 | 
			
		||||
  unbind(el) {
 | 
			
		||||
    removeResizeListener(window.document.body, el.resizeListener)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										13
									
								
								src/directive/el-table/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,13 @@
 | 
			
		||||
import adaptive from './adaptive'
 | 
			
		||||
 | 
			
		||||
const install = function(Vue) {
 | 
			
		||||
  Vue.directive('el-height-adaptive-table', adaptive)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
if (window.Vue) {
 | 
			
		||||
  window['el-height-adaptive-table'] = adaptive
 | 
			
		||||
  Vue.use(install); // eslint-disable-line
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
adaptive.install = install
 | 
			
		||||
export default adaptive
 | 
			
		||||
							
								
								
									
										13
									
								
								src/directive/permission/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,13 @@
 | 
			
		||||
import permission from './permission'
 | 
			
		||||
 | 
			
		||||
const install = function(Vue) {
 | 
			
		||||
  Vue.directive('permission', permission)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
if (window.Vue) {
 | 
			
		||||
  window['permission'] = permission
 | 
			
		||||
  Vue.use(install); // eslint-disable-line
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
permission.install = install
 | 
			
		||||
export default permission
 | 
			
		||||
							
								
								
									
										22
									
								
								src/directive/permission/permission.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,22 @@
 | 
			
		||||
import store from '@/store'
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  inserted(el, binding, vnode) {
 | 
			
		||||
    const { value } = binding
 | 
			
		||||
    const roles = store.getters && store.getters.roles
 | 
			
		||||
 | 
			
		||||
    if (value && value instanceof Array && value.length > 0) {
 | 
			
		||||
      const permissionRoles = value
 | 
			
		||||
 | 
			
		||||
      const hasPermission = roles.some(role => {
 | 
			
		||||
        return permissionRoles.includes(role)
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      if (!hasPermission) {
 | 
			
		||||
        el.parentNode && el.parentNode.removeChild(el)
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      throw new Error(`need roles! Like v-permission="['admin','editor']"`)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										91
									
								
								src/directive/sticky.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,91 @@
 | 
			
		||||
const vueSticky = {}
 | 
			
		||||
let listenAction
 | 
			
		||||
vueSticky.install = Vue => {
 | 
			
		||||
  Vue.directive('sticky', {
 | 
			
		||||
    inserted(el, binding) {
 | 
			
		||||
      const params = binding.value || {}
 | 
			
		||||
      const stickyTop = params.stickyTop || 0
 | 
			
		||||
      const zIndex = params.zIndex || 1000
 | 
			
		||||
      const elStyle = el.style
 | 
			
		||||
 | 
			
		||||
      elStyle.position = '-webkit-sticky'
 | 
			
		||||
      elStyle.position = 'sticky'
 | 
			
		||||
      // if the browser support css sticky(Currently Safari, Firefox and Chrome Canary)
 | 
			
		||||
      // if (~elStyle.position.indexOf('sticky')) {
 | 
			
		||||
      //     elStyle.top = `${stickyTop}px`;
 | 
			
		||||
      //     elStyle.zIndex = zIndex;
 | 
			
		||||
      //     return
 | 
			
		||||
      // }
 | 
			
		||||
      const elHeight = el.getBoundingClientRect().height
 | 
			
		||||
      const elWidth = el.getBoundingClientRect().width
 | 
			
		||||
      elStyle.cssText = `top: ${stickyTop}px; z-index: ${zIndex}`
 | 
			
		||||
 | 
			
		||||
      const parentElm = el.parentNode || document.documentElement
 | 
			
		||||
      const placeholder = document.createElement('div')
 | 
			
		||||
      placeholder.style.display = 'none'
 | 
			
		||||
      placeholder.style.width = `${elWidth}px`
 | 
			
		||||
      placeholder.style.height = `${elHeight}px`
 | 
			
		||||
      parentElm.insertBefore(placeholder, el)
 | 
			
		||||
 | 
			
		||||
      let active = false
 | 
			
		||||
 | 
			
		||||
      const getScroll = (target, top) => {
 | 
			
		||||
        const prop = top ? 'pageYOffset' : 'pageXOffset'
 | 
			
		||||
        const method = top ? 'scrollTop' : 'scrollLeft'
 | 
			
		||||
        let ret = target[prop]
 | 
			
		||||
        if (typeof ret !== 'number') {
 | 
			
		||||
          ret = window.document.documentElement[method]
 | 
			
		||||
        }
 | 
			
		||||
        return ret
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const sticky = () => {
 | 
			
		||||
        if (active) {
 | 
			
		||||
          return
 | 
			
		||||
        }
 | 
			
		||||
        if (!elStyle.height) {
 | 
			
		||||
          elStyle.height = `${el.offsetHeight}px`
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        elStyle.position = 'fixed'
 | 
			
		||||
        elStyle.width = `${elWidth}px`
 | 
			
		||||
        placeholder.style.display = 'inline-block'
 | 
			
		||||
        active = true
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const reset = () => {
 | 
			
		||||
        if (!active) {
 | 
			
		||||
          return
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        elStyle.position = ''
 | 
			
		||||
        placeholder.style.display = 'none'
 | 
			
		||||
        active = false
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const check = () => {
 | 
			
		||||
        const scrollTop = getScroll(window, true)
 | 
			
		||||
        const offsetTop = el.getBoundingClientRect().top
 | 
			
		||||
        if (offsetTop < stickyTop) {
 | 
			
		||||
          sticky()
 | 
			
		||||
        } else {
 | 
			
		||||
          if (scrollTop < elHeight + stickyTop) {
 | 
			
		||||
            reset()
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      listenAction = () => {
 | 
			
		||||
        check()
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      window.addEventListener('scroll', listenAction)
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    unbind() {
 | 
			
		||||
      window.removeEventListener('scroll', listenAction)
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default vueSticky
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										13
									
								
								src/directive/waves/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,13 @@
 | 
			
		||||
import waves from './waves'
 | 
			
		||||
 | 
			
		||||
const install = function(Vue) {
 | 
			
		||||
  Vue.directive('waves', waves)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
if (window.Vue) {
 | 
			
		||||
  window.waves = waves
 | 
			
		||||
  Vue.use(install); // eslint-disable-line
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
waves.install = install
 | 
			
		||||
export default waves
 | 
			
		||||
							
								
								
									
										26
									
								
								src/directive/waves/waves.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,26 @@
 | 
			
		||||
.waves-ripple {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    border-radius: 100%;
 | 
			
		||||
    background-color: rgba(0, 0, 0, 0.15);
 | 
			
		||||
    background-clip: padding-box;
 | 
			
		||||
    pointer-events: none;
 | 
			
		||||
    -webkit-user-select: none;
 | 
			
		||||
    -moz-user-select: none;
 | 
			
		||||
    -ms-user-select: none;
 | 
			
		||||
    user-select: none;
 | 
			
		||||
    -webkit-transform: scale(0);
 | 
			
		||||
    -ms-transform: scale(0);
 | 
			
		||||
    transform: scale(0);
 | 
			
		||||
    opacity: 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.waves-ripple.z-active {
 | 
			
		||||
    opacity: 0;
 | 
			
		||||
    -webkit-transform: scale(2);
 | 
			
		||||
    -ms-transform: scale(2);
 | 
			
		||||
    transform: scale(2);
 | 
			
		||||
    -webkit-transition: opacity 1.2s ease-out, -webkit-transform 0.6s ease-out;
 | 
			
		||||
    transition: opacity 1.2s ease-out, -webkit-transform 0.6s ease-out;
 | 
			
		||||
    transition: opacity 1.2s ease-out, transform 0.6s ease-out;
 | 
			
		||||
    transition: opacity 1.2s ease-out, transform 0.6s ease-out, -webkit-transform 0.6s ease-out;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										72
									
								
								src/directive/waves/waves.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,72 @@
 | 
			
		||||
import './waves.css'
 | 
			
		||||
 | 
			
		||||
const context = '@@wavesContext'
 | 
			
		||||
 | 
			
		||||
function handleClick(el, binding) {
 | 
			
		||||
  function handle(e) {
 | 
			
		||||
    const customOpts = Object.assign({}, binding.value)
 | 
			
		||||
    const opts = Object.assign({
 | 
			
		||||
      ele: el, // 波纹作用元素
 | 
			
		||||
      type: 'hit', // hit 点击位置扩散 center中心点扩展
 | 
			
		||||
      color: 'rgba(0, 0, 0, 0.15)' // 波纹颜色
 | 
			
		||||
    },
 | 
			
		||||
    customOpts
 | 
			
		||||
    )
 | 
			
		||||
    const target = opts.ele
 | 
			
		||||
    if (target) {
 | 
			
		||||
      target.style.position = 'relative'
 | 
			
		||||
      target.style.overflow = 'hidden'
 | 
			
		||||
      const rect = target.getBoundingClientRect()
 | 
			
		||||
      let ripple = target.querySelector('.waves-ripple')
 | 
			
		||||
      if (!ripple) {
 | 
			
		||||
        ripple = document.createElement('span')
 | 
			
		||||
        ripple.className = 'waves-ripple'
 | 
			
		||||
        ripple.style.height = ripple.style.width = Math.max(rect.width, rect.height) + 'px'
 | 
			
		||||
        target.appendChild(ripple)
 | 
			
		||||
      } else {
 | 
			
		||||
        ripple.className = 'waves-ripple'
 | 
			
		||||
      }
 | 
			
		||||
      switch (opts.type) {
 | 
			
		||||
        case 'center':
 | 
			
		||||
          ripple.style.top = rect.height / 2 - ripple.offsetHeight / 2 + 'px'
 | 
			
		||||
          ripple.style.left = rect.width / 2 - ripple.offsetWidth / 2 + 'px'
 | 
			
		||||
          break
 | 
			
		||||
        default:
 | 
			
		||||
          ripple.style.top =
 | 
			
		||||
            (e.pageY - rect.top - ripple.offsetHeight / 2 - document.documentElement.scrollTop ||
 | 
			
		||||
              document.body.scrollTop) + 'px'
 | 
			
		||||
          ripple.style.left =
 | 
			
		||||
            (e.pageX - rect.left - ripple.offsetWidth / 2 - document.documentElement.scrollLeft ||
 | 
			
		||||
              document.body.scrollLeft) + 'px'
 | 
			
		||||
      }
 | 
			
		||||
      ripple.style.backgroundColor = opts.color
 | 
			
		||||
      ripple.className = 'waves-ripple z-active'
 | 
			
		||||
      return false
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!el[context]) {
 | 
			
		||||
    el[context] = {
 | 
			
		||||
      removeHandle: handle
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    el[context].removeHandle = handle
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return handle
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  bind(el, binding) {
 | 
			
		||||
    el.addEventListener('click', handleClick(el, binding), false)
 | 
			
		||||
  },
 | 
			
		||||
  update(el, binding) {
 | 
			
		||||
    el.removeEventListener('click', el[context].removeHandle, false)
 | 
			
		||||
    el.addEventListener('click', handleClick(el, binding), false)
 | 
			
		||||
  },
 | 
			
		||||
  unbind(el) {
 | 
			
		||||
    el.removeEventListener('click', el[context].removeHandle, false)
 | 
			
		||||
    el[context] = null
 | 
			
		||||
    delete el[context]
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										68
									
								
								src/filters/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,68 @@
 | 
			
		||||
// import parseTime, formatTime and set to filter
 | 
			
		||||
export { parseTime, formatTime } from '@/utils'
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Show plural label if time is plural number
 | 
			
		||||
 * @param {number} time
 | 
			
		||||
 * @param {string} label
 | 
			
		||||
 * @return {string}
 | 
			
		||||
 */
 | 
			
		||||
function pluralize(time, label) {
 | 
			
		||||
  if (time === 1) {
 | 
			
		||||
    return time + label
 | 
			
		||||
  }
 | 
			
		||||
  return time + label + 's'
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {number} time
 | 
			
		||||
 */
 | 
			
		||||
export function timeAgo(time) {
 | 
			
		||||
  const between = Date.now() / 1000 - Number(time)
 | 
			
		||||
  if (between < 3600) {
 | 
			
		||||
    return pluralize(~~(between / 60), ' minute')
 | 
			
		||||
  } else if (between < 86400) {
 | 
			
		||||
    return pluralize(~~(between / 3600), ' hour')
 | 
			
		||||
  } else {
 | 
			
		||||
    return pluralize(~~(between / 86400), ' day')
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Number formatting
 | 
			
		||||
 * like 10000 => 10k
 | 
			
		||||
 * @param {number} num
 | 
			
		||||
 * @param {number} digits
 | 
			
		||||
 */
 | 
			
		||||
export function numberFormatter(num, digits) {
 | 
			
		||||
  const si = [
 | 
			
		||||
    { value: 1E18, symbol: 'E' },
 | 
			
		||||
    { value: 1E15, symbol: 'P' },
 | 
			
		||||
    { value: 1E12, symbol: 'T' },
 | 
			
		||||
    { value: 1E9, symbol: 'G' },
 | 
			
		||||
    { value: 1E6, symbol: 'M' },
 | 
			
		||||
    { value: 1E3, symbol: 'k' }
 | 
			
		||||
  ]
 | 
			
		||||
  for (let i = 0; i < si.length; i++) {
 | 
			
		||||
    if (num >= si[i].value) {
 | 
			
		||||
      return (num / si[i].value).toFixed(digits).replace(/\.0+$|(\.[0-9]*[1-9])0+$/, '$1') + si[i].symbol
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return num.toString()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 10000 => "10,000"
 | 
			
		||||
 * @param {number} num
 | 
			
		||||
 */
 | 
			
		||||
export function toThousandFilter(num) {
 | 
			
		||||
  return (+num || 0).toString().replace(/^-?\d+/g, m => m.replace(/(?=(?!\b)(\d{3})+$)/g, ','))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Upper case first char
 | 
			
		||||
 * @param {String} string
 | 
			
		||||
 */
 | 
			
		||||
export function uppercaseFirst(string) {
 | 
			
		||||
  return string.charAt(0).toUpperCase() + string.slice(1)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										9
									
								
								src/icons/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,9 @@
 | 
			
		||||
import Vue from 'vue'
 | 
			
		||||
import SvgIcon from '@/components/SvgIcon'// svg component
 | 
			
		||||
 | 
			
		||||
// register globally
 | 
			
		||||
Vue.component('svg-icon', SvgIcon)
 | 
			
		||||
 | 
			
		||||
const req = require.context('./svg', false, /\.svg$/)
 | 
			
		||||
const requireAll = requireContext => requireContext.keys().map(requireContext)
 | 
			
		||||
requireAll(req)
 | 
			
		||||
							
								
								
									
										1
									
								
								src/icons/svg/404.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1 @@
 | 
			
		||||
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M121.718 73.272v9.953c3.957-7.584 6.199-16.05 6.199-24.995C127.917 26.079 99.273 0 63.958 0 28.644 0 0 26.079 0 58.23c0 .403.028.806.028 1.21l22.97-25.953h13.34l-19.76 27.187h6.42V53.77l13.728-19.477v49.361H22.998V73.272H2.158c5.951 20.284 23.608 36.208 45.998 41.399-1.44 3.3-5.618 11.263-12.565 12.674-8.607 1.764 23.358.428 46.163-13.178 17.519-4.611 31.938-15.849 39.77-30.513h-13.506V73.272H85.02V59.464l22.998-25.977h13.008l-19.429 27.187h6.421v-7.433l13.727-19.402v39.433h-.027zm-78.24 2.822a10.516 10.516 0 0 1-.996-4.535V44.548c0-1.613.332-3.124.996-4.535a11.66 11.66 0 0 1 2.713-3.68c1.134-1.032 2.49-1.864 4.04-2.468 1.55-.605 3.21-.908 4.982-.908h11.292c1.77 0 3.431.303 4.981.908 1.522.604 2.85 1.41 3.986 2.418l-12.26 16.303v-2.898a1.96 1.96 0 0 0-.665-1.512c-.443-.403-.996-.604-1.66-.604-.665 0-1.218.201-1.661.604a1.96 1.96 0 0 0-.664 1.512v9.071L44.364 77.606a10.556 10.556 0 0 1-.886-1.512zm35.73-4.535c0 1.613-.332 3.124-.997 4.535a11.66 11.66 0 0 1-2.712 3.68c-1.134 1.032-2.49 1.864-4.04 2.469-1.55.604-3.21.907-4.982.907H55.185c-1.77 0-3.431-.303-4.981-.907-1.55-.605-2.906-1.437-4.041-2.47a12.49 12.49 0 0 1-1.384-1.512l13.727-18.217v6.375c0 .605.222 1.109.665 1.512.442.403.996.604 1.66.604.664 0 1.218-.201 1.66-.604a1.96 1.96 0 0 0 .665-1.512V53.87L75.97 36.838c.913.932 1.66 1.99 2.214 3.175.664 1.41.996 2.922.996 4.535v27.011h.028z"/></svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 1.4 KiB  |