├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── README.md
├── babel.config.js
├── directoryTree.md
├── package-lock.json
├── package.json
├── public
├── favicon.ico
└── index.html
├── src
├── App.vue
├── assets
│ ├── http
│ │ ├── apiUrl.js
│ │ ├── index.js
│ │ └── service.js
│ ├── images
│ │ ├── 404.png
│ │ ├── 404_cloud.png
│ │ └── logo.png
│ ├── mock
│ │ └── index.js
│ └── utils
│ │ ├── get-page-title.js
│ │ ├── index.js
│ │ ├── token.js
│ │ └── validate.js
├── components
│ ├── Breadcrumb
│ │ └── index.vue
│ ├── Hamburger
│ │ └── index.vue
│ └── SvgIcon
│ │ └── index.vue
├── icons
│ ├── index.js
│ ├── svg
│ │ ├── account.svg
│ │ ├── dashboard.svg
│ │ ├── drink.svg
│ │ ├── example.svg
│ │ ├── eye-open.svg
│ │ ├── eye.svg
│ │ ├── form.svg
│ │ ├── link.svg
│ │ ├── nested.svg
│ │ ├── password.svg
│ │ ├── table.svg
│ │ ├── tree.svg
│ │ └── user.svg
│ └── svgo.yml
├── layout
│ ├── components
│ │ ├── AppMain.vue
│ │ ├── Navbar.vue
│ │ ├── Sidebar
│ │ │ ├── FixiOSBug.js
│ │ │ ├── Item.vue
│ │ │ ├── Link.vue
│ │ │ ├── Logo.vue
│ │ │ ├── SidebarItem.vue
│ │ │ └── index.vue
│ │ ├── TagsView
│ │ │ ├── ScrollPane.vue
│ │ │ └── index.vue
│ │ └── index.js
│ ├── index.vue
│ └── mixin
│ │ └── ResizeHandler.js
├── main.js
├── permission.js
├── router
│ ├── index.js
│ └── modules
│ │ └── table.js
├── settings.js
├── store
│ ├── getters.js
│ ├── index.js
│ └── modules
│ │ ├── app.js
│ │ ├── permission.js
│ │ ├── settings.js
│ │ ├── tagsView.js
│ │ └── user.js
├── styles
│ ├── element-ui.scss
│ ├── index.scss
│ ├── login.scss
│ ├── mixin.scss
│ ├── sidebar.scss
│ ├── table
│ │ └── demo.scss
│ ├── transition.scss
│ └── variables.scss
└── views
│ ├── 404
│ └── index.vue
│ ├── homepage
│ └── index.vue
│ ├── login
│ ├── index.vue
│ ├── register.vue
│ └── resetPsw.vue
│ ├── redirect
│ └── index.vue
│ └── table
│ ├── demo.vue
│ └── demoTest.vue
└── vue.config.js
/.eslintignore:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ccccai/vuecli3-ele-admin-template/c5d8b2d7571ec90b6ed3ee84e6aa2a42f3969301/.eslintignore
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | parserOptions: {
4 | parser: 'babel-eslint',
5 | sourceType: 'module'
6 | },
7 | env: {
8 | browser: true,
9 | node: true,
10 | es6: true,
11 | },
12 | extends: ['plugin:vue/recommended', 'eslint:recommended'],
13 |
14 | // add your custom rules here
15 | //it is base on https://github.com/vuejs/eslint-config-vue
16 | rules: {
17 | "vue/max-attributes-per-line": [2, {
18 | "singleline": 10,
19 | "multiline": {
20 | "max": 1,
21 | "allowFirstLine": false
22 | }
23 | }],
24 | "vue/singleline-html-element-content-newline": "off",
25 | "vue/multiline-html-element-content-newline":"off",
26 | "vue/name-property-casing": ["error", "PascalCase"],
27 | "vue/no-v-html": "off",
28 | 'accessor-pairs': 2,
29 | 'arrow-spacing': [2, {
30 | 'before': true,
31 | 'after': true
32 | }],
33 | 'block-spacing': [2, 'always'],
34 | 'brace-style': [2, '1tbs', {
35 | 'allowSingleLine': true
36 | }],
37 | 'camelcase': [0, {
38 | 'properties': 'always'
39 | }],
40 | 'comma-dangle': [2, 'never'],
41 | 'comma-spacing': [2, {
42 | 'before': false,
43 | 'after': true
44 | }],
45 | 'comma-style': [2, 'last'],
46 | 'constructor-super': 2,
47 | 'curly': [2, 'multi-line'],
48 | 'dot-location': [2, 'property'],
49 | 'eol-last': 2,
50 | 'eqeqeq': ["error", "always", {"null": "ignore"}],
51 | 'generator-star-spacing': [2, {
52 | 'before': true,
53 | 'after': true
54 | }],
55 | 'handle-callback-err': [2, '^(err|error)$'],
56 | 'indent': [2, 2, {
57 | 'SwitchCase': 1
58 | }],
59 | 'jsx-quotes': [2, 'prefer-single'],
60 | 'key-spacing': [2, {
61 | 'beforeColon': false,
62 | 'afterColon': true
63 | }],
64 | 'keyword-spacing': [2, {
65 | 'before': true,
66 | 'after': true
67 | }],
68 | 'new-cap': [2, {
69 | 'newIsCap': true,
70 | 'capIsNew': false
71 | }],
72 | 'new-parens': 2,
73 | 'no-array-constructor': 2,
74 | 'no-caller': 2,
75 | 'no-console': 'off',
76 | 'no-class-assign': 2,
77 | 'no-cond-assign': 2,
78 | 'no-const-assign': 2,
79 | 'no-control-regex': 0,
80 | 'no-delete-var': 2,
81 | 'no-dupe-args': 2,
82 | 'no-dupe-class-members': 2,
83 | 'no-dupe-keys': 2,
84 | 'no-duplicate-case': 2,
85 | 'no-empty-character-class': 2,
86 | 'no-empty-pattern': 2,
87 | 'no-eval': 2,
88 | 'no-ex-assign': 2,
89 | 'no-extend-native': 2,
90 | 'no-extra-bind': 2,
91 | 'no-extra-boolean-cast': 2,
92 | 'no-extra-parens': [2, 'functions'],
93 | 'no-fallthrough': 2,
94 | 'no-floating-decimal': 2,
95 | 'no-func-assign': 2,
96 | 'no-implied-eval': 2,
97 | 'no-inner-declarations': [2, 'functions'],
98 | 'no-invalid-regexp': 2,
99 | 'no-irregular-whitespace': 2,
100 | 'no-iterator': 2,
101 | 'no-label-var': 2,
102 | 'no-labels': [2, {
103 | 'allowLoop': false,
104 | 'allowSwitch': false
105 | }],
106 | 'no-lone-blocks': 2,
107 | 'no-mixed-spaces-and-tabs': 2,
108 | 'no-multi-spaces': 2,
109 | 'no-multi-str': 2,
110 | 'no-multiple-empty-lines': [2, {
111 | 'max': 1
112 | }],
113 | 'no-native-reassign': 2,
114 | 'no-negated-in-lhs': 2,
115 | 'no-new-object': 2,
116 | 'no-new-require': 2,
117 | 'no-new-symbol': 2,
118 | 'no-new-wrappers': 2,
119 | 'no-obj-calls': 2,
120 | 'no-octal': 2,
121 | 'no-octal-escape': 2,
122 | 'no-path-concat': 2,
123 | 'no-proto': 2,
124 | 'no-redeclare': 2,
125 | 'no-regex-spaces': 2,
126 | 'no-return-assign': [2, 'except-parens'],
127 | 'no-self-assign': 2,
128 | 'no-self-compare': 2,
129 | 'no-sequences': 2,
130 | 'no-shadow-restricted-names': 2,
131 | 'no-spaced-func': 2,
132 | 'no-sparse-arrays': 2,
133 | 'no-this-before-super': 2,
134 | 'no-throw-literal': 2,
135 | 'no-trailing-spaces': 2,
136 | 'no-undef': 2,
137 | 'no-undef-init': 2,
138 | 'no-unexpected-multiline': 2,
139 | 'no-unmodified-loop-condition': 2,
140 | 'no-unneeded-ternary': [2, {
141 | 'defaultAssignment': false
142 | }],
143 | 'no-unreachable': 2,
144 | 'no-unsafe-finally': 2,
145 | 'no-unused-vars': [2, {
146 | 'vars': 'all',
147 | 'args': 'none'
148 | }],
149 | 'no-useless-call': 2,
150 | 'no-useless-computed-key': 2,
151 | 'no-useless-constructor': 2,
152 | 'no-useless-escape': 0,
153 | 'no-whitespace-before-property': 2,
154 | 'no-with': 2,
155 | 'one-var': [2, {
156 | 'initialized': 'never'
157 | }],
158 | 'operator-linebreak': [2, 'after', {
159 | 'overrides': {
160 | '?': 'before',
161 | ':': 'before'
162 | }
163 | }],
164 | 'padded-blocks': [2, 'never'],
165 | 'quotes': [2, 'single', {
166 | 'avoidEscape': true,
167 | 'allowTemplateLiterals': true
168 | }],
169 | 'semi': [2, 'never'],
170 | 'semi-spacing': [2, {
171 | 'before': false,
172 | 'after': true
173 | }],
174 | 'space-before-blocks': [2, 'always'],
175 | 'space-before-function-paren': [2, 'never'],
176 | 'space-in-parens': [2, 'never'],
177 | 'space-infix-ops': 2,
178 | 'space-unary-ops': [2, {
179 | 'words': true,
180 | 'nonwords': false
181 | }],
182 | 'spaced-comment': [2, 'always', {
183 | 'markers': ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ',']
184 | }],
185 | 'template-curly-spacing': [2, 'never'],
186 | 'use-isnan': 2,
187 | 'valid-typeof': 2,
188 | 'wrap-iife': [2, 'any'],
189 | 'yield-star-spacing': [2, 'both'],
190 | 'yoda': [2, 'never'],
191 | 'prefer-const': 2,
192 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
193 | 'object-curly-spacing': [2, 'always', {
194 | objectsInObjects: false
195 | }],
196 | 'array-bracket-spacing': [2, 'never']
197 | }
198 | }
199 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /dist
4 |
5 | # local env files
6 | .env.local
7 | .env.*.local
8 |
9 | # Log files
10 | npm-debug.log*
11 | yarn-debug.log*
12 | yarn-error.log*
13 |
14 | # Editor directories and files
15 | .idea
16 | .vscode
17 | *.suo
18 | *.ntvs*
19 | *.njsproj
20 | *.sln
21 | *.sw?
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # vuecli3-ele-admin-template
2 | :star2: 一个基于vuecli3和vue-admin-template改造的响应式后台管理系统
3 |
4 | #### 软件架构
5 | vue-cli3 + element-UI
6 |
7 | #### 安装教程
8 |
9 | npm install
10 |
11 | #### 使用说明
12 |
13 | npm run serve
14 |
15 | #### 编译
16 |
17 | npm run build
18 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: ["@vue/app"]
3 | };
4 |
--------------------------------------------------------------------------------
/directoryTree.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ccccai/vuecli3-ele-admin-template/c5d8b2d7571ec90b6ed3ee84e6aa2a42f3969301/directoryTree.md
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vuecli3-ele-admin-template",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "serve": "vue-cli-service serve",
7 | "build": "vue-cli-service build",
8 | "lint": "vue-cli-service lint",
9 | "svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml"
10 | },
11 | "dependencies": {
12 | "axios": "^0.19.0",
13 | "babel-polyfill": "^6.26.0",
14 | "core-js": "^2.6.5",
15 | "es6-promise": "^4.2.8",
16 | "js-cookie": "^2.2.0",
17 | "lib-flexible": "^0.3.2",
18 | "normalize.css": "^8.0.1",
19 | "nprogress": "^0.2.0",
20 | "vue": "^2.6.10",
21 | "vue-router": "^3.0.3",
22 | "vuex": "^3.0.1",
23 | "vuex-persistedstate": "^2.5.4"
24 | },
25 | "devDependencies": {
26 | "@vue/cli-plugin-babel": "^3.10.0",
27 | "@vue/cli-plugin-eslint": "^3.10.0",
28 | "@vue/cli-service": "^3.10.0",
29 | "@vue/eslint-config-prettier": "^5.0.0",
30 | "autoprefixer": "^9.6.1",
31 | "babel-eslint": "^10.0.1",
32 | "element-ui": "^2.9.1",
33 | "eslint": "^5.16.0",
34 | "eslint-plugin-prettier": "^3.1.0",
35 | "eslint-plugin-vue": "^5.0.0",
36 | "mockjs": "^1.0.1-beta3",
37 | "node-sass": "^4.9.0",
38 | "postcss-plugin-px2rem": "^0.8.1",
39 | "prettier": "^1.18.2",
40 | "px2rem-loader": "^0.1.9",
41 | "sass-loader": "^7.1.0",
42 | "svg-sprite-loader": "^3.8.0",
43 | "svgo": "1.0.5",
44 | "vue-cli-plugin-element": "^1.0.1",
45 | "vue-template-compiler": "^2.6.10"
46 | },
47 | "browserslist": [
48 | "> 1%",
49 | "last 2 versions",
50 | "not ie <= 8",
51 | "iOS >= 8",
52 | "Firefox >= 20",
53 | "Android >= 4.4"
54 | ]
55 | }
56 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ccccai/vuecli3-ele-admin-template/c5d8b2d7571ec90b6ed3ee84e6aa2a42f3969301/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | vuecli3-ele-admin-template
14 |
15 |
16 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
13 |
--------------------------------------------------------------------------------
/src/assets/http/apiUrl.js:
--------------------------------------------------------------------------------
1 | /* 全局定义接口url */
2 |
3 | // host头,这里我们要使用代理,所以定义的字符串apiReplace是用来进行反向代理时的标记字符串。
4 | const apiHost = '/apiReplace/'
5 | // 密码登录
6 | const Login = `${apiHost}login`
7 | // 短信登录
8 | const LoginByVin = `${apiHost}loginByVin`
9 | // 发送短信
10 | const SendSms = `${apiHost}sendSms`
11 | // 表格模拟数据
12 | const GetPermissionData = `${apiHost}permission`
13 | // 表格模拟数据
14 | const GetTableData = `${apiHost}table`
15 |
16 | export default {
17 | Login,
18 | LoginByVin,
19 | SendSms,
20 | GetPermissionData,
21 | GetTableData
22 | }
23 |
--------------------------------------------------------------------------------
/src/assets/http/index.js:
--------------------------------------------------------------------------------
1 | /* 封装axios请求 */
2 | /* 用法示例:(*)为必须参数
3 | this.$request.httpRequest({
4 | headers: false, // 是否格式化参数
5 | (*)method: 'post', // 请求方式,post或get
6 | (*)url: this.API.ResetPassword, // 请求地址,请求地址的配置在@/api/apiUrl.js
7 | noLoading: true, // 是否显示全局Loading遮罩,默认每个请求都显示遮罩,即默认不设置该参数。如果需要某个请求不加遮罩,就设置noLoading: true即可
8 | returnFullData: true, // 是否返回完整数据,例如接口返回的数据格式为{ code:0, data: [], meaasge:''},则默认请求成功之后的回调函数的参数为data:[],如果设置returnFullData: true,则回调参数为{ code:0, data: [], meaasge:''}
9 | hideErrorMsg: true, // 是否展示错误提示
10 | (*)params: {}, // 请求参数,object类型
11 | (*)success: (data) => { // 请求成功之后的回调函数,data是回调参数
12 | // 在这里写请求成功后的逻辑
13 | },
14 | error: (err) => { 请求不成功之后的回调函数,data是回调参数
15 | // 在这里写请求报错后的逻辑
16 | }
17 | })
18 | */
19 | import service from './service'
20 | import { Message, Loading } from 'element-ui'
21 | import Qs from 'qs'
22 |
23 | function requestMethods(options) {
24 | return new Promise((resolve, reject) => {
25 | try {
26 | switch (options.method) {
27 | case 'post':
28 | if (options.headers) {
29 | resolve(
30 | service({
31 | url: options.url,
32 | method: 'post',
33 | data: options.params
34 | })
35 | )
36 | } else {
37 | resolve(
38 | service({
39 | url: options.url,
40 | method: 'post',
41 | data: Qs.stringify(options.params)
42 | })
43 | )
44 | }
45 | break
46 | case 'get':
47 | resolve(
48 | service({
49 | url: options.url,
50 | method: 'get',
51 | params: options.params
52 | })
53 | )
54 | break
55 | default: // 默认是get调用
56 | resolve(
57 | service({
58 | url: options.url,
59 | method: 'get',
60 | params: options.params
61 | })
62 | )
63 | break
64 | }
65 | } catch (e) {
66 | Message({
67 | message: 'HTTP请求方法出错!',
68 | type: 'error',
69 | duration: 3 * 1000
70 | })
71 | reject('methods error!')
72 | }
73 | })
74 | }
75 |
76 | function httpRequest(options = {}) {
77 | let loading
78 | if (!options.noLoading) {
79 | // 启用全局loading
80 | loading = Loading.service({
81 | lock: true,
82 | text: '加载中...',
83 | spinner: 'el-icon-loading',
84 | background: 'rgba(0, 0, 0, 0.7)'
85 | })
86 | }
87 |
88 | try {
89 | requestMethods(options).then(response => {
90 | // 成功返回结果的逻辑。根据接口定义的数据返回格式 修改判断条件
91 | const data = response.data
92 | if (data.resultCode === '1' || data.resultCode === 1) {
93 | // 成功
94 | const result = options.returnFullData ? data : data.data // 返回完整数据结构还是只返回有效数据
95 | options.success(result)
96 | } else {
97 | if (!options.hideErrorMsg) {
98 | // 失败
99 | let errorMsg = data.hasOwnProperty('resultMessage') ? data.resultMessage : '数据解析错误'
100 | switch (data.resultCode) {
101 | case '401':
102 | errorMsg = '暂无操作权限'
103 | break
104 | }
105 | Message.closeAll()
106 | Message({
107 | message: errorMsg,
108 | type: 'error',
109 | customClass: 'errorloginwidth',
110 | duration: 3000
111 | })
112 | }
113 | options.error(data)
114 | }
115 | if (!options.noLoading) {
116 | // loading完毕
117 | loading.close()
118 | }
119 | }).catch(e => {
120 | options.error(e.response)
121 | })
122 | } catch (e) {
123 | Message({
124 | message: 'Axios请求出错!',
125 | type: 'error',
126 | duration: 3 * 1000
127 | })
128 | }
129 | }
130 | export default {
131 | httpRequest
132 | }
133 |
--------------------------------------------------------------------------------
/src/assets/http/service.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import { Message } from 'element-ui'
3 | import { getToken } from '@/assets/utils/token'
4 | import router from '@/router'
5 | import store from '@/store'
6 |
7 | // 创建axios实例
8 | const service = axios.create({
9 | baseURL: process.env.BASE_API, // api 的 base_url
10 | timeout: 10000 // 请求超时时间
11 | })
12 |
13 | // request拦截器
14 | service.interceptors.request.use(
15 | config => {
16 | // 在此处设置请求头参数
17 | const token = getToken()
18 | if (token != null) {
19 | config.headers['Authorization'] = token
20 | }
21 | return config
22 | },
23 | error => {
24 | // Do something with request error
25 | console.log(error) // for debug
26 | return Promise.reject(error)
27 | }
28 | )
29 |
30 | // axios response 拦截器
31 | service.interceptors.response.use(
32 | response => {
33 | return response // 返回请求成功结果,status=200
34 | },
35 | err => {
36 | // 请求失败时,即status!=200
37 | if (err && err.response) {
38 | switch (err.response.status) {
39 | case 400:
40 | err.message = '错误请求'
41 | break
42 | case 401:
43 | err.message = '未授权,请重新登录'
44 | break
45 | case 403:
46 | err.message = '禁止访问'
47 | break
48 | case 404:
49 | err.message = '请求错误,未找到该资源'
50 | break
51 | case 405:
52 | err.message = '请求方法未允许'
53 | break
54 | case 408:
55 | err.message = '请求超时'
56 | break
57 | case 413:
58 | err.message = '上传文件过大'
59 | break
60 | case 500:
61 | err.message = '服务器端出错'
62 | break
63 | case 501:
64 | err.message = '网络未实现'
65 | break
66 | case 502:
67 | err.message = '网络错误'
68 | break
69 | case 503:
70 | err.message = '服务不可用'
71 | break
72 | case 504:
73 | err.message = '网络超时'
74 | break
75 | case 505:
76 | err.message = 'http版本不支持该请求'
77 | break
78 | default:
79 | err.message = `连接错误,${err.response.msg}`
80 | }
81 | } else {
82 | err.message = '当前网络状态不佳'
83 | }
84 | Message.closeAll()
85 | Message({
86 | message: err.message || '数据解析出错',
87 | type: 'error',
88 | customClass: 'errorloginwidth',
89 | duration: '3000'
90 | })
91 | if (err.response && err.response.status === 401) {
92 | store.dispatch('FedLogOut') // 前端登出,移除token
93 | router.replace({
94 | path: `/login?redirect=${window.location.href.split(/[#]/g)[1]}`
95 | })
96 | }
97 | return Promise.reject(err)
98 | }
99 | )
100 |
101 | export default service
102 |
--------------------------------------------------------------------------------
/src/assets/images/404.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ccccai/vuecli3-ele-admin-template/c5d8b2d7571ec90b6ed3ee84e6aa2a42f3969301/src/assets/images/404.png
--------------------------------------------------------------------------------
/src/assets/images/404_cloud.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ccccai/vuecli3-ele-admin-template/c5d8b2d7571ec90b6ed3ee84e6aa2a42f3969301/src/assets/images/404_cloud.png
--------------------------------------------------------------------------------
/src/assets/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ccccai/vuecli3-ele-admin-template/c5d8b2d7571ec90b6ed3ee84e6aa2a42f3969301/src/assets/images/logo.png
--------------------------------------------------------------------------------
/src/assets/mock/index.js:
--------------------------------------------------------------------------------
1 | /* mock配置文件
2 | ps:mock模块会影响原生的ajax请求,使得服务器返回的blob类型变成乱码,
3 | 所以如果在代码中有使用blob时,需要在mainjs中把mock注释掉,才能正常使用
4 | */
5 | // 引入mockjs
6 | import Mock from 'mockjs'
7 | // 获取 mock.Random 对象
8 | const Random = Mock.Random
9 | const result = {
10 | data: {},
11 | resultCode: 1,
12 | resultMessage: 'success'
13 | }
14 | // mock一组数据
15 | const loginData = () => {
16 | result.data = {
17 | token: Random.string(10),
18 | userInfo: {
19 | name: 'cc',
20 | roles: [
21 | {
22 | id: 2,
23 | role: 'superAdmin'
24 | }
25 | ]
26 | }
27 | }
28 | return result
29 | }
30 |
31 | const tableData = () => {
32 | const length = Random.integer(1, 20)
33 | const data = {
34 | totalCount: length,
35 | currentPage: 1,
36 | data: []
37 | }
38 | for (let i = 0; i < length; i++) {
39 | data.data[i] = {
40 | id: Random.id(),
41 | name: Random.cname(),
42 | age: Random.integer(1, 100),
43 | gender: Random.cword('男女'),
44 | phone: `1${Random.integer(1000000000, 9999999999)}`
45 | }
46 | }
47 | result.data = data
48 | return result
49 | }
50 |
51 | const smsData = () => {
52 | result.data = {
53 | code: Random.natural(1000, 9999)
54 | }
55 | return result
56 | }
57 |
58 | const permissionData = () => {
59 | result.data = [
60 | {
61 | name: 'Table',
62 | children: [
63 | {
64 | name: 'TableDemo',
65 | auth: {
66 | add: true,
67 | check: true,
68 | delete: true,
69 | edit: true
70 | }
71 | }
72 | ]
73 | }
74 | ]
75 | return result
76 | }
77 | Mock.mock('/apiReplace/login', 'post', loginData)
78 | Mock.mock('/apiReplace/sendSms', 'post', smsData)
79 | Mock.mock('/apiReplace/permission', 'post', permissionData)
80 | Mock.mock('/apiReplace/table', 'post', tableData)
81 |
--------------------------------------------------------------------------------
/src/assets/utils/get-page-title.js:
--------------------------------------------------------------------------------
1 | import defaultSettings from '@/settings'
2 |
3 | const title = defaultSettings.title || '奔腾开放云平台'
4 |
5 | export default function getPageTitle(pageTitle) {
6 | if (pageTitle) {
7 | return `${pageTitle} - ${title}`
8 | }
9 | return `${title}`
10 | }
11 |
--------------------------------------------------------------------------------
/src/assets/utils/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 |
3 | /**
4 | * 公共基础工具类
5 | */
6 |
7 | // 判断是否为空
8 | export function isNotEmpty(obj) {
9 | try {
10 | if (obj == null || obj == undefined) {
11 | return false
12 | }
13 | //判断数字是否是NaN
14 | if (typeof obj === 'number') {
15 | if (isNaN(obj)) {
16 | return false
17 | } else {
18 | return true
19 | }
20 | }
21 | //判断参数是否是布尔、函数、日期、正则,是则返回true
22 | if (
23 | typeof obj === 'boolean' ||
24 | typeof obj === 'function' ||
25 | obj instanceof Date ||
26 | obj instanceof RegExp
27 | ) {
28 | return true
29 | }
30 | //判断参数是否是字符串,去空,如果长度为0则返回false
31 | if (typeof obj === 'string') {
32 | if (obj.trim().length == 0) {
33 | return false
34 | } else {
35 | return true
36 | }
37 | }
38 |
39 | if (typeof obj === 'object') {
40 | //判断参数是否是数组,数组为空则返回false
41 | if (obj instanceof Array) {
42 | if (obj.length == 0) {
43 | return false
44 | } else {
45 | return true
46 | }
47 | }
48 |
49 | //判断参数是否是对象,判断是否是空对象,是则返回false
50 | if (obj instanceof Object) {
51 | //判断对象属性个数
52 | if (Object.getOwnPropertyNames(obj).length == 0) {
53 | return false
54 | } else {
55 | return true
56 | }
57 | }
58 | }
59 | } catch (e) {
60 | console.log(e)
61 | return false
62 | }
63 | }
64 |
65 | // 组装菜单
66 | export function formattingPermission(data) {
67 | let tmpData = []
68 | const defaultExpanded = []
69 | if (Object.prototype.toString.call(data) !== '[object Array]') {
70 | return { defaultExpanded: defaultExpanded, data: tmpData }
71 | }
72 | const level1 = [], level2 = [], level3 = []
73 | // 组装数据,拼接成elementUI树形控件支持的形式
74 | data.forEach(item => {
75 | switch (item.level) {
76 | case 1 :
77 | level1.push({...item, children: []})
78 | defaultExpanded.push(item.id) // 默认全部一级菜单是展开状态
79 | break
80 | case 2:
81 | level2.push({...item, children: []})
82 | break
83 | case 3:
84 | level3.push({...item})
85 | break
86 | }
87 | })
88 |
89 | level3.forEach((lastLevelItem, index) => {
90 | level2.forEach((secondLevelItem, i) => {
91 | if (secondLevelItem.id === lastLevelItem.parentId) {
92 | level2[i].children.push({
93 | ...lastLevelItem
94 | })
95 | }
96 | })
97 | })
98 |
99 | level2.forEach((secondLevelItem, index) => {
100 | level1.forEach((firstLevelItem, i) => {
101 | if (firstLevelItem.id === secondLevelItem.parentId) {
102 | level1[i].children.push({
103 | ...secondLevelItem
104 | })
105 | }
106 | })
107 | })
108 |
109 | tmpData = [...level1]
110 | return { defaultExpanded: defaultExpanded, data: tmpData }
111 | }
112 |
113 | // 将数组装换成字符串类型
114 | export function arrayToString(arr) {
115 | if (Object.prototype.toString.call(arr) !== '[object Array]') {
116 | return
117 | }
118 | const length = arr.length
119 | if (length === 0) {
120 | return ''
121 | } else {
122 | return JSON.stringify(arr)
123 | }
124 | }
125 |
126 | export function parseTime(time, cFormat) {
127 | if (arguments.length === 0) {
128 | return null
129 | }
130 | const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}'
131 | let date
132 | if (typeof time === 'object') {
133 | date = time
134 | } else {
135 | if (('' + time).length === 10) time = parseInt(time) * 1000
136 | date = new Date(time)
137 | }
138 | const formatObj = {
139 | y: date.getFullYear(),
140 | m: date.getMonth() + 1,
141 | d: date.getDate(),
142 | h: date.getHours(),
143 | i: date.getMinutes(),
144 | s: date.getSeconds(),
145 | a: date.getDay()
146 | }
147 | const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => {
148 | let value = formatObj[key]
149 | // Note: getDay() returns 0 on Sunday
150 | if (key === 'a') {
151 | return ['日', '一', '二', '三', '四', '五', '六'][value]
152 | }
153 | if (result.length > 0 && value < 10) {
154 | value = '0' + value
155 | }
156 | return value || 0
157 | })
158 | return time_str
159 | }
160 |
161 | export function formatTime(time, option) {
162 | time = +time * 1000
163 | const d = new Date(time)
164 | const now = Date.now()
165 |
166 | const diff = (now - d) / 1000
167 |
168 | if (diff < 30) {
169 | return '刚刚'
170 | } else if (diff < 3600) {
171 | // less 1 hour
172 | return Math.ceil(diff / 60) + '分钟前'
173 | } else if (diff < 3600 * 24) {
174 | return Math.ceil(diff / 3600) + '小时前'
175 | } else if (diff < 3600 * 24 * 2) {
176 | return '1天前'
177 | }
178 | if (option) {
179 | return parseTime(time, option)
180 | } else {
181 | return (
182 | d.getMonth() +
183 | 1 +
184 | '月' +
185 | d.getDate() +
186 | '日' +
187 | d.getHours() +
188 | '时' +
189 | d.getMinutes() +
190 | '分'
191 | )
192 | }
193 | }
194 |
195 | export function isExternal(path) {
196 | return /^(https?:|mailto:|tel:)/.test(path)
197 | }
198 |
199 | export function getUrlParam(name) {
200 | const reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)')
201 | const r = window.location.search.substr(1).match(reg)
202 | if (r !== null) {
203 | return unescape(r[2])
204 | }
205 | return null
206 | }
207 |
--------------------------------------------------------------------------------
/src/assets/utils/token.js:
--------------------------------------------------------------------------------
1 | /**
2 | * token本地存储
3 | */
4 |
5 | const TokenKey = 'User-Token'
6 |
7 | export function getToken() {
8 | return localStorage.getItem(TokenKey)
9 | }
10 |
11 | export function setToken(token) {
12 | return localStorage.setItem(TokenKey, token)
13 | }
14 |
15 | export function removeToken() {
16 | return localStorage.removeItem(TokenKey)
17 | }
18 |
--------------------------------------------------------------------------------
/src/assets/utils/validate.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 |
3 | /**
4 | * 表单验证工具类
5 | */
6 |
7 | export function isvalidPositiveFloat(str) {
8 | // 正浮点数,不包括0
9 | const strRegex = /^(([0-9]+\.[0-9]*[1-9][0-9]*)|([0-9]*[1-9][0-9]*\.[0-9]+)|([0-9]*[1-9][0-9]*))$/
10 | return strRegex.test(str)
11 | }
12 |
13 | export function isvalidPositiveNumber(str) {
14 | // 正整数,不包括0
15 | const strRegex = /^[0-9]*[1-9][0-9]*$/
16 | return strRegex.test(str)
17 | }
18 |
19 | /* 合法手机号码 */
20 | export function isvalidPhoneNumber(str) {
21 | const phoneRegex = /^1[34578]\d{9}$/
22 | return phoneRegex.test(str)
23 | }
24 |
25 | /* 合法密码 */
26 | export function isvalidPassword(str) {
27 | // 字符或字母6-20位,不考虑全为数字和全为字符情况
28 | // const passwordRegex = /^[0-9a-zA-Z]{6,20}$/ // 不能保证不能是纯数字或纯英文
29 | const passwordRegex = /^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{6,20}$/
30 | return passwordRegex.test(str)
31 | }
32 |
33 | /* 四位数字*/
34 | export function isvalidCode(str) {
35 | const codeRegex = /^\d{6}$/
36 | return codeRegex.test(str)
37 | }
38 |
39 | /* 合法uri*/
40 | export function validateURL(textval) {
41 | const urlregex = /^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/
42 | return urlregex.test(textval)
43 | }
44 |
45 | /* 小写字母*/
46 | export function validateLowerCase(str) {
47 | const reg = /^[a-z]+$/
48 | return reg.test(str)
49 | }
50 |
51 | /* 大写字母*/
52 | export function validateUpperCase(str) {
53 | const reg = /^[A-Z]+$/
54 | return reg.test(str)
55 | }
56 |
57 | /* 大小写字母*/
58 | export function validatAlphabets(str) {
59 | const reg = /^[A-Za-z]+$/
60 | return reg.test(str)
61 | }
62 |
--------------------------------------------------------------------------------
/src/components/Breadcrumb/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ levelList[0].meta.title }}
6 |
7 |
8 |
9 |
10 |
11 |
55 |
56 |
69 |
--------------------------------------------------------------------------------
/src/components/Hamburger/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
16 |
32 |
33 |
45 |
--------------------------------------------------------------------------------
/src/components/SvgIcon/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
34 |
35 |
44 |
--------------------------------------------------------------------------------
/src/icons/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import SvgIcon from '@/components/SvgIcon'// svg component
3 |
4 | // register globally
5 | Vue.component('svg-icon', SvgIcon)
6 |
7 | const req = require.context('./svg', false, /\.svg$/)
8 | const requireAll = requireContext => requireContext.keys().map(requireContext)
9 | requireAll(req)
10 |
--------------------------------------------------------------------------------
/src/icons/svg/account.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/svg/dashboard.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/svg/drink.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/svg/example.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/svg/eye-open.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/svg/eye.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/svg/form.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/svg/link.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/svg/nested.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/svg/password.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/svg/table.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/svg/tree.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/svg/user.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
83 |
--------------------------------------------------------------------------------
/src/icons/svgo.yml:
--------------------------------------------------------------------------------
1 | # replace default config
2 |
3 | # multipass: true
4 | # full: true
5 |
6 | plugins:
7 |
8 | # - name
9 | #
10 | # or:
11 | # - name: false
12 | # - name: true
13 | #
14 | # or:
15 | # - name:
16 | # param1: 1
17 | # param2: 2
18 |
19 | - removeAttrs:
20 | attrs:
21 | - 'fill'
22 | - 'fill-rule'
23 |
--------------------------------------------------------------------------------
/src/layout/components/AppMain.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
24 |
25 |
50 |
51 |
59 |
--------------------------------------------------------------------------------
/src/layout/components/Navbar.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
25 |
26 |
27 |
28 |
64 |
65 |
86 |
87 |
180 |
--------------------------------------------------------------------------------
/src/layout/components/Sidebar/FixiOSBug.js:
--------------------------------------------------------------------------------
1 | export default {
2 | computed: {
3 | device() {
4 | return this.$store.state.app.device
5 | }
6 | },
7 | mounted() {
8 | // In order to fix the click on menu on the ios device will trigger the mouseleave bug
9 | // https://github.com/PanJiaChen/vue-element-admin/issues/1135
10 | this.fixBugIniOS()
11 | },
12 | methods: {
13 | fixBugIniOS() {
14 | const $subMenu = this.$refs.subMenu
15 | if ($subMenu) {
16 | const handleMouseleave = $subMenu.handleMouseleave
17 | $subMenu.handleMouseleave = (e) => {
18 | if (this.device === 'mobile') {
19 | return
20 | }
21 | handleMouseleave(e)
22 | }
23 | }
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/layout/components/Sidebar/Item.vue:
--------------------------------------------------------------------------------
1 |
30 |
--------------------------------------------------------------------------------
/src/layout/components/Sidebar/Link.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
37 |
--------------------------------------------------------------------------------
/src/layout/components/Sidebar/Logo.vue:
--------------------------------------------------------------------------------
1 |
2 |
24 |
25 |
26 |
43 |
44 |
103 |
--------------------------------------------------------------------------------
/src/layout/components/Sidebar/SidebarItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
25 |
26 |
27 |
96 |
125 |
126 |
--------------------------------------------------------------------------------
/src/layout/components/Sidebar/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
57 |
--------------------------------------------------------------------------------
/src/layout/components/TagsView/ScrollPane.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
69 |
70 |
86 |
--------------------------------------------------------------------------------
/src/layout/components/TagsView/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
15 | {{ tag.title }}
16 |
17 |
18 |
19 |
30 |
31 |
32 |
33 |
201 |
202 |
270 |
271 |
296 |
--------------------------------------------------------------------------------
/src/layout/components/index.js:
--------------------------------------------------------------------------------
1 | export { default as Navbar } from './Navbar'
2 | export { default as Sidebar } from './Sidebar'
3 | export { default as AppMain } from './AppMain'
4 | export { default as TagsView } from './TagsView'
5 |
--------------------------------------------------------------------------------
/src/layout/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
52 |
53 |
94 |
--------------------------------------------------------------------------------
/src/layout/mixin/ResizeHandler.js:
--------------------------------------------------------------------------------
1 | import store from '@/store'
2 |
3 | const { body } = document
4 | const WIDTH = 992 // refer to Bootstrap's responsive design
5 |
6 | export default {
7 | watch: {
8 | $route(route) {
9 | if (this.device === 'mobile' && this.sidebar.opened) {
10 | store.dispatch('app/closeSideBar', { withoutAnimation: false })
11 | }
12 | }
13 | },
14 | beforeMount() {
15 | window.addEventListener('resize', this.$_resizeHandler)
16 | },
17 | beforeDestroy() {
18 | window.removeEventListener('resize', this.$_resizeHandler)
19 | },
20 | mounted() {
21 | const isMobile = this.$_isMobile()
22 | if (isMobile) {
23 | store.dispatch('app/toggleDevice', 'mobile')
24 | store.dispatch('app/closeSideBar', { withoutAnimation: true })
25 | }
26 | },
27 | methods: {
28 | // use $_ for mixins properties
29 | // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential
30 | $_isMobile() {
31 | const rect = body.getBoundingClientRect()
32 | return rect.width - 1 < WIDTH
33 | },
34 | $_resizeHandler() {
35 | if (!document.hidden) {
36 | const isMobile = this.$_isMobile()
37 | store.dispatch('app/toggleDevice', isMobile ? 'mobile' : 'desktop')
38 |
39 | if (isMobile) {
40 | store.dispatch('app/closeSideBar', { withoutAnimation: true })
41 | }
42 | }
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import App from '@/App.vue'
3 | import router from '@/router'
4 | import store from '@/store'
5 | import ElementUI from 'element-ui'
6 | import locale from 'element-ui/lib/locale/lang/zh-CN' // lang i18n
7 | import 'element-ui/lib/theme-chalk/index.css'
8 | import 'normalize.css/normalize.css' // A modern alternative to CSS resets
9 | import '@/styles/index.scss' // global css
10 | import 'element-ui/lib/theme-chalk/index.css'
11 | import '@/icons' // icon
12 | import '@/assets/mock'
13 | import '@/permission' // permission control
14 | import 'lib-flexible' // 使用lib-flexible来解决移动端适配
15 | // 解决低版本浏览器不支持promise问题
16 | import 'babel-polyfill'
17 | import Es6Promise from 'es6-promise'
18 | Es6Promise.polyfill()
19 |
20 | import Api from '@/assets/http/apiUrl'
21 | import Request from '@/assets/http'
22 |
23 | Vue.config.productionTip = false
24 | Vue.use(ElementUI, {
25 | locale
26 | })
27 | Vue.prototype.API = Api
28 | Vue.prototype.$request = Request
29 |
30 | new Vue({
31 | router,
32 | store,
33 | render: h => h(App)
34 | }).$mount('#app')
35 |
--------------------------------------------------------------------------------
/src/permission.js:
--------------------------------------------------------------------------------
1 | import router from '@/router'
2 | import store from '@/store'
3 | import { Message } from 'element-ui'
4 | import NProgress from 'nprogress' // Progress 进度条
5 | import 'nprogress/nprogress.css'// Progress 进度条样式
6 | import getPageTitle from '@/assets/utils/get-page-title'
7 |
8 | NProgress.configure({ showSpinner: false }) // NProgress Configuration
9 |
10 | const whiteList = ['/login', '/register', '/resetPsw'] // 不重定向白名单
11 |
12 | router.beforeEach(async(to, from, next) => {
13 | NProgress.start()
14 |
15 | // set page title
16 | document.title = getPageTitle(to.meta.title)
17 |
18 | // 有无token判断
19 | const token = localStorage.getItem('ADMIN_TOKEN')
20 | if (token) {
21 | if (whiteList.includes(to.path)) {
22 | next()
23 | NProgress.done()
24 | } else {
25 | // 判断当前用户是不是进行了刷新操作,防止进入死循环,如果存在就表示正常跳转,如果不存在就表示刷新了,vuex中的状态丢失了,需要重新挂载路由
26 | const hasUser = store.state.user.token
27 | if (hasUser) {
28 | next()
29 | } else {
30 | try {
31 | // 防止进入死循环
32 | await store.commit('SET_TOKEN', token)
33 | // 是不是超级管理员
34 | const isSuperAdmin = store.state.user.roles.some(item => item.id === 1)
35 | const accessRoutes = await store.dispatch('GenerateRoutes', isSuperAdmin)
36 | // 异步加载路由
37 | router.addRoutes(accessRoutes)
38 | router.options.routes = store.state.permission.routes
39 | // 设置replace:true,导航不会留下历史记录
40 | next({ ...to, replace: true })
41 | } catch (error) {
42 | // 移除token,重定向到登录页
43 | await store.dispatch('ResetToken')
44 | Message.error(error || '身份验证出错,请重新登录。')
45 | next(`/login?redirect=${to.path}`)
46 | NProgress.done()
47 | }
48 | }
49 | }
50 | } else {
51 | // 没有token
52 | if (whiteList.indexOf(to.path) !== -1) {
53 | next()
54 | } else {
55 | // next(`/login?redirect=${to.path}`) // 否则全部重定向到登录页
56 | next('/login') // 否则全部重定向到登录页
57 | NProgress.done()
58 | }
59 | }
60 | })
61 |
62 | router.afterEach(() => {
63 | NProgress.done() // 结束Progress
64 | })
65 |
--------------------------------------------------------------------------------
/src/router/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Router from 'vue-router'
3 |
4 | Vue.use(Router)
5 |
6 | export const constantRoutes = [
7 | {
8 | path: '/redirect',
9 | component: () => import('@/layout'),
10 | hidden: true,
11 | children: [
12 | {
13 | path: '/redirect/:path*',
14 | component: resolve => void require(['@/views/redirect/index'], resolve)
15 | }
16 | ]
17 | },
18 | {
19 | path: '/',
20 | redirect: '/home'
21 | },
22 | {
23 | path: '/login',
24 | name: 'Login',
25 | component: resolve => void require(['@/views/login/index'], resolve),
26 | hidden: true
27 | },
28 | {
29 | path: '/register',
30 | name: 'Register',
31 | component: resolve => void require(['@/views/login/register'], resolve),
32 | hidden: true
33 | },
34 | {
35 | path: '/resetPsw',
36 | name: 'ResetPsw',
37 | component: resolve => void require(['@/views/login/resetPsw'], resolve),
38 | hidden: true
39 | },
40 |
41 | {
42 | path: '/404',
43 | component: () => import('@/views/404'),
44 | hidden: true
45 | },
46 | {
47 | path: '/home',
48 | component: () => import('@/layout'),
49 | hidden: true,
50 | children: [
51 | {
52 | path: '/home',
53 | name: 'Home',
54 | component: resolve => void require(['@/views/homepage/index'], resolve),
55 | meta: {
56 | title: '主页',
57 | keepAlive: false, // 该字段表示该页面需要缓存
58 | isBack: false // 用于判断上一个页面是哪个
59 | }
60 | }
61 | ]
62 | }
63 | ]
64 |
65 | /**
66 | * 异步挂载的路由
67 | * 动态需要根据权限加载的路由表
68 | */
69 | const modulesFiles = require.context('./modules', true, /\.js$/)
70 | const routesModules = []
71 | // 自动引入modules目录下的所有模块
72 | modulesFiles.keys().reduce((modules, modulePath) => {
73 | const value = modulesFiles(modulePath)
74 | routesModules.push(value.default)
75 | }, {})
76 | export const asyncRoutes = routesModules
77 |
78 | /**
79 | * 最终无法匹配到相应路由,重定向到404
80 | * 异步加载路由时,在生成完异步路由准备挂载时,需要将重定向404的匹配规则定义在最后面,否则刷新会出错。
81 | */
82 | export const notFoundRoutes = [
83 | {
84 | path: '*',
85 | redirect: '/404',
86 | hidden: true,
87 | meta: {
88 | title: '404'
89 | }
90 | }
91 | ]
92 |
93 | const createRouter = () => new Router({
94 | // mode: 'history', // require service support
95 | scrollBehavior: () => ({ y: 0 }),
96 | routes: constantRoutes
97 | })
98 | const router = createRouter()
99 |
100 | // Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465
101 | export function resetRouter() {
102 | const newRouter = createRouter()
103 | router.matcher = newRouter.matcher // reset router
104 | }
105 |
106 | export default router
107 |
--------------------------------------------------------------------------------
/src/router/modules/table.js:
--------------------------------------------------------------------------------
1 | const table = {
2 | path: 'table',
3 | component: () => import('@/layout'),
4 | redirect: '/table/demo',
5 | name: 'Table',
6 | meta: {
7 | title: 'parentTitle',
8 | icon: 'table'
9 | },
10 | children: [
11 | {
12 | path: '/table/demo',
13 | name: 'TableDemo',
14 | component: resolve => void require(['@/views/table/demo'], resolve),
15 | meta: {
16 | title: 'tableDemo'
17 | }
18 | },
19 | {
20 | path: '/table/demoTest',
21 | name: 'DemoTest',
22 | component: resolve => void require(['@/views/table/demoTest'], resolve),
23 | meta: {
24 | title: 'demoTest'
25 | }
26 | }
27 | ]
28 | }
29 |
30 | export default table
31 |
--------------------------------------------------------------------------------
/src/settings.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 |
3 | title: '后台管理系统',
4 |
5 | /**
6 | * @type {boolean} true | false
7 | * @description Whether fix the header
8 | */
9 | fixedHeader: false,
10 |
11 | /**
12 | * @type {boolean} true | false
13 | * @description Whether need tagsView
14 | */
15 | tagsView: true,
16 |
17 | /**
18 | * @type {boolean} true | false
19 | * @description Whether show the logo in sidebar
20 | */
21 | sidebarLogo: true
22 | }
23 |
--------------------------------------------------------------------------------
/src/store/getters.js:
--------------------------------------------------------------------------------
1 | const getters = {
2 | sidebar: state => state.app.sidebar,
3 | device: state => state.app.device,
4 | visitedViews: state => state.tagsView.visitedViews,
5 | cachedViews: state => state.tagsView.cachedViews
6 | }
7 | export default getters
8 |
--------------------------------------------------------------------------------
/src/store/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Vuex from 'vuex'
3 | import getters from './getters'
4 | import createPersistedState from 'vuex-persistedstate'
5 |
6 | Vue.use(Vuex)
7 |
8 | // (创建了)一个包含了modules文件夹(包含子目录)下面的,所有文件名以 `.js` 结尾的、能被 require 请求到的文件的上下文。
9 | const modulesFiles = require.context('./modules', true, /\.js$/)
10 | // keys() 方法用于从modules创建一个包含modules里键值的可迭代对象。
11 | const modules = modulesFiles.keys().reduce((modules, modulePath) => {
12 | // 模块名,取文件名
13 | const moduleName = modulePath.replace(/^\.\/(.*)\.\w+$/, '$1')
14 | // 获取键名为modulePath的文件内容
15 | const value = modulesFiles(modulePath)
16 | // 将文件中的默认导出模块赋值给迭代对象modules
17 | modules[moduleName] = value.default
18 | // 返回迭代对象modules
19 | return modules
20 | // 默认值是空对象{}
21 | }, {})
22 |
23 | const store = new Vuex.Store({
24 | modules,
25 | getters,
26 | plugins: [
27 | // 存储vuex状态,使之刷新不丢失
28 | createPersistedState({
29 | storage: window.localStorage,
30 | reducer(val) {
31 | return {
32 | // 将要存储的state中的值放在这里
33 | user: {
34 | name: val.user.name,
35 | roles: val.user.roles
36 | }
37 | }
38 | }
39 | })
40 | ]
41 | })
42 |
43 | export default store
44 |
--------------------------------------------------------------------------------
/src/store/modules/app.js:
--------------------------------------------------------------------------------
1 | import Cookies from 'js-cookie'
2 |
3 | const state = {
4 | sidebar: {
5 | opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true,
6 | withoutAnimation: false
7 | },
8 | device: 'desktop'
9 | }
10 |
11 | const mutations = {
12 | TOGGLE_SIDEBAR: state => {
13 | state.sidebar.opened = !state.sidebar.opened
14 | state.sidebar.withoutAnimation = false
15 | if (state.sidebar.opened) {
16 | Cookies.set('sidebarStatus', 1)
17 | } else {
18 | Cookies.set('sidebarStatus', 0)
19 | }
20 | },
21 | CLOSE_SIDEBAR: (state, withoutAnimation) => {
22 | Cookies.set('sidebarStatus', 0)
23 | state.sidebar.opened = false
24 | state.sidebar.withoutAnimation = withoutAnimation
25 | },
26 | TOGGLE_DEVICE: (state, device) => {
27 | state.device = device
28 | }
29 | }
30 |
31 | const actions = {
32 | toggleSideBar({ commit }) {
33 | commit('TOGGLE_SIDEBAR')
34 | },
35 | closeSideBar({ commit }, { withoutAnimation }) {
36 | commit('CLOSE_SIDEBAR', withoutAnimation)
37 | },
38 | toggleDevice({ commit }, device) {
39 | commit('TOGGLE_DEVICE', device)
40 | }
41 | }
42 |
43 | export default {
44 | namespaced: true,
45 | state,
46 | mutations,
47 | actions
48 | }
49 |
--------------------------------------------------------------------------------
/src/store/modules/permission.js:
--------------------------------------------------------------------------------
1 | /**
2 | * constantRoutes 常规路由,不需要权限即可访问
3 | * asyncRoutes 需要访问权限的路由
4 | * notFoundRoutes 404路由
5 | * resetRouter 重置路由的方法
6 | */
7 | import { asyncRoutes, constantRoutes, notFoundRoutes, resetRouter } from '@/router'
8 | import API from '@/assets/http/apiUrl'
9 | import Request from '@/assets/http'
10 |
11 | const permission = {
12 | state: {
13 | routes: [],
14 | addRoutes: [] // 异步加载的路由
15 | },
16 |
17 | mutations: {
18 | SET_ROUTES: (state, routes) => {
19 | state.addRoutes = routes
20 | state.routes = constantRoutes.concat(routes)
21 | }
22 | },
23 |
24 | actions: {
25 | // 获取动态路由
26 | GenerateRoutes({ commit }, isSuperAdmin) {
27 | resetRouter() // 先初始化路由
28 | return new Promise((resolve, reject) => {
29 | // 如果是超级管理员,挂载全部路由全部权限
30 | if (isSuperAdmin) {
31 | // 重定向404的匹配规则需要在整个完整路由定义的最后面,否则刷新会出错。
32 | const accessedRoutes = [...asyncRoutes, ...notFoundRoutes]
33 | accessedRoutes.forEach(item => {
34 | if (item.children) {
35 | // 超级管理员赋全部权限
36 | item.children.forEach(elem => {
37 | elem.meta = {
38 | ...elem.meta,
39 | check: true,
40 | delete: true,
41 | add: true,
42 | edit: true
43 | }
44 | })
45 | }
46 | })
47 | commit('SET_ROUTES', accessedRoutes)
48 | resolve(accessedRoutes)
49 | } else {
50 | Request.httpRequest({
51 | method: 'post',
52 | url: API.GetPermissionData,
53 | noLoading: true,
54 | params: {},
55 | success: (data) => {
56 | console.log(data)
57 | let accessedRoutes = []
58 | // 匹配前端路由和后台返回的菜单
59 | accessedRoutes = filterAsyncRoutes(asyncRoutes, data)
60 | // 重定向404的匹配规则需要在整个完整路由定义的最后面,否则刷新会出错。
61 | accessedRoutes.push(...notFoundRoutes)
62 | commit('SET_ROUTES', accessedRoutes)
63 | resolve(accessedRoutes)
64 | },
65 | error: res => {
66 | reject(res)
67 | }
68 | })
69 | }
70 | })
71 | }
72 | }
73 | }
74 |
75 | /**
76 | * Filter asynchronous routing tables by recursion
77 | * 匹配后台返回的菜单信息和前端定义的路由
78 | * @param routes 前端定义好的异步路由
79 | * @param menus 后台返回的菜单
80 | */
81 | export function filterAsyncRoutes(routes = [], menus = []) {
82 | const res = []
83 |
84 | routes.forEach(route => {
85 | // 复制一遍路由,这样改变tmp的同时路由不会受影响
86 | const tmp = {
87 | ...route
88 | }
89 |
90 | // 是否匹配到了
91 | if (hasPermission(menus, tmp)) { // 有符合的匹配项
92 | // 找出那一条匹配成功的路由项
93 | const findMenu = menus.find((menu, index, menus) => {
94 | return menu.name.includes(tmp.name)
95 | })
96 |
97 | // 赋权
98 | if (findMenu.hasOwnProperty('auth')) {
99 | tmp.meta = {
100 | ...tmp.meta,
101 | ...findMenu.auth
102 | }
103 | }
104 |
105 | // 如果该路由项中含有子路由,子路由也是需要和菜单进行匹配的
106 | if (findMenu.hasOwnProperty('children') && findMenu.children.length) {
107 | // 子路由匹配的步骤和父路由一样
108 | tmp.children = filterAsyncRoutes(tmp.children, findMenu.children)
109 | } else {
110 | // 将匹配不到的子路由从路由中删除
111 | delete tmp.children
112 | }
113 |
114 | // 最后得到的结果就是和后台返回菜单匹配一致的异步路由值
115 | res.push(tmp)
116 | }
117 | })
118 |
119 | return res
120 | }
121 |
122 | /**
123 | * Use meta.role to determine if the current user has permission
124 | * @param menus 后台返回的菜单
125 | * @param route 前端定义好的异步路由中的项
126 | */
127 | function hasPermission(menus, route) {
128 | // 进行匹配
129 | if (route.name) { // 前提是异步路由要存在name
130 | // 匹配的规则是,name要一致,只要匹配到就返回true,停止继续往下循环
131 | return menus.some(menu => route.name.includes(menu.name))
132 | } else {
133 | return true
134 | }
135 | }
136 |
137 | export default permission
138 |
--------------------------------------------------------------------------------
/src/store/modules/settings.js:
--------------------------------------------------------------------------------
1 | import defaultSettings from '@/settings'
2 |
3 | const { showSettings, tagsView, fixedHeader, sidebarLogo } = defaultSettings
4 |
5 | const state = {
6 | showSettings: showSettings,
7 | fixedHeader: fixedHeader,
8 | tagsView: tagsView,
9 | sidebarLogo: sidebarLogo
10 | }
11 |
12 | const mutations = {
13 | CHANGE_SETTING: (state, { key, value }) => {
14 | if (state.hasOwnProperty(key)) {
15 | state[key] = value
16 | }
17 | }
18 | }
19 |
20 | const actions = {
21 | changeSetting({ commit }, data) {
22 | commit('CHANGE_SETTING', data)
23 | }
24 | }
25 |
26 | export default {
27 | namespaced: true,
28 | state,
29 | mutations,
30 | actions
31 | }
32 |
33 |
--------------------------------------------------------------------------------
/src/store/modules/tagsView.js:
--------------------------------------------------------------------------------
1 | const state = {
2 | visitedViews: [],
3 | cachedViews: []
4 | }
5 |
6 | const mutations = {
7 | ADD_VISITED_VIEW: (state, view) => {
8 | if (state.visitedViews.some(v => v.path === view.path)) return
9 | state.visitedViews.push(
10 | Object.assign({}, view, {
11 | title: view.meta.title || 'no-name'
12 | })
13 | )
14 | },
15 | ADD_CACHED_VIEW: (state, view) => {
16 | if (state.cachedViews.includes(view.name)) return
17 | if (!view.meta.noCache) {
18 | state.cachedViews.push(view.name)
19 | }
20 | },
21 |
22 | DEL_VISITED_VIEW: (state, view) => {
23 | for (const [i, v] of state.visitedViews.entries()) {
24 | if (v.path === view.path) {
25 | state.visitedViews.splice(i, 1)
26 | break
27 | }
28 | }
29 | },
30 | DEL_CACHED_VIEW: (state, view) => {
31 | for (const i of state.cachedViews) {
32 | if (i === view.name) {
33 | const index = state.cachedViews.indexOf(i)
34 | state.cachedViews.splice(index, 1)
35 | break
36 | }
37 | }
38 | },
39 |
40 | DEL_OTHERS_VISITED_VIEWS: (state, view) => {
41 | state.visitedViews = state.visitedViews.filter(v => {
42 | return v.meta.affix || v.path === view.path
43 | })
44 | },
45 | DEL_OTHERS_CACHED_VIEWS: (state, view) => {
46 | for (const i of state.cachedViews) {
47 | if (i === view.name) {
48 | const index = state.cachedViews.indexOf(i)
49 | state.cachedViews = state.cachedViews.slice(index, index + 1)
50 | break
51 | }
52 | }
53 | },
54 |
55 | DEL_ALL_VISITED_VIEWS: state => {
56 | // keep affix tags
57 | const affixTags = state.visitedViews.filter(tag => tag.meta.affix)
58 | state.visitedViews = affixTags
59 | },
60 | DEL_ALL_CACHED_VIEWS: state => {
61 | state.cachedViews = []
62 | },
63 |
64 | UPDATE_VISITED_VIEW: (state, view) => {
65 | for (let v of state.visitedViews) {
66 | if (v.path === view.path) {
67 | v = Object.assign(v, view)
68 | break
69 | }
70 | }
71 | }
72 | }
73 |
74 | const actions = {
75 | addView({ dispatch }, view) {
76 | dispatch('addVisitedView', view)
77 | dispatch('addCachedView', view)
78 | },
79 | addVisitedView({ commit }, view) {
80 | commit('ADD_VISITED_VIEW', view)
81 | },
82 | addCachedView({ commit }, view) {
83 | commit('ADD_CACHED_VIEW', view)
84 | },
85 |
86 | delView({ dispatch, state }, view) {
87 | return new Promise(resolve => {
88 | dispatch('delVisitedView', view)
89 | dispatch('delCachedView', view)
90 | resolve({
91 | visitedViews: [...state.visitedViews],
92 | cachedViews: [...state.cachedViews]
93 | })
94 | })
95 | },
96 | delVisitedView({ commit, state }, view) {
97 | return new Promise(resolve => {
98 | commit('DEL_VISITED_VIEW', view)
99 | resolve([...state.visitedViews])
100 | })
101 | },
102 | delCachedView({ commit, state }, view) {
103 | return new Promise(resolve => {
104 | commit('DEL_CACHED_VIEW', view)
105 | resolve([...state.cachedViews])
106 | })
107 | },
108 |
109 | delOthersViews({ dispatch, state }, view) {
110 | return new Promise(resolve => {
111 | dispatch('delOthersVisitedViews', view)
112 | dispatch('delOthersCachedViews', view)
113 | resolve({
114 | visitedViews: [...state.visitedViews],
115 | cachedViews: [...state.cachedViews]
116 | })
117 | })
118 | },
119 | delOthersVisitedViews({ commit, state }, view) {
120 | return new Promise(resolve => {
121 | commit('DEL_OTHERS_VISITED_VIEWS', view)
122 | resolve([...state.visitedViews])
123 | })
124 | },
125 | delOthersCachedViews({ commit, state }, view) {
126 | return new Promise(resolve => {
127 | commit('DEL_OTHERS_CACHED_VIEWS', view)
128 | resolve([...state.cachedViews])
129 | })
130 | },
131 |
132 | delAllViews({ dispatch, state }, view) {
133 | return new Promise(resolve => {
134 | dispatch('delAllVisitedViews', view)
135 | dispatch('delAllCachedViews', view)
136 | resolve({
137 | visitedViews: [...state.visitedViews],
138 | cachedViews: [...state.cachedViews]
139 | })
140 | })
141 | },
142 | delAllVisitedViews({ commit, state }) {
143 | return new Promise(resolve => {
144 | commit('DEL_ALL_VISITED_VIEWS')
145 | resolve([...state.visitedViews])
146 | })
147 | },
148 | delAllCachedViews({ commit, state }) {
149 | return new Promise(resolve => {
150 | commit('DEL_ALL_CACHED_VIEWS')
151 | resolve([...state.cachedViews])
152 | })
153 | },
154 |
155 | updateVisitedView({ commit }, view) {
156 | commit('UPDATE_VISITED_VIEW', view)
157 | }
158 | }
159 |
160 | export default {
161 | namespaced: true,
162 | state,
163 | mutations,
164 | actions
165 | }
166 |
--------------------------------------------------------------------------------
/src/store/modules/user.js:
--------------------------------------------------------------------------------
1 | import API from '@/assets/http/apiUrl'
2 | import Request from '@/assets/http'
3 |
4 | const user = {
5 | state: {
6 | token: '',
7 | name: '',
8 | roles: ''
9 | },
10 | mutations: {
11 | SET_TOKEN: (state, data) => {
12 | state.token = data
13 | },
14 | SET_USER_INFO: (state, data) => {
15 | state.name = data.name || ''
16 | state.roles = data.roles || []
17 | }
18 | },
19 | actions: {
20 | Login({ commit }, params) {
21 | return new Promise((resolve, reject) => {
22 | Request.httpRequest({
23 | method: 'post',
24 | url: API.Login,
25 | params: params,
26 | success: data => {
27 | localStorage.setItem('ADMIN_TOKEN', data.token)
28 | commit('SET_USER_INFO', data.userInfo)
29 | resolve(data)
30 | },
31 | error: err => {
32 | reject(err)
33 | }
34 | })
35 | })
36 | },
37 |
38 | LoginByVin({ dispatch, commit }, params) {
39 | return dispatch('Login', params)
40 | },
41 |
42 | ResetToken({ commit }) {
43 | return new Promise(resolve => {
44 | console.log(1111)
45 | commit('SET_TOKEN', '')
46 | commit('SET_USER_INFO', {})
47 | localStorage.removeItem('ADMIN_TOKEN')
48 | resolve()
49 | })
50 | },
51 |
52 | // ??
53 | LogOut({ dispatch, commit }) {
54 | return dispatch('ResetToken')
55 | }
56 | }
57 | }
58 |
59 | export default user
60 |
61 |
--------------------------------------------------------------------------------
/src/styles/element-ui.scss:
--------------------------------------------------------------------------------
1 | // cover some element-ui styles
2 |
3 | .el-breadcrumb__inner,
4 | .el-breadcrumb__inner a {
5 | font-weight: 400 !important;
6 | }
7 |
8 | .el-upload {
9 | input[type="file"] {
10 | display: none !important;
11 | }
12 | }
13 |
14 | .el-upload__input {
15 | display: none;
16 | }
17 |
18 |
19 | // to fixed https://github.com/ElemeFE/element/issues/2461
20 | .el-dialog {
21 | transform: none;
22 | left: 0;
23 | position: relative;
24 | margin: 0 auto;
25 | }
26 |
27 | // refine element ui upload
28 | .upload-container {
29 | .el-upload {
30 | width: 100%;
31 |
32 | .el-upload-dragger {
33 | width: 100%;
34 | height: 200px;
35 | }
36 | }
37 | }
38 |
39 | // dropdown
40 | .el-dropdown-menu {
41 | a {
42 | display: block
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/styles/index.scss:
--------------------------------------------------------------------------------
1 | @import './variables.scss';
2 | @import './mixin.scss';
3 | @import './transition.scss';
4 | @import './element-ui.scss';
5 | @import './sidebar.scss';
6 |
7 | body {
8 | height: 100%;
9 | -moz-osx-font-smoothing: grayscale;
10 | -webkit-font-smoothing: antialiased;
11 | text-rendering: optimizeLegibility;
12 | font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
13 | }
14 |
15 | label {
16 | font-weight: 700;
17 | }
18 |
19 | html {
20 | height: 100%;
21 | box-sizing: border-box;
22 | }
23 |
24 | #app {
25 | height: 100%;
26 | }
27 |
28 | *,
29 | *:before,
30 | *:after {
31 | box-sizing: inherit;
32 | }
33 |
34 | a:focus,
35 | a:active {
36 | outline: none;
37 | }
38 |
39 | a,
40 | a:focus,
41 | a:hover {
42 | cursor: pointer;
43 | color: inherit;
44 | text-decoration: none;
45 | }
46 |
47 | div:focus {
48 | outline: none;
49 | }
50 |
51 | .clearfix {
52 | &:after {
53 | visibility: hidden;
54 | display: block;
55 | font-size: 0;
56 | content: " ";
57 | clear: both;
58 | height: 0;
59 | }
60 | }
61 |
62 | // main-container global css
63 | .app-container {
64 | padding: 20px;
65 | }
66 |
67 | /* 去掉箭头 */
68 | input::-webkit-outer-spin-button,
69 | input::-webkit-inner-spin-button {
70 | -webkit-appearance: none;
71 | }
72 | input[type="number"] {
73 | -moz-appearance: textfield;
74 | }
75 |
76 | // 改变Placeholder文字的颜色
77 | input::-moz-placeholder{color:rgb(204, 204, 204)} //Firefox
78 | input::-webkit-input-placeholder{color:rgb(204, 204, 204)} //Chrome,Safari
79 | input:-ms-input-placeholder{color:rgb(204, 204, 204)} // ie
80 | textarea::-moz-placeholder{color:rgb(204, 204, 204)} //Firefox
81 | textarea::-webkit-input-placeholder{color:rgb(204, 204, 204)} //Chrome,Safari
82 | textarea:-ms-input-placeholder{color:rgb(204, 204, 204)} // ie
83 |
--------------------------------------------------------------------------------
/src/styles/login.scss:
--------------------------------------------------------------------------------
1 | $bg: rgb(245, 245, 245);
2 | $dark_gray: rgb(18, 29, 37);
3 | $light_gray: rgb(18, 29, 37);
4 | .login-container {
5 | position: fixed;
6 | height: 100%;
7 | width: 100%;
8 | background-color: $bg;
9 | .el-form-item {
10 | margin-bottom: 25px;
11 | }
12 | .login-card {
13 | border-radius: 8px;
14 | .el-card__body {
15 | padding: 40px;
16 | }
17 | }
18 | .login-form {
19 | position: absolute;
20 | left: 0;
21 | right: 0;
22 | width: 540px;
23 | max-width: 100%;
24 | padding: 35px 35px 15px 35px;
25 | margin: 120px auto;
26 | .login-card {
27 | .login-tab {
28 | display: flex;
29 | align-items: center;
30 | padding: 0 20px 20px;
31 | .tab-item {
32 | flex: 1;
33 | text-align: center;
34 | font-size: 16px;
35 | cursor: pointer;
36 | &:hover {
37 | color: #121d25 !important;
38 | }
39 | }
40 | }
41 | .login-input-item {
42 | border: 1px solid rgba(0, 0, 0, 0.1);
43 | border-radius: 5px;
44 | color: #454545;
45 | position: relative;
46 | .el-input {
47 | display: inline-block;
48 | width: 100%;
49 | padding-left: 15px;
50 | margin-bottom: 2px;
51 | input {
52 | background: transparent;
53 | border: 0px;
54 | -webkit-appearance: none;
55 | border-radius: 0px;
56 | color: $light_gray;
57 | padding-left: 0;
58 | &:-webkit-autofill {
59 | -webkit-box-shadow: 0 0 0px 1000px #fff inset !important;
60 | -webkit-text-fill-color: $light_gray !important;
61 | color: $light_gray !important;
62 | caret-color:$light_gray !important;
63 | }
64 | &::-moz-placeholder{color:rgb(204, 204, 204)} //Firefox
65 | &::-webkit-input-placeholder{color:rgb(204, 204, 204)} //Chrome,Safari
66 | &:-ms-input-placeholder{color:rgb(204, 204, 204)} // ie
67 | }
68 | }
69 | .svg-container {
70 | position: absolute;
71 | top: 0;
72 | left: 0;
73 | padding: 0 15px;
74 | color: $dark_gray;
75 | vertical-align: middle;
76 | display: inline-block;
77 | }
78 | .code {
79 | position: absolute;
80 | right: 10px;
81 | top: 2px;
82 | font-size: 14px;
83 | color: #121d25;
84 | cursor: pointer;
85 | user-select: none;
86 | }
87 | }
88 | .to-login {
89 | display: flex;
90 | justify-content: flex-end;
91 | font-size: 14px;
92 | margin-top: 8px;
93 | .btn {
94 | cursor: pointer;
95 | &:hover {
96 | color: #409EFF;
97 | }
98 | }
99 | }
100 | .accept-agreement {
101 | margin-bottom: 20px;
102 | font-size: 14px;
103 | .agreement {
104 | cursor: pointer;
105 | &:hover {
106 | color: #409EFF;
107 | }
108 | }
109 | }
110 | .psw {
111 | margin-bottom: 0
112 | }
113 | .forget-psw {
114 | display: flex;
115 | justify-content: flex-end;
116 | cursor: pointer;
117 | font-size: 14px;
118 | margin-top: 8px;
119 | margin-bottom: 22px;
120 | &:hover {
121 | color: #409EFF;
122 | }
123 | }
124 | }
125 | }
126 | .tips {
127 | font-size: 14px;
128 | color: #000;
129 | margin-bottom: 10px;
130 | span {
131 | &:first-of-type {
132 | margin-right: 16px;
133 | }
134 | }
135 | }
136 | .title {
137 | font-size: 32px;
138 | color: $light_gray;
139 | margin: 0px auto 40px auto;
140 | text-align: center;
141 | letter-spacing: 1px;
142 | }
143 | .show-pwd {
144 | position: absolute;
145 | right: 10px;
146 | top: 7px;
147 | font-size: 16px;
148 | color: $dark_gray;
149 | cursor: pointer;
150 | user-select: none;
151 | }
152 | }
--------------------------------------------------------------------------------
/src/styles/mixin.scss:
--------------------------------------------------------------------------------
1 | @mixin clearfix {
2 | &:after {
3 | content: "";
4 | display: table;
5 | clear: both;
6 | }
7 | }
8 |
9 | @mixin scrollBar {
10 | &::-webkit-scrollbar-track-piece {
11 | background: #d3dce6;
12 | }
13 |
14 | &::-webkit-scrollbar {
15 | width: 6px;
16 | }
17 |
18 | &::-webkit-scrollbar-thumb {
19 | background: #99a9bf;
20 | border-radius: 20px;
21 | }
22 | }
23 |
24 | @mixin relative {
25 | position: relative;
26 | width: 100%;
27 | height: 100%;
28 | }
29 |
30 | /* 背景自适应容器大小 */
31 | @mixin bgCover($url) {
32 | background-image: url($url);
33 | background-repeat: no-repeat;
34 | background-size: cover;
35 | background-position: 0 center;
36 | }
37 |
38 | @mixin noData($url) {
39 | width: 100%;
40 | font-size: 14px;
41 | text-align: center;
42 | color: #666;
43 | line-height: 60px;
44 | }
45 |
46 |
--------------------------------------------------------------------------------
/src/styles/sidebar.scss:
--------------------------------------------------------------------------------
1 | #app {
2 |
3 | .main-container {
4 | min-height: 100%;
5 | transition: margin-left .28s;
6 | margin-left: $sideBarWidth;
7 | position: relative;
8 | }
9 |
10 | .sidebar-container {
11 | transition: width 0.28s;
12 | width: $sideBarWidth !important;
13 | background-color: $menuBg;
14 | height: 100%;
15 | position: fixed;
16 | font-size: 0px;
17 | top: 0;
18 | bottom: 0;
19 | left: 0;
20 | z-index: 1001;
21 | overflow: hidden;
22 |
23 | // reset element-ui css
24 | .horizontal-collapse-transition {
25 | transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out;
26 | }
27 |
28 | .scrollbar-wrapper {
29 | overflow-x: hidden !important;
30 | }
31 |
32 | .el-scrollbar__bar.is-vertical {
33 | right: 0px;
34 | }
35 |
36 | .el-scrollbar {
37 | height: 100%;
38 | }
39 |
40 | &.has-logo {
41 | .el-scrollbar {
42 | height: calc(100% - 50px);
43 | }
44 | }
45 |
46 | .is-horizontal {
47 | display: none;
48 | }
49 |
50 | a {
51 | display: inline-block;
52 | width: 100%;
53 | overflow: hidden;
54 | }
55 |
56 | .svg-icon {
57 | margin-right: 16px;
58 | }
59 |
60 | .el-menu {
61 | border: none;
62 | height: 100%;
63 | width: 100% !important;
64 | }
65 |
66 | // menu hover
67 | .submenu-title-noDropdown,
68 | .el-submenu__title {
69 | &:hover {
70 | background-color: $menuHover !important;
71 | }
72 | }
73 |
74 | .is-active>.el-submenu__title {
75 | color: $subMenuActiveText !important;
76 | }
77 |
78 | & .nest-menu .el-submenu>.el-submenu__title,
79 | & .el-submenu .el-menu-item {
80 | min-width: $sideBarWidth !important;
81 | background-color: $subMenuBg !important;
82 |
83 | &:hover {
84 | background-color: $subMenuHover !important;
85 | }
86 | }
87 | }
88 |
89 | .hideSidebar {
90 | .sidebar-container {
91 | width: 54px !important;
92 | }
93 |
94 | .main-container {
95 | margin-left: 54px;
96 | }
97 |
98 | .submenu-title-noDropdown {
99 | padding: 0 !important;
100 | position: relative;
101 |
102 | .el-tooltip {
103 | padding: 0 !important;
104 |
105 | .svg-icon {
106 | margin-left: 20px;
107 | }
108 | }
109 | }
110 |
111 | .el-submenu {
112 | overflow: hidden;
113 |
114 | &>.el-submenu__title {
115 | padding: 0 !important;
116 |
117 | .svg-icon {
118 | margin-left: 20px;
119 | }
120 |
121 | .el-submenu__icon-arrow {
122 | display: none;
123 | }
124 | }
125 | }
126 |
127 | .el-menu--collapse {
128 | .el-submenu {
129 | &>.el-submenu__title {
130 | &>span {
131 | height: 0;
132 | width: 0;
133 | overflow: hidden;
134 | visibility: hidden;
135 | display: inline-block;
136 | }
137 | }
138 | }
139 | }
140 | }
141 |
142 | .el-menu--collapse .el-menu .el-submenu {
143 | min-width: $sideBarWidth !important;
144 | }
145 |
146 | // mobile responsive
147 | .mobile {
148 | .main-container {
149 | margin-left: 0px;
150 | }
151 |
152 | .sidebar-container {
153 | transition: transform .28s;
154 | width: $sideBarWidth !important;
155 | }
156 |
157 | &.hideSidebar {
158 | .sidebar-container {
159 | pointer-events: none;
160 | transition-duration: 0.3s;
161 | transform: translate3d(-$sideBarWidth, 0, 0);
162 | }
163 | }
164 | }
165 |
166 | .withoutAnimation {
167 |
168 | .main-container,
169 | .sidebar-container {
170 | transition: none;
171 | }
172 | }
173 | }
174 |
175 | // when menu collapsed
176 | .el-menu--vertical {
177 | &>.el-menu {
178 | .svg-icon {
179 | margin-right: 16px;
180 | }
181 | }
182 |
183 | .nest-menu .el-submenu>.el-submenu__title,
184 | .el-menu-item {
185 | &:hover {
186 | // you can use $subMenuHover
187 | background-color: $menuHover !important;
188 | }
189 | }
190 |
191 | // the scroll bar appears when the subMenu is too long
192 | >.el-menu--popup {
193 | max-height: 100vh;
194 | overflow-y: auto;
195 |
196 | &::-webkit-scrollbar-track-piece {
197 | background: #d3dce6;
198 | }
199 |
200 | &::-webkit-scrollbar {
201 | width: 6px;
202 | }
203 |
204 | &::-webkit-scrollbar-thumb {
205 | background: #99a9bf;
206 | border-radius: 20px;
207 | }
208 | }
209 | }
210 |
--------------------------------------------------------------------------------
/src/styles/table/demo.scss:
--------------------------------------------------------------------------------
1 |
2 | @import '~@/styles/mixin';
3 | .el-card {
4 | border-radius: 8px;
5 | }
6 |
7 | .pagination {
8 | overflow: auto;
9 | text-align: right;
10 | padding: 20px 0;
11 | margin: 0 10px;
12 | }
13 |
14 | .list-content {
15 | background: #fff;
16 | border: 0;
17 | .el-card__body {
18 | padding: 0;
19 | .el-table thead th {
20 | background: #e6e6e6;
21 | color: #606266;
22 | }
23 | }
24 | .table-content {
25 | border-radius: 8px;
26 | .table-img {
27 | width: 120px;
28 | height: 120px;
29 | }
30 | .icon-btn {
31 | cursor: pointer;
32 | }
33 | .operation {
34 | text-overflow: unset;
35 | .cell {
36 | white-space: nowrap;
37 | text-overflow: unset;
38 | }
39 | .item {
40 | color: #409EFF;
41 | margin-right: 20px;
42 | &:last-child {
43 | margin-right: 0px;
44 | }
45 | &:hover {
46 | color: #606266;
47 | }
48 | }
49 | .item-gray {
50 | cursor: default;
51 | color: #aaa;
52 | margin-right: 20px;
53 | &:last-child {
54 | margin-right: 0px;
55 | }
56 | }
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/styles/transition.scss:
--------------------------------------------------------------------------------
1 | // global transition css
2 |
3 | /* fade */
4 | .fade-enter-active,
5 | .fade-leave-active {
6 | transition: opacity 0.28s;
7 | }
8 |
9 | .fade-enter,
10 | .fade-leave-active {
11 | opacity: 0;
12 | }
13 |
14 | /* fade-transform */
15 | .fade-transform-leave-active,
16 | .fade-transform-enter-active {
17 | transition: all .5s;
18 | }
19 |
20 | .fade-transform-enter {
21 | opacity: 0;
22 | transform: translateX(-30px);
23 | }
24 |
25 | .fade-transform-leave-to {
26 | opacity: 0;
27 | transform: translateX(30px);
28 | }
29 |
30 | /* breadcrumb transition */
31 | .breadcrumb-enter-active,
32 | .breadcrumb-leave-active {
33 | transition: all .5s;
34 | }
35 |
36 | .breadcrumb-enter,
37 | .breadcrumb-leave-active {
38 | opacity: 0;
39 | transform: translateX(20px);
40 | }
41 |
42 | .breadcrumb-move {
43 | transition: all .5s;
44 | }
45 |
46 | .breadcrumb-leave-active {
47 | position: absolute;
48 | }
49 |
--------------------------------------------------------------------------------
/src/styles/variables.scss:
--------------------------------------------------------------------------------
1 | // sidebar
2 | $menuText:#bfcbd9;
3 | $menuActiveText:#409EFF;
4 | $subMenuActiveText:#f4f4f5; //https://github.com/ElemeFE/element/issues/12951
5 |
6 | $menuBg:#304156;
7 | $menuHover:#263445;
8 |
9 | $subMenuBg:#1f2d3d;
10 | $subMenuHover:#001528;
11 |
12 | $sideBarWidth: 210px;
13 |
14 | // the :export directive is the magic sauce for webpack
15 | // https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass
16 | :export {
17 | menuText: $menuText;
18 | menuActiveText: $menuActiveText;
19 | subMenuActiveText: $subMenuActiveText;
20 | menuBg: $menuBg;
21 | menuHover: $menuHover;
22 | subMenuBg: $subMenuBg;
23 | subMenuHover: $subMenuHover;
24 | sideBarWidth: $sideBarWidth;
25 | }
26 |
--------------------------------------------------------------------------------
/src/views/404/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
22 |
23 |
24 | OOPS!
25 |
26 |
27 | 版权所有
28 |
华尔街见闻
33 |
34 |
35 | {{ message }}
36 |
37 |
38 | 请检查您输入的网址是否正确,请点击以下按钮返回主页或者发送错误报告
39 |
40 |
返回首页
41 |
42 |
43 |
44 |
45 |
46 |
56 |
57 |
251 |
--------------------------------------------------------------------------------
/src/views/homepage/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
10 |
24 |
--------------------------------------------------------------------------------
/src/views/login/index.vue:
--------------------------------------------------------------------------------
1 | /* 登录 */
2 |
3 |
4 |
12 |
13 | 欢迎登录后台管理系统
14 |
15 |
16 |
17 |
24 | {{ item.name }}
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
40 |
41 |
42 |
43 |
44 |
45 | {return}"
53 | @copy.native.capture.prevent="() => {return}"
54 | @cut.native.capture.prevent="() => {return}"
55 | @keyup.enter.native="handleLogin"
56 | />
57 |
58 |
59 | 忘记密码?
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
75 |
76 |
77 |
78 |
79 |
80 |
90 | {{ word }}
91 |
92 |
93 |
94 |
100 | 登 录
101 |
102 |
103 |
104 |
109 | 立 即 注 册
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
288 |
289 |
293 |
--------------------------------------------------------------------------------
/src/views/login/register.vue:
--------------------------------------------------------------------------------
1 | /* 注册 */
2 |
3 |
4 |
5 |
13 |
14 | 欢迎登录后台管理系统
15 |
16 |
17 |
22 |
23 |
29 |
30 |
31 |
38 | {{ word }}
39 |
40 |
41 |
47 |
48 |
49 |
56 |
57 |
58 |
59 | 我已阅读并接受
60 |
61 |
62 | 《后台管理系统用户协议》
63 |
64 |
65 |
66 |
73 | 立 即 注 册
74 |
75 |
76 |
77 | 已有账号?
78 | 立即登录
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
255 |
256 |
260 |
--------------------------------------------------------------------------------
/src/views/login/resetPsw.vue:
--------------------------------------------------------------------------------
1 | /* 重置密码 */
2 |
3 |
4 |
5 |
13 |
14 | 欢迎登录后台管理系统
15 |
16 |
17 |
22 |
23 |
29 |
30 |
31 |
38 | {{ word }}
39 |
40 |
41 |
47 |
48 |
49 |
56 |
57 |
58 |
64 | 重 置
65 |
66 |
67 |
68 | 立即登录
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
224 |
225 |
263 |
264 |
343 |
--------------------------------------------------------------------------------
/src/views/redirect/index.vue:
--------------------------------------------------------------------------------
1 |
13 |
--------------------------------------------------------------------------------
/src/views/table/demo.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
13 |
20 |
28 |
34 |
35 | 修改
36 | 删除
37 |
38 |
39 |
40 |
41 |
42 | 您暂时没有查看的权限
43 |
44 |
45 |
46 |
59 |
60 |
61 |
121 |
127 |
--------------------------------------------------------------------------------
/src/views/table/demoTest.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | I'm the demoTest
4 |
5 |
6 |
11 |
--------------------------------------------------------------------------------
/vue.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 |
3 | function resolve(dir) {
4 | return path.join(__dirname, dir)
5 | }
6 |
7 | module.exports = {
8 | publicPath: '/',
9 | outputDir: 'dist', // 输出文件目录
10 | assetsDir: 'assets', // 静态资源文件夹
11 | productionSourceMap: false,
12 | devServer: {
13 | port: 9566, // 端口号
14 | open: true,
15 | proxy: null // 设置代理
16 | },
17 | css: {
18 | loaderOptions: {
19 | sass: { // 如果用的是less就改成less
20 | javascriptEnabled: true
21 | },
22 | postcss: {
23 | plugins: [
24 | require('autoprefixer')({}),
25 | require('postcss-plugin-px2rem')({
26 | rootValue: 54, // 换算基数,默认100,自行根据效果调整。
27 | mediaQuery: false, // (布尔值)允许在媒体查询中转换px。
28 | minPixelValue: 3 // 设置要替换的最小像素值默认0,这里表示大于3px会被转rem。
29 | })
30 | ]
31 | }
32 | }
33 | },
34 | chainWebpack: config => {
35 | config.module
36 | .rule('css')
37 | .test(/\.css$/)
38 | .oneOf('vue')
39 | .resourceQuery(/\?vue/)
40 | .use('px2rem')
41 | .loader('px2rem-loader')
42 | .options({
43 | remUnit: 54
44 | })
45 |
46 | config.module
47 | .rule('svg')
48 | .exclude.add(resolve('src/icons'))
49 | .end()
50 |
51 | config.module
52 | .rule('icons')
53 | .test(/\.svg$/)
54 | .include.add(resolve('src/icons'))
55 | .end()
56 | .use('svg-sprite-loader')
57 | .loader('svg-sprite-loader')
58 | .options({
59 | symbolId: 'icon-[name]'
60 | })
61 |
62 | config.entry.app = ['babel-polyfill', './src/main.js']
63 | }
64 | }
65 |
66 |
--------------------------------------------------------------------------------