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:
|
||||||
|
|
||||||
|
![donate](https://wpimg.wallstcn.com/bd273f0d-83a0-4ef2-92e1-9ac8ed3746b9.png)
|
||||||
|
|
||||||
|
[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 |