update to 2.0

This commit is contained in:
zhigang.li 2018-05-18 18:45:20 +08:00
parent 8f65158bc3
commit b14e85b17b
137 changed files with 13277 additions and 9286 deletions

10
.babelrc Executable file → Normal file
View File

@ -1,5 +1,5 @@
{
"presets": ["es2015", "stage-3"],
"plugins": ["transform-runtime"],
"comments": false
}
{
"presets": [
"@vue/app"
]
}

3
.editorconfig Executable file → Normal file
View File

@ -1,8 +1,9 @@
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 4
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

View File

@ -1,4 +0,0 @@
src/vendors
src/libs/table2excel.js
build
src/views/my-components/text-editor/tinymce

14
.eslintrc.js Normal file
View File

@ -0,0 +1,14 @@
module.exports = {
root: true,
'extends': [
'plugin:vue/essential',
'@vue/standard'
],
rules: {
// allow async-await
'generator-star-spacing': 'off',
// allow debugger during development
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'vue/no-parsing-error': [2, { 'x-invalid-end-tag': false }]
}
}

View File

@ -1,25 +0,0 @@
{
"extends": "standard",
"installedESLint": true,
"root": true,
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module"
},
"env": {
"browser": true,
"node": true
},
"plugins": [ "html", "standard" ],
"rules": {
"indent": ["error", 4, { "SwitchCase": 1 }],
"quotes": ["error", "single"],
"semi": ["error", "always"],
"no-console": ["error"],
"no-empty": 2,
"no-eq-null": 2,
"no-new": 0,
"no-fallthrough": 0,
"no-unreachable": 0
}
}

33
.gitignore vendored Executable file → Normal file
View File

@ -1,9 +1,24 @@
npm-debug.log
node_modules
.project
.vscode
.history
.DS_Store
\.settings/
src/config/env.js
dist
.DS_Store
node_modules
/dist
/tests/e2e/videos/
/tests/e2e/screenshots/
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw*

5
.postcssrc.js Normal file
View File

@ -0,0 +1,5 @@
module.exports = {
plugins: {
autoprefixer: {}
}
}

View File

@ -1,5 +1,5 @@
language: node_js
node_js:
- "6"
script:
- npm run test
node_js: stable
script: npm run lint
notifications:
email: false

0
LICENSE Executable file → Normal file
View File

View File

@ -1,71 +0,0 @@
<p align="center">
<a href="https://www.iviewui.com">
<img width="200" src="https://file.iviewui.com/logo.svg">
</a>
</p>
# iView-Admin template
[![](https://img.shields.io/travis/iview/iview-admin.svg?style=flat-square)](https://travis-ci.org/iview/iview-admin)
[![vue](https://img.shields.io/badge/vue-2.5.2-brightgreen.svg?style=flat-square)](https://github.com/vuejs/vue)
[![iview ui](https://img.shields.io/badge/iview-2.7.2-brightgreen.svg?style=flat-square)](https://github.com/iview/iview)
## 当前版本v1.0.0
`注:这是简化之后的模板,去除了所有拓展功能,只留下了基础框架。`
## Install
```bush
// install dependencies
npm install
```
## Run
### Development
```bush
npm run dev
```
### Production(Build)
```bush
npm run build
```
## 文件结构
```shell
.
├── build  项目构建配置
└── src
   ├── images  图片文件
   ├── libs  工具方法
   ├── router  路由配置
   ├── store  状态管理
   ├── styles  样式文件
├── template 模板文件
├── vendors 公共库文件
└── views
├── error-page 错误页面
├── group 带二级目录的页面
│   ├── page1 二级页面1
│   ├── page2 二级页面2
├── home 首页
├── main-components 主组件
├── page 一级目录页面
```
## Links
- [TalkingData](https://github.com/TalkingData)
- [iView](https://github.com/iview/iview)
- [Vue](https://github.com/vuejs/vue)
- [Webpack](https://github.com/webpack/webpack)
## 效果展示
- 首页
![image](https://github.com/iview/iview-admin/raw/template/pic/home.png)
- 单页
![image](https://github.com/iview/iview-admin/raw/template/pic/page.png)
## License
[MIT](http://opensource.org/licenses/MIT)
Copyright (c) 2016-present, iView

View File

@ -1,6 +0,0 @@
import Env from './env';
let config = {
env: Env
};
export default config;

View File

@ -1 +0,0 @@
export default "production";

View File

@ -1,95 +0,0 @@
const path = require('path');
const os = require('os');
const webpack = require('webpack');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const HappyPack = require('happypack');
var happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });
function resolve (dir) {
return path.join(__dirname, dir);
}
module.exports = {
entry: {
main: '@/main',
'vender-base': '@/vendors/vendors.base.js',
'vender-exten': '@/vendors/vendors.exten.js'
},
output: {
path: path.resolve(__dirname, '../dist/dist')
},
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader',
options: {
loaders: {
less: ExtractTextPlugin.extract({
use: ['css-loader?minimize', 'autoprefixer-loader', 'less-loader'],
fallback: 'vue-style-loader'
}),
css: ExtractTextPlugin.extract({
use: ['css-loader', 'autoprefixer-loader'],
fallback: 'vue-style-loader'
})
}
}
},
{
test: /iview\/.*?js$/,
loader: 'babel-loader'
},
{
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules/
},
{
test: /\.js[x]?$/,
include: [resolve('src')],
exclude: /node_modules/,
loader: 'happypack/loader?id=happybabel'
},
{
test: /\.css$/,
use: ExtractTextPlugin.extract({
use: ['css-loader?minimize', 'autoprefixer-loader'],
fallback: 'style-loader'
})
},
{
test: /\.less$/,
use: ExtractTextPlugin.extract({
use: ['css-hot-loader', 'autoprefixer-loader', 'less-loader'],
fallback: 'style-loader'
}),
},
{
test: /\.(gif|jpg|png|woff|svg|eot|ttf)\??.*$/,
loader: 'url-loader?limit=1024'
},
{
test: /\.(html|tpl)$/,
loader: 'html-loader'
}
]
},
plugins: [
new HappyPack({
id: 'happybabel',
loaders: ['babel-loader'],
threadPool: happyThreadPool,
cache: true,
verbose: true
})
],
resolve: {
extensions: ['.js', '.vue'],
alias: {
'vue': 'vue/dist/vue.esm.js',
'@': resolve('../src'),
}
}
};

View File

@ -1,38 +0,0 @@
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const merge = require('webpack-merge');
const webpackBaseConfig = require('./webpack.base.config.js');
const fs = require('fs');
const package = require('../package.json');
fs.open('./env.js', 'w', function(err, fd) {
const buf = 'export default "development";';
fs.write(fd, buf, 0, buf.length, 0, function(err, written, buffer) {});
});
module.exports = merge(webpackBaseConfig, {
devtool: '#source-map',
output: {
publicPath: '/dist/',
filename: '[name].js',
chunkFilename: '[name].chunk.js'
},
plugins: [
new ExtractTextPlugin({
filename: '[name].css',
allChunks: true
}),
new webpack.optimize.CommonsChunkPlugin({
name: ['vender-exten', 'vender-base'],
minChunks: Infinity
}),
new HtmlWebpackPlugin({
title: 'iView admin v' + package.version,
filename: '../index.html',
template: './src/template/index.ejs',
inject: false
})
]
});

View File

@ -1,53 +0,0 @@
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const cleanWebpackPlugin = require('clean-webpack-plugin');
const merge = require('webpack-merge');
const webpackBaseConfig = require('./webpack.base.config.js');
const fs = require('fs');
const path = require('path');
const package = require('../package.json');
fs.open('./build/env.js', 'w', function(err, fd) {
const buf = 'export default "production";';
fs.write(fd, buf, 0, buf.length, 0, function(err, written, buffer) {});
});
module.exports = merge(webpackBaseConfig, {
output: {
publicPath: 'https://iview.github.io/iview-admin/dist/',
filename: '[name].[hash].js',
chunkFilename: '[name].[hash].chunk.js'
},
plugins: [
new cleanWebpackPlugin(['dist/*'], {
root: path.resolve(__dirname, '../')
}),
new ExtractTextPlugin({
filename: '[name].[hash].css',
allChunks: true
}),
new webpack.optimize.CommonsChunkPlugin({
name: ['vender-exten', 'vender-base'],
minChunks: Infinity
}),
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: '"production"'
}
}),
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false
}
}),
new HtmlWebpackPlugin({
title: 'iView admin v' + package.version,
favicon: './td_icon.ico',
filename: '../index.html',
template: './src/template/index.ejs',
inject: false
})
]
});

1
config/env.js Normal file
View File

@ -0,0 +1 @@
export default 'development'

6
config/url.js Normal file
View File

@ -0,0 +1,6 @@
import env from './env'
const DEV_URL = 'https://www.easy-mock.com/mock/5add9213ce4d0e69998a6f51/iview-admin/'
const PRO_URL = 'https://produce.com'
export default env === 'development' ? DEV_URL : PRO_URL

3
cypress.json Normal file
View File

@ -0,0 +1,3 @@
{
"pluginsFile": "tests/e2e/plugins/index.js"
}

1
env.js
View File

@ -1 +0,0 @@
export default "development";

View File

@ -1,19 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<title>iView admin</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=0">
<link rel="stylesheet" href="/dist/main.css">
<link rel="icon" href="./td_icon.ico" type="image/x-icon" />
</head>
<body>
<div id="app"></div>
<script type="text/javascript" src="/dist/vender-base.js"></script>
<script type="text/javascript" src="/dist/vender-exten.js"></script>
<script type="text/javascript" src="/dist/main.js"></script>
</body>
</html>

16154
package-lock.json generated Executable file → Normal file

File diff suppressed because it is too large Load Diff

117
package.json Executable file → Normal file
View File

@ -1,70 +1,63 @@
{
"name": "iview-admin-template",
"version": "1.0.0",
"description": "a management template bases on iview",
"main": "index.js",
"name": "iview-admin",
"version": "2.0.0",
"author": "Lison<lison.modern@gmail.com>",
"private": false,
"scripts": {
"init": "webpack --progress --config build/webpack.dev.config.js",
"dev": "webpack-dev-server --content-base ./ --open --inline --hot --compress --config build/webpack.dev.config.js",
"build": "webpack --progress --hide-modules --config build/webpack.prod.config.js",
"lint": "eslint --fix --ext .js,.vue src",
"test": "npm run lint"
"dev": "vue-cli-service serve --open",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"test:unit": "vue-cli-service test:unit",
"test:e2e": "vue-cli-service test:e2e"
},
"repository": {
"type": "git",
"url": "https://github.com/iview/iview-admin.git"
},
"author": "",
"license": "MIT",
"dependencies": {
"axios": "^0.15.3",
"iview": "^2.7.2",
"js-cookie": "^2.1.3",
"vue": "^2.5.2",
"vue-router": "^2.8.1",
"vuex": "^2.5.0"
"axios": "^0.18.0",
"clipboard": "^2.0.0",
"countup": "^1.8.2",
"cropperjs": "^1.3.5",
"echarts": "^4.0.4",
"html2canvas": "^1.0.0-alpha.12",
"iview": "^2.14.0-rc.1",
"iview-area": "^1.5.17",
"js-cookie": "^2.2.0",
"sortablejs": "^1.7.0",
"tinymce": "^4.7.11",
"vue": "^2.5.10",
"vue-i18n": "^7.6.0",
"vue-router": "^3.0.1",
"vuex": "^3.0.1"
},
"devDependencies": {
"autoprefixer-loader": "^2.0.0",
"babel": "^6.23.0",
"babel-core": "^6.23.1",
"babel-eslint": "^8.0.1",
"babel-loader": "^6.4.1",
"babel-plugin-transform-runtime": "^6.12.0",
"babel-preset-env": "^1.6.0",
"babel-preset-es2015": "^6.9.0",
"babel-preset-stage-3": "^6.24.1",
"babel-runtime": "^6.11.6",
"clean-webpack-plugin": "^0.1.17",
"copy-webpack-plugin": "^4.2.0",
"css-hot-loader": "^1.3.3",
"css-loader": "^0.23.1",
"eslint": "^3.19.0",
"eslint-config-google": "^0.9.1",
"eslint-config-standard": "^10.2.1",
"eslint-plugin-html": "^1.7.0",
"eslint-plugin-import": "^2.7.0",
"eslint-plugin-node": "^5.2.0",
"eslint-plugin-promise": "^3.6.0",
"eslint-plugin-standard": "^3.0.1",
"extract-text-webpack-plugin": "^2.0.0",
"file-loader": "^0.8.5",
"happypack": "^4.0.0",
"html-loader": "^0.3.0",
"html-webpack-plugin": "^2.28.0",
"less": "^2.7.1",
"less-loader": "^2.2.3",
"semver": "^5.4.1",
"style-loader": "^0.13.1",
"unsupported": "^1.1.0",
"url-loader": "^0.5.7",
"vue-hot-reload-api": "^1.3.3",
"vue-html-loader": "^1.2.3",
"vue-loader": "^11.0.0",
"vue-style-loader": "^1.0.0",
"vue-template-compiler": "^2.5.2",
"webpack": "^2.2.1",
"webpack-dev-server": "^2.9.2",
"webpack-merge": "^3.0.0"
"@vue/cli-plugin-babel": "^3.0.0-beta.10",
"@vue/cli-plugin-e2e-cypress": "^3.0.0-beta.10",
"@vue/cli-plugin-eslint": "^3.0.0-beta.10",
"@vue/cli-plugin-unit-mocha": "^3.0.0-beta.10",
"@vue/cli-service": "^3.0.0-beta.10",
"@vue/eslint-config-standard": "^3.0.0-beta.10",
"@vue/test-utils": "^1.0.0-beta.10",
"chai": "^4.1.2",
"less": "^2.7.3",
"less-loader": "^4.0.5",
"lint-staged": "^6.0.0",
"mockjs": "^1.0.1-beta3",
"vue-template-compiler": "^2.5.13"
},
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 8"
],
"gitHooks": {
"pre-commit": "lint-staged"
},
"lint-staged": {
"*.js": [
"vue-cli-service lint",
"git add"
],
"*.vue": [
"vue-cli-service lint",
"git add"
]
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 203 KiB

0
td_icon.ico → public/favicon.ico Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

17
public/index.html Normal file
View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title>iview-admin</title>
</head>
<body>
<noscript>
<strong>We're sorry but iview-admin doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

11
src/api/routers.js Normal file
View File

@ -0,0 +1,11 @@
import axios from '@/libs/api.request'
export const getRouterReq = (access) => {
return axios.request({
url: 'get_router',
params: {
access
},
method: 'get'
})
}

30
src/api/user.js Normal file
View File

@ -0,0 +1,30 @@
import axios from '@/libs/api.request'
export const login = ({ userName, password }) => {
const data = {
userName,
password
}
return axios.request({
url: 'login',
data,
method: 'post'
})
}
export const getUserInfo = (token) => {
return axios.request({
url: 'get_info',
params: {
token
},
method: 'get'
})
}
export const logout = (token) => {
return axios.request({
url: 'logout',
method: 'post'
})
}

37
src/app.vue Executable file → Normal file
View File

@ -1,26 +1,27 @@
<template>
<div id="main" class="app-main">
<router-view></router-view>
</div>
<div id="app">
<router-view/>
</div>
</template>
<script>
export default {
mounted () {
},
beforeDestroy () {
},
methods: {
}
};
export default {
name: 'App'
}
</script>
<style>
.app-main{
width: 100%;
height: 100%;
<style lang="less">
.size{
width: 100%;
height: 100%;
}
html,body{
.size;
overflow: hidden;
margin: 0;
padding: 0;
}
#app {
.size;
}
</style>

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 31 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 26 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 33 KiB

View File

Before

Width:  |  Height:  |  Size: 726 B

After

Width:  |  Height:  |  Size: 726 B

0
src/images/logo.jpg → src/assets/images/logo.jpg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -0,0 +1,8 @@
.no-select{
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}

View File

@ -0,0 +1,3 @@
export const showTitle = (item, vm) => {
return vm.$config.useI18n ? vm.$t(item.name) : ((item.meta && item.meta.title) || item.name)
}

View File

@ -0,0 +1,2 @@
import loginForm from './login-form.vue'
export default loginForm

View File

@ -0,0 +1,72 @@
<template>
<Form ref="loginForm" :model="form" :rules="rules">
<FormItem prop="userName">
<Input v-model="form.userName" placeholder="请输入用户名">
<span slot="prepend">
<Icon :size="16" type="person"></Icon>
</span>
</Input>
</FormItem>
<FormItem prop="password">
<Input type="password" v-model="form.password" placeholder="请输入密码">
<span slot="prepend">
<Icon :size="14" type="locked"></Icon>
</span>
</Input>
</FormItem>
<FormItem>
<Button @click="handleSubmit" type="primary" long>登录</Button>
</FormItem>
</Form>
</template>
<script>
export default {
name: 'loginForm',
props: {
userNameRules: {
type: Array,
default: () => {
return [
{ required: true, message: '账号不能为空', trigger: 'blur' }
]
}
},
passwordRules: {
type: Array,
default: () => {
return [
{ required: true, message: '密码不能为空', trigger: 'blur' }
]
}
}
},
data () {
return {
form: {
userName: 'super_admin',
password: ''
}
}
},
computed: {
rules () {
return {
userName: this.userNameRules,
password: this.passwordRules
}
}
},
methods: {
handleSubmit () {
this.$refs.loginForm.validate((valid) => {
if (valid) {
this.$emit('on-success-valid', {
userName: this.form.userName,
password: this.form.password
})
}
})
}
}
}
</script>

View File

@ -0,0 +1,2 @@
import parentView from './parent-view.vue'
export default parentView

View File

@ -0,0 +1,8 @@
<template>
<router-view/>
</template>
<script>
export default {
name: 'parentView'
}
</script>

12
src/config/index.js Normal file
View File

@ -0,0 +1,12 @@
export default {
/**
* @description token在Cookie中存储的天数默认1天
*/
cookieExpires: 1,
/**
* @description 是否使用国际化默认为false
* 如果不使用则需要在路由中给需要在菜单中展示的路由设置meta: {title: 'xxx'}
* 用来在菜单中显示文字
*/
useI18n: false
}

3
src/libs/api.request.js Normal file
View File

@ -0,0 +1,3 @@
import HttpRequest from '@/libs/axios'
const axios = new HttpRequest()
export default axios

90
src/libs/axios.js Normal file
View File

@ -0,0 +1,90 @@
import Axios from 'axios'
import baseURL from '_conf/url'
import { Message } from 'iview'
import Cookies from 'js-cookie'
import { TOKEN_KEY } from '@/libs/util'
class httpRequest {
constructor () {
this.options = {
method: '',
url: ''
}
// 存储请求队列
this.queue = {}
}
// 销毁请求实例
destroy (url) {
delete this.queue[url]
const queue = Object.keys(this.queue)
return queue.length
}
// 请求拦截
interceptors (instance, url) {
// 添加请求拦截器
instance.interceptors.request.use(config => {
if (!config.url.includes('/users')) {
config.headers['x-access-token'] = Cookies.get(TOKEN_KEY)
}
// Spin.show()
// 在发送请求之前做些什么
return config
}, error => {
// 对请求错误做些什么
return Promise.reject(error)
})
// 添加响应拦截器
instance.interceptors.response.use((res) => {
let { data } = res
const is = this.destroy(url)
if (!is) {
setTimeout(() => {
// Spin.hide()
}, 500)
}
if (!(data instanceof Blob)) {
if (data.code !== 200) {
// 后端服务在个别情况下回报201待确认
if (data.code === 401) {
Cookies.remove(TOKEN_KEY)
window.location.href = '/#/login'
Message.error('未登录,或登录失效,请登录')
} else {
if (data.msg) Message.error(data.msg)
}
return false
}
}
return data
}, (error) => {
Message.error('服务内部错误')
// 对响应错误做点什么
return Promise.reject(error)
})
}
// 创建实例
create () {
let conf = {
baseURL: baseURL,
// timeout: 2000,
headers: {
'Content-Type': 'application/json; charset=utf-8',
'X-URL-PATH': location.pathname
}
}
return Axios.create(conf)
}
// 合并请求实例
mergeReqest (instances = []) {
//
}
// 请求实例
request (options) {
var instance = this.create()
this.interceptors(instance, options.url)
options = Object.assign({}, options)
this.queue[options.url] = instance
return instance(options)
}
}
export default httpRequest

38
src/libs/tools.js Normal file
View File

@ -0,0 +1,38 @@
export const forEach = (arr, fn) => {
if (!arr.length || !fn) return
let i = -1
let len = arr.length
while (++i < len) {
let item = arr[i]
fn(item, i, arr)
}
}
export const getCommonString = (arr1, arr2) => {
let len = Math.min(arr1.length, arr2.length)
let i = -1
let res = []
while (++i < len) {
const item = arr2[i]
if (arr1.indexOf() > -1) res.push(item)
}
return res
}
/**
* @param {Array} arr1
* @param {Array} arr2
* @description 得到两个数组的交集, 两个数组的元素为数值或字符串
*/
export const getIntersection = (arr1, arr2) => {
return Array.from(new Set([...arr1, ...arr2]))
}
/**
* @param {Array} target 目标数组
* @param {Array} arr 需要查询的数组
* @description 判断要查询的数组是否至少有一个元素包含在目标数组中
*/
export const hasOneOf = (target, arr) => {
return target.some(_ => arr.indexOf(_) > -1)
}

420
src/libs/util.js Executable file → Normal file
View File

@ -1,271 +1,175 @@
import axios from 'axios';
import env from '../../build/env';
import semver from 'semver';
import packjson from '../../package.json';
import Cookies from 'js-cookie'
// cookie保存的天数
import config from '@/config'
import { forEach, hasOneOf } from '@/libs/tools'
let util = {
export const TOKEN_KEY = 'token'
};
util.title = function (title) {
title = title || 'iView admin';
window.document.title = title;
};
export const setToken = (token) => {
Cookies.set(TOKEN_KEY, token, {expires: config.cookieExpires || 1})
}
const ajaxUrl = env === 'development'
? 'http://127.0.0.1:8888'
: env === 'production'
? 'https://www.url.com'
: 'https://debug.url.com';
export const getToken = () => {
const token = Cookies.get(TOKEN_KEY)
if (token) return token
else return false
}
util.ajax = axios.create({
baseURL: ajaxUrl,
timeout: 30000
});
export const hasChild = (item) => {
return item.children && item.children.length !== 0
}
util.inOf = function (arr, targetArr) {
let res = true;
arr.map(item => {
if (targetArr.indexOf(item) < 0) {
res = false;
}
});
return res;
};
const showThisMenuEle = (item, access) => {
if (item.meta && item.meta.access && item.meta.access.length) {
if (hasOneOf(item.meta.access, access)) return true
else return false
} else return true
}
/**
* @param {Array} list 通过路由列表得到菜单列表
* @returns {Array}
*/
export const getMenuByRouter = (list, access) => {
let res = []
forEach(list, item => {
if (item.meta && !item.meta.hideInMenu) {
let obj = {
icon: (item.meta && item.meta.icon) || '',
name: item.name,
meta: item.meta
}
if (hasChild(item) && showThisMenuEle(item, access)) {
obj.children = getMenuByRouter(item.children, access)
}
if (showThisMenuEle(item, access)) res.push(obj)
}
})
return res
}
util.oneOf = function (ele, targetArr) {
if (targetArr.indexOf(ele) >= 0) {
return true;
/**
* @param {Array} routeMetched 当前路由metched
* @returns {Array}
*/
export const getBreadCrumbList = (routeMetched) => {
let res = routeMetched.map(item => {
let obj = {
icon: (item.meta && item.meta.icon) || '',
name: item.name,
meta: item.meta
}
return obj
})
res = res.filter(item => {
return !item.meta.hideInMenu
})
return [{
name: 'home',
to: '/home'
}, ...res]
}
export const showTitle = (item, vm) => vm.$config.useI18n ? vm.$t(item.name) : ((item.meta && item.meta.title) || item.name)
/**
* @description 本地存储和获取标签导航列表
*/
export const setTagNavListInLocalstorage = list => {
localStorage.tagNaveList = JSON.stringify(list)
}
/**
* @returns {Array} 其中的每个元素只包含路由原信息中的name, path, meta三项
*/
export const getTagNavListFromLocalstorage = () => {
const list = localStorage.tagNaveList
return list ? JSON.parse(list) : []
}
/**
* @param {Array} routers 路由列表数组
* @description 用于找到路由列表中name为home的对象
*/
export const getHomeRoute = routers => {
let i = -1
let len = routers.length
let homeRoute = {}
while (++i < len) {
let item = routers[i]
if (item.children && item.children.length) {
let res = getHomeRoute(item.children)
if (res.name) return res
} else {
return false;
if (item.name === 'home') homeRoute = item
}
};
}
return homeRoute
}
util.showThisRoute = function (itAccess, currentAccess) {
if (typeof itAccess === 'object' && itAccess.isArray()) {
return util.oneOf(currentAccess, itAccess);
} else {
return itAccess === currentAccess;
}
};
/**
* @param {*} list 现有标签导航列表
* @param {*} newRoute 新添加的路由原信息对象
* @description 如果该newRoute已经存在则不再添加
*/
export const getNewTagList = (list, newRoute) => {
const { name, path, meta } = newRoute
let newList = [...list]
if (newList.findIndex(item => item.name === name) >= 0) return newList
else newList.push({ name, path, meta })
return newList
}
util.getRouterObjByName = function (routers, name) {
let routerObj = {};
routers.forEach(item => {
if (item.name === 'otherRouter') {
item.children.forEach((child, i) => {
if (child.name === name) {
routerObj = item.children[i];
}
});
/**
* @param {Boolean} status 状态 1 => locked 0 => unlocked
* @description 这里只是为了演示实际应该将锁定状态的设置和获取用接口来实现
*/
export const setLockStatus = (status) => {
localStorage.isLocked = status
}
export const getLockStatus = () => {
return parseInt(localStorage.isLocked)
}
/**
* @param {*} access 用户权限数组 ['super_admin', 'admin']
* @param {*} route 路由列表
*/
const hasAccess = (access, route) => {
if (route.meta && route.meta.access) return hasOneOf(access, route.meta.access)
else return true
}
/**
* @param {*} name 即将跳转的路由name
* @param {*} access 用户权限数组
* @param {*} routes 路由列表
* @description 用户是否可跳转到该页
*/
export const canTurnTo = (name, access, routes) => {
const getHasAccessRouteNames = (list) => {
let res = []
list.forEach(item => {
if (item.children && item.children.length) {
res = [].concat(res, getHasAccessRouteNames(item.children))
} else {
if (item.meta && item.meta.access) {
if (hasAccess(access, item)) res.push(item.name)
} else {
if (item.children.length === 1) {
if (item.children[0].name === name) {
routerObj = item.children[0];
}
} else {
item.children.forEach((child, i) => {
if (child.name === name) {
routerObj = item.children[i];
}
});
}
res.push(item.name)
}
});
return routerObj;
};
}
})
return res
}
const canTurnToNames = getHasAccessRouteNames(routes)
return canTurnToNames.indexOf(name) > -1
}
util.handleTitle = function (vm, item) {
return item.title;
};
util.setCurrentPath = function (vm, name) {
let title = '';
let isOtherRouter = false;
vm.$store.state.app.routers.forEach(item => {
if (item.children.length === 1) {
if (item.children[0].name === name) {
title = util.handleTitle(vm, item);
if (item.name === 'otherRouter') {
isOtherRouter = true;
}
}
} else {
item.children.forEach(child => {
if (child.name === name) {
title = util.handleTitle(vm, child);
if (item.name === 'otherRouter') {
isOtherRouter = true;
}
}
});
}
});
let currentPathArr = [];
if (name === 'home_index') {
currentPathArr = [
{
title: util.handleTitle(vm, util.getRouterObjByName(vm.$store.state.app.routers, 'home_index')),
path: '',
name: 'home_index'
}
];
} else if ((name.indexOf('_index') >= 0 || isOtherRouter) && name !== 'home_index') {
currentPathArr = [
{
title: util.handleTitle(vm, util.getRouterObjByName(vm.$store.state.app.routers, 'home_index')),
path: '/home',
name: 'home_index'
},
{
title: title,
path: '',
name: name
}
];
} else {
let currentPathObj = vm.$store.state.app.routers.filter(item => {
if (item.children.length <= 1) {
return item.children[0].name === name;
} else {
let i = 0;
let childArr = item.children;
let len = childArr.length;
while (i < len) {
if (childArr[i].name === name) {
return true;
}
i++;
}
return false;
}
})[0];
if (currentPathObj.children.length <= 1 && currentPathObj.name === 'home') {
currentPathArr = [
{
title: '首页',
path: '',
name: 'home_index'
}
];
} else if (currentPathObj.children.length <= 1 && currentPathObj.name !== 'home') {
currentPathArr = [
{
title: '首页',
path: '/home',
name: 'home_index'
},
{
title: currentPathObj.title,
path: '',
name: name
}
];
} else {
let childObj = currentPathObj.children.filter((child) => {
return child.name === name;
})[0];
currentPathArr = [
{
title: '首页',
path: '/home',
name: 'home_index'
},
{
title: currentPathObj.title,
path: '',
name: currentPathObj.name
},
{
title: childObj.title,
path: currentPathObj.path + '/' + childObj.path,
name: name
}
];
}
}
vm.$store.commit('setCurrentPath', currentPathArr);
return currentPathArr;
};
util.openNewPage = function (vm, name, argu, query) {
let pageOpenedList = vm.$store.state.app.pageOpenedList;
let openedPageLen = pageOpenedList.length;
let i = 0;
let tagHasOpened = false;
while (i < openedPageLen) {
if (name === pageOpenedList[i].name) { // 页面已经打开
vm.$store.commit('pageOpenedList', {
index: i,
argu: argu,
query: query
});
tagHasOpened = true;
break;
}
i++;
}
if (!tagHasOpened) {
let tag = vm.$store.state.app.tagsList.filter((item) => {
if (item.children) {
return name === item.children[0].name;
} else {
return name === item.name;
}
});
tag = tag[0];
if (tag) {
tag = tag.children ? tag.children[0] : tag;
if (argu) {
tag.argu = argu;
}
if (query) {
tag.query = query;
}
vm.$store.commit('increateTag', tag);
}
}
vm.$store.commit('setCurrentPageName', name);
};
util.toDefaultPage = function (routers, name, route, next) {
let len = routers.length;
let i = 0;
let notHandle = true;
while (i < len) {
if (routers[i].name === name && routers[i].redirect === undefined) {
route.replace({
name: routers[i].children[0].name
});
notHandle = false;
next();
break;
}
i++;
}
if (notHandle) {
next();
}
};
util.fullscreenEvent = function (vm) {
// 权限菜单过滤相关
vm.$store.commit('updateMenulist');
};
util.checkUpdate = function (vm) {
axios.get('https://api.github.com/repos/iview/iview-admin/releases/latest').then(res => {
let version = res.data.tag_name;
vm.$Notice.config({
duration: 0
});
if (semver.lt(packjson.version, version)) {
vm.$Notice.info({
title: 'iview-admin更新啦',
desc: '<p>iView-admin更新到了' + version + '了,去看看有哪些变化吧</p><a style="font-size:13px;" href="https://github.com/iview/iview-admin/releases" target="_blank">前往github查看</a>'
});
}
});
};
export default util;
export const getParams = url => {
const keyValueArr = url.split('?')[1].split('&')
let paramObj = {}
keyValueArr.forEach(item => {
const keyValue = item.split('=')
paramObj[keyValue[0]] = keyValue[1]
})
return paramObj
}

32
src/locale/index.js Normal file
View File

@ -0,0 +1,32 @@
import Vue from 'vue'
import VueI18n from 'vue-i18n'
import customZhCn from './lang/zh-CN'
import customZhTw from './lang/zh-TW'
import customEnUs from './lang/en-US'
import zhCnLocale from 'iview/src/locale/lang/zh-CN'
import enUsLocale from 'iview/src/locale/lang/en-US'
import zhTwLocale from 'iview/src/locale/lang/zh-TW'
Vue.use(VueI18n)
// 自动根据浏览器系统语言设置语言
// const navLang = navigator.language
// const localLang = (navLang === 'zh-CN' || navLang === 'en-US') ? navLang : false
// let lang = window.localStorage.lang || localLang || 'zh-CN'
let lang = 'zh-CN'
Vue.config.lang = lang
Vue.locale = () => {}
const messages = {
'zh-CN': Object.assign(zhCnLocale, customZhCn),
'zh-TW': Object.assign(zhTwLocale, customZhTw),
'en-US': Object.assign(enUsLocale, customEnUs)
}
const i18n = new VueI18n({
locale: lang,
messages
})
export default i18n

8
src/locale/lang/en-US.js Normal file
View File

@ -0,0 +1,8 @@
export default {
components: 'components',
count_to: 'count-to',
multilevel: 'multilevel',
level_1: 'level-1',
level_2: 'level-2',
level_2_1: 'level-2-1'
}

8
src/locale/lang/zh-CN.js Normal file
View File

@ -0,0 +1,8 @@
export default {
components: '组件',
count_to: '数字渐变',
multilevel: '多级菜单',
level_1: 'level-1',
level_2: 'level-2',
level_2_1: 'level-2-1'
}

8
src/locale/lang/zh-TW.js Normal file
View File

@ -0,0 +1,8 @@
export default {
components: '组件',
count_to: '数字渐变',
multilevel: '多级菜单',
level_1: 'level-1',
level_2: 'level-2',
level_2_1: 'level-2-1'
}

40
src/main.js Executable file → Normal file
View File

@ -1,18 +1,28 @@
import Vue from 'vue';
import iView from 'iview';
import {router} from './router/index';
import store from './store';
import App from './app.vue';
import 'iview/dist/styles/iview.css';
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'
import router from './router'
import store from './store'
import iView from 'iview'
import i18n from '@/locale'
import config from '@/config'
import 'iview/dist/styles/iview.css'
import env from '../config/env'
if (env === 'development') require('@/mock')
Vue.use(iView);
Vue.use(iView)
Vue.config.productionTip = false
/**
* @description 全局注册应用配置
*/
Vue.prototype.$config = config
/* eslint-disable no-new */
new Vue({
el: '#app',
router: router,
store: store,
render: h => h(App),
mounted () {
this.$store.commit('updateMenulist');
}
});
el: '#app',
router,
store,
i18n,
render: h => h(App)
})

9
src/mock/index.js Normal file
View File

@ -0,0 +1,9 @@
import Mock from 'mockjs'
import { login, logout, getUserInfo } from './login'
// 登录相关和获取用户信息
Mock.mock(/\/login/, login)
Mock.mock(/\/get_info/, getUserInfo)
Mock.mock(/\/logout/, logout)
export default Mock

43
src/mock/login.js Normal file
View File

@ -0,0 +1,43 @@
import { getParams } from '@/libs/util'
const USER_MAP = {
super_admin: {
name: 'super_admin',
user_id: '1',
access: ['super_admin', 'admin'],
token: 'super_admin',
avator: 'https://file.iviewui.com/dist/a0e88e83800f138b94d2414621bd9704.png'
},
admin: {
name: 'admin',
user_id: '2',
access: ['admin'],
token: 'admin',
avator: 'https://avatars0.githubusercontent.com/u/20942571?s=460&v=4'
}
}
export const login = req => {
req = JSON.parse(req.body)
return {
code: 200,
data: {token: USER_MAP[req.userName].token},
msg: ''
}
}
export const getUserInfo = req => {
const params = getParams(req.url)
return {
code: 200,
data: USER_MAP[params.token],
msg: ''
}
}
export const logout = req => {
return {
code: 200,
data: null,
msg: ''
}
}

50
src/router/base-router.js Normal file
View File

@ -0,0 +1,50 @@
import Main from '@/view/main'
import parentView from '@/components/main/parent-view'
export default [
{
path: '/login',
name: 'login',
meta: {
title: 'Login - 登录'
},
component: () => import('@/view/login/login.vue')
},
{
path: '/',
name: 'index',
// redirect: '/home',
component: Main,
children: [
{
path: 'home',
name: 'home',
component: () => import('@/view/single-page/home')
},
{
path: 'multilevel',
name: 'multilevel',
component: parentView,
children: [
{
path: 'level_1',
name: 'level_1',
component: () => import('@/view/multilevel/level-1.vue')
},
{
path: 'level_2',
name: 'level_2',
component: parentView,
children: [
{
path: 'level_2_1',
name: 'level_2_1',
component: () => import('@/view/multilevel/level-2/level-2-1.vue')
}
]
}
]
}
]
}
]

View File

@ -1,26 +1,57 @@
import Vue from 'vue';
import iView from 'iview';
import Util from '../libs/util';
import VueRouter from 'vue-router';
import {routers} from './router';
import Vue from 'vue'
import Router from 'vue-router'
import routes from './routers'
import store from '@/store'
import iView from 'iview'
import { getToken, getLockStatus, canTurnTo } from '@/libs/util'
Vue.use(VueRouter);
// 路由配置
const RouterConfig = {
// mode: 'history',
routes: routers
};
export const router = new VueRouter(RouterConfig);
Vue.use(Router)
const router = new Router({
routes
})
const LOCK_PAGE_NAME = 'lock_page'
const LOGIN_PAGE_NAME = 'login'
const IS_LOCKED = getLockStatus()
router.beforeEach((to, from, next) => {
iView.LoadingBar.start();
Util.title(to.meta.title);
next();
});
iView.LoadingBar.start()
if (IS_LOCKED && to.name !== LOCK_PAGE_NAME) {
// 当前是锁定状态并且用户要跳转到的页面不是解锁页面
next({
replace: true, // 重定向到解锁页面
name: LOCK_PAGE_NAME
})
} else if (IS_LOCKED && to.name === LOCK_PAGE_NAME) {
// 当前未锁定且用户要跳转到的页面是解锁页面
next(false) // 不做跳转
} else {
const token = getToken()
if (!token && to.name !== LOGIN_PAGE_NAME) {
// 未登录且要跳转的页面不是登录页
next({
name: LOGIN_PAGE_NAME // 跳转到登录页
})
} else if (!token && to.name === LOGIN_PAGE_NAME) {
// 未登陆且要跳转的页面是登录页
next() // 跳转
} else if (token && to.name === LOGIN_PAGE_NAME) {
// 已登录且要跳转的页面是登录页
next({
name: 'home' // 跳转到home页
})
} else {
store.dispatch('getUserInfo').then(user => {
// 拉取用户信息通过用户权限和跳转的页面的name来判断是否有权限访问;access必须是一个数组['super_admin'] ['super_admin', 'admin']
if (canTurnTo(to.name, user.access, routes)) next() // 有权限,可访问
else next({ replace: true, name: 'error_401' }) // 无权限重定向到401页面
})
}
}
})
router.afterEach((to) => {
iView.LoadingBar.finish();
window.scrollTo(0, 0);
});
router.afterEach(to => {
iView.LoadingBar.finish()
window.scrollTo(0, 0)
})
export default router

View File

@ -1,95 +0,0 @@
import Main from '@/views/Main.vue';
// 不作为Main组件的子页面展示的页面单独写如下
export const loginRouter = {
path: '/login',
name: 'login',
meta: {
title: 'Login - 登录'
},
component: resolve => { require(['@/views/login.vue'], resolve); }
};
export const page404 = {
path: '/*',
name: 'error-404',
meta: {
title: '404-页面不存在'
},
component: resolve => { require(['@/views/error-page/404.vue'], resolve); }
};
export const page403 = {
path: '/403',
meta: {
title: '403-权限不足'
},
name: 'error-403',
component: resolve => { require(['@//views/error-page/403.vue'], resolve); }
};
export const page500 = {
path: '/500',
meta: {
title: '500-服务端错误'
},
name: 'error-500',
component: resolve => { require(['@/views/error-page/500.vue'], resolve); }
};
// 作为Main组件的子页面展示但是不在左侧菜单显示的路由写在otherRouter里
export const otherRouter = {
path: '/',
name: 'otherRouter',
component: Main,
children: [
{ path: 'home', title: {i18n: 'home'}, name: 'home_index', component: resolve => { require(['@/views/home/home.vue'], resolve); } }
]
};
// 作为Main组件的子页面展示并且在左侧菜单显示的路由写在appRouter里
export const appRouter = [
{
path: '/group',
icon: 'ios-folder',
name: 'group',
title: 'Group',
component: Main,
children: [
{
path: 'page1',
icon: 'ios-paper-outline',
name: 'page1',
title: 'Page1',
component: resolve => { require(['@/views/group/page1/page1.vue'], resolve); }
},
{
path: 'page2',
icon: 'ios-list-outline',
name: 'page2',
title: 'Page2',
component: resolve => { require(['@/views/group/page2/page2.vue'], resolve); }
}
]
},
{
path: '/page',
icon: 'ios-paper',
title: 'Page',
name: 'page',
component: Main,
children: [
{ path: 'index', title: 'Page', name: 'page_index', component: resolve => { require(['@/views/page/page.vue'], resolve); } }
]
}
];
// 所有上面定义的路由都要写在下面的routers里
export const routers = [
loginRouter,
otherRouter,
...appRouter,
page500,
page403,
page404
];

142
src/router/routers.js Normal file
View File

@ -0,0 +1,142 @@
import Main from '@/view/main'
import parentView from '@/components/parent-view'
export default [
{
path: '/login',
name: 'login',
meta: {
title: 'Login - 登录',
hideInMenu: true
},
component: () => import('@/view/login/login.vue')
},
{
path: '/',
name: 'index',
redirect: '/home',
component: Main,
meta: {
hideInMenu: true,
notCache: true
},
children: [
{
path: 'home',
name: 'home',
meta: {
hideInMenu: true,
notCache: true
},
component: () => import('@/view/single-page/home')
}
]
},
{
path: '/components',
name: 'components',
meta: {
icon: 'social-buffer',
title: '组件'
},
component: Main,
children: [
{
path: 'count_to',
name: 'count_to',
meta: {
icon: 'arrow-graph-up-right',
title: '数字渐变'
},
component: () => import('@/view/components/count-to/count-to.vue')
}
]
},
{
path: '/multilevel',
name: 'multilevel',
meta: {
icon: 'arrow-graph-up-right',
title: '多级菜单'
},
component: Main,
children: [
{
path: 'level_2_1',
name: 'level_2_1',
meta: {
icon: 'arrow-graph-up-right',
title: '二级-1'
},
component: () => import('@/view/multilevel/level-1.vue')
},
{
path: 'level_2_2',
name: 'level_2_2',
meta: {
access: ['super_admin'],
icon: 'arrow-graph-up-right',
title: '二级-2'
},
component: parentView,
children: [
{
path: 'level_2_2_1',
name: 'level_2_2_1',
meta: {
icon: 'arrow-graph-up-right',
title: '三级'
},
component: () => import('@/view/multilevel/level-2/level-2-1.vue')
}
]
},
{
path: 'level_2_3',
name: 'level_2_3',
meta: {
icon: 'arrow-graph-up-right',
title: '二级-3'
},
component: parentView,
children: [
{
path: 'level_2_3_1',
name: 'level_2_3_1',
meta: {
access: ['super_admin'],
icon: 'arrow-graph-up-right',
title: '三级-1'
},
component: () => import('@/view/multilevel/level-2/level-2-1.vue')
},
{
path: 'level_2_3_2',
name: 'level_2_3_2',
meta: {
access: ['super_admin', 'admin'],
icon: 'arrow-graph-up-right',
title: '三级-2'
},
component: () => import('@/view/multilevel/level-2/level-2-1.vue')
}
]
}
]
},
{
path: '/401',
name: 'error_401',
component: () => import('@/view/error-page/401.vue')
},
{
path: '/500',
name: 'error_500',
component: () => import('@/view/error-page/500.vue')
},
{
path: '*',
name: 'error_404',
component: () => import('@/view/error-page/404.vue')
}
]

View File

@ -1,22 +1,23 @@
import Vue from 'vue';
import Vuex from 'vuex';
import Vue from 'vue'
import Vuex from 'vuex'
import app from './modules/app';
import user from './modules/user';
import user from './module/user'
import app from './module/app'
Vue.use(Vuex);
Vue.use(Vuex)
const store = new Vuex.Store({
mutations: {
//
},
actions: {
},
modules: {
app,
user
}
});
export default store;
export default new Vuex.Store({
state: {
//
},
mutations: {
//
},
actions: {
//
},
modules: {
user,
app
}
})

30
src/store/module/app.js Normal file
View File

@ -0,0 +1,30 @@
import { getBreadCrumbList, setTagNavListInLocalstorage, getMenuByRouter, getTagNavListFromLocalstorage, getHomeRoute } from '@/libs/util'
import routers from '@/router/routers'
export default {
state: {
breadCrumbList: [],
tagNavList: [],
homeRoute: getHomeRoute(routers)
},
getters: {
menuList: (state, getters, rootState) => getMenuByRouter(routers, rootState.user.access)
},
mutations: {
setBreadCrumb (state, routeMetched) {
state.breadCrumbList = getBreadCrumbList(routeMetched)
},
setTagNavList (state, list) {
if (list) {
state.tagNavList = [...list]
setTagNavListInLocalstorage([...list])
} else state.tagNavList = getTagNavListFromLocalstorage()
},
addTag (state, item, type = 'unshift') {
if (state.tagNavList.findIndex(tag => tag.name === item.name) < 0) {
if (type === 'push') state.tagNavList.push(item)
else state.tagNavList.unshift(item)
setTagNavListInLocalstorage([...state.tagNavList])
}
}
}
}

79
src/store/module/user.js Normal file
View File

@ -0,0 +1,79 @@
import { login, logout, getUserInfo } from '@/api/user'
import { setToken, getToken } from '@/libs/util'
export default {
state: {
userName: '',
userId: '',
avatorImgPath: '',
token: getToken(),
access: ''
},
mutations: {
setAvator (state, avatorPath) {
state.avatorImgPath = avatorPath
},
setUserId (state, id) {
state.userId = id
},
setUserName (state, name) {
state.userName = name
},
setAccess (state, access) {
state.access = access
},
setToken (state, token) {
state.token = token
setToken(token)
}
},
actions: {
// 登录
handleLogin ({ commit }, {userName, password}) {
userName = userName.trim()
return new Promise((resolve, reject) => {
login({
userName,
password
}).then(res => {
const data = res.data
commit('setToken', data.token)
resolve()
}).catch(err => {
reject(err)
})
})
},
// 退出登录
handleLogOut ({ state, commit }) {
return new Promise((resolve, reject) => {
logout(state.token).then(() => {
commit('setToken', '')
commit('setAccess', [])
resolve()
}).catch(err => {
reject(err)
})
// 如果你的退出登录无需请求接口则可以直接使用下面三行代码而无需使用logout调用接口
// commit('setToken', '')
// commit('setAccess', [])
// resolve()
})
},
// 获取用户相关信息
getUserInfo ({ state, commit }) {
return new Promise((resolve, reject) => {
getUserInfo(state.token).then(res => {
const data = res.data
commit('setAvator', data.avator)
commit('setUserName', data.user_name)
commit('setUserId', data.user_id)
commit('setAccess', data.access)
resolve(data)
}).catch(err => {
reject(err)
})
})
}
}
}

View File

@ -1,18 +0,0 @@
import {otherRouter, appRouter} from '@/router/router';
const app = {
state: {
menuList: [],
routers: [
otherRouter,
...appRouter
]
},
mutations: {
updateMenulist (state) {
state.menuList = appRouter;
}
}
};
export default app;

View File

@ -1,13 +0,0 @@
import Cookies from 'js-cookie';
const user = {
state: {},
mutations: {
logout (state, vm) {
Cookies.remove('user');
localStorage.clear();
}
}
};
export default user;

View File

@ -1,64 +0,0 @@
.margin-top-8{
margin-top: 8px;
}
.margin-top-10{
margin-top: 10px;
}
.margin-top-20{
margin-top: 20px;
}
.margin-left-10{
margin-left: 10px;
}
.margin-bottom-10{
margin-bottom: 10px;
}
.margin-bottom-100{
margin-bottom: 100px;
}
.margin-right-10{
margin-right: 10px;
}
.padding-left-6{
padding-left: 6px;
}
.padding-left-8{
padding-left: 5px;
}
.padding-left-10{
padding-left: 10px;
}
.padding-left-20{
padding-left: 20px;
}
.height-100{
height: 100%;
}
.height-120px{
height: 100px;
}
.height-200px{
height: 200px;
}
.height-492px{
height: 492px;
}
.height-460px{
height: 460px;
}
.line-gray{
height: 0;
border-bottom: 2px solid #dcdcdc;
}
.notwrap{
word-break:keep-all;
white-space:nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.padding-left-5{
padding-left: 10px;
}
[v-cloak]{
display: none;
}

Binary file not shown.

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 326 KiB

Binary file not shown.

Binary file not shown.

View File

@ -1,8 +0,0 @@
.demo-spin-icon-load{
animation: ani-demo-spin 1s linear infinite;
}
@keyframes ani-demo-spin {
from { transform: rotate(0deg);}
50% { transform: rotate(180deg);}
to { transform: rotate(360deg);}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 386 KiB

View File

@ -1,19 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<title><%= htmlWebpackPlugin.options.title %></title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=0">
<link rel="stylesheet" href="<%= htmlWebpackPlugin.files.css[0] %>">
</head>
<body>
<div id="app"></div>
<div class="lock-screen-back" id="lock_screen_back"></div>
<script type="text/javascript" src="<%= htmlWebpackPlugin.files.js[0] %>"></script>
<script type="text/javascript" src="<%= htmlWebpackPlugin.files.js[1] %>"></script>
<script type="text/javascript" src="<%= htmlWebpackPlugin.files.js[2] %>"></script>
</body>
</html>

View File

@ -1,4 +0,0 @@
import Vue from 'vue';
import iView from 'iview';
import VueRouter from 'vue-router';
import Vuex from 'vuex';

View File

@ -1,2 +0,0 @@
import axios from 'axios';
import Cookies from 'js-cookie';

View File

@ -0,0 +1,13 @@
<template>
<div>count to</div>
</template>
<script>
export default {
name: 'countTo'
}
</script>
<style>
</style>

View File

@ -0,0 +1,19 @@
<template>
<error-content code="401" desc="Oh~~您没有浏览这个页面的权限~" :src="src"/>
</template>
<script>
import error401 from '@/assets/images/error-page/error-401.svg'
import errorContent from './error-content.vue'
export default {
name: 'error_401',
components: {
errorContent
},
data () {
return {
src: error401
}
}
}
</script>

View File

@ -0,0 +1,19 @@
<template>
<error-content code="404" desc="Oh~~您的页面好像飞走了~" :src="src"/>
</template>
<script>
import error404 from '@/assets/images/error-page/error-404.svg'
import errorContent from './error-content.vue'
export default {
name: 'error_404',
components: {
errorContent
},
data () {
return {
src: error404
}
}
}
</script>

View File

@ -0,0 +1,19 @@
<template>
<error-content code="500" desc="Oh~~鬼知道服务器经历了什么~" :src="src"/>
</template>
<script>
import error404 from '@/assets/images/error-page/error-500.svg'
import errorContent from './error-content.vue'
export default {
name: 'error_500',
components: {
errorContent
},
data () {
return {
src: error404
}
}
}
</script>

View File

@ -0,0 +1,38 @@
<template>
<div>
<Button size="large" type="text" @click="backHome">返回首页</Button>
<Button size="large" type="text">返回上一页({{ second }}s)</Button>
</div>
</template>
<script>
import './error.less'
export default {
name: 'backBtnGroup',
data () {
return {
second: 5,
timer: null
}
},
methods: {
backHome () {
this.$router.replace({
name: 'home'
})
},
backPrev () {
this.$router.go(-1)
}
},
mounted () {
this.timer = setInterval(() => {
if (this.second === 0) this.backPrev()
else this.second--
}, 1000)
},
beforeDestroy () {
clearInterval(this.timer)
}
}
</script>

View File

@ -0,0 +1,28 @@
<template>
<div class="error-page">
<div class="content-con">
<img :src="src" alt="404">
<div class="text-con">
<h4>{{ code }}</h4>
<h5>{{ desc }}</h5>
</div>
<back-btn-group class="back-btn-group"></back-btn-group>
</div>
</div>
</template>
<script>
import './error.less'
import backBtnGroup from './back-btn-group.vue'
export default {
name: 'error_404',
components: {
backBtnGroup
},
props: {
code: String,
desc: String,
src: String
}
}
</script>

View File

@ -0,0 +1,46 @@
.error-page{
width: 100%;
height: 100%;
position: relative;
background: #f8f8f9;
.content-con{
width: 700px;
height: 600px;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -60%);
img{
display: block;
width: 100%;
height: 100%;
}
.text-con{
position: absolute;
left: 0px;
top: 0px;
h4{
position: absolute;
left: 0px;
top: 0px;
font-size: 80px;
font-weight: 700;
color: #348EED;
}
h5{
position: absolute;
width: 700px;
left: 0px;
top: 100px;
font-size: 20px;
font-weight: 700;
color: #67647D;
}
}
.back-btn-group{
position: absolute;
right: 0px;
bottom: 20px;
}
}
}

0
src/views/login.less → src/view/login/login.less Normal file → Executable file
View File

45
src/view/login/login.vue Executable file
View File

@ -0,0 +1,45 @@
<style lang="less">
@import './login.less';
</style>
<template>
<div class="login" @keydown.enter="handleLogin">
<div class="login-con">
<Card icon="log-in" title="欢迎登录" :bordered="false">
<div class="form-con">
<login-form @on-success-valid="handleSubmit"></login-form>
<p class="login-tip">输入任意用户名和密码即可</p>
</div>
</Card>
</div>
</div>
</template>
<script>
import loginForm from '_c/login-form'
import { mapActions } from 'vuex'
export default {
components: {
loginForm
},
methods: {
...mapActions([
'handleLogin',
'getUserInfo'
]),
handleSubmit ({ userName, password }) {
this.handleLogin({ userName, password }).then(res => {
this.getUserInfo().then(res => {
this.$router.push({
name: 'home'
})
})
})
}
}
}
</script>
<style>
</style>

View File

@ -0,0 +1,4 @@
.custom-bread-crumb{
display: inline-block;
vertical-align: top;
}

View File

@ -0,0 +1,33 @@
<template>
<div class="custom-bread-crumb">
<Breadcrumb :style="{fontSize: `${fontSize}px`}">
<BreadcrumbItem v-for="item in list" :to="item.to" :key="`bread-crumb-${item.name}`"><Icon v-if="showIcon" :type="item.icon" style="margin-right: 4px;" />{{ showTitle(item) }}</BreadcrumbItem>
</Breadcrumb>
</div>
</template>
<script>
import { showTitle } from '_c/common/util'
import './custom-bread-crumb.less'
export default {
name: 'customBreadCrumb',
props: {
list: {
type: Array,
default: () => []
},
fontSize: {
type: Number,
default: 14
},
showIcon: {
type: Boolean,
default: false
}
},
methods: {
showTitle (item) {
return showTitle(item, this)
}
}
}
</script>

View File

@ -0,0 +1,2 @@
import customBreadCrumb from './custom-bread-crumb.vue'
export default customBreadCrumb

View File

@ -0,0 +1,10 @@
.header-bar{
width: 100%;
height: 100%;
position: relative;
.custom-content-con{
float: right;
height: auto;
padding-right: 20px;
}
}

View File

@ -0,0 +1,34 @@
<template>
<div class="header-bar">
<sider-trigger :collapsed="collapsed" icon="navicon-round" @on-change="handleCollpasedChange"></sider-trigger>
<custom-bread-crumb show-icon style="margin-left: 30px;" :list="breadCrumbList"></custom-bread-crumb>
<div class="custom-content-con">
<slot></slot>
</div>
</div>
</template>
<script>
import siderTrigger from './sider-trigger'
import customBreadCrumb from './custom-bread-crumb'
import './header-bar.less'
export default {
name: 'headerBar',
components: {
siderTrigger,
customBreadCrumb
},
props: {
collapsed: Boolean
},
computed: {
breadCrumbList () {
return this.$store.state.app.breadCrumbList
}
},
methods: {
handleCollpasedChange (state) {
this.$emit('on-coll-change', state)
}
}
}
</script>

View File

@ -0,0 +1,2 @@
import headerBar from './header-bar'
export default headerBar

View File

@ -0,0 +1,2 @@
import siderTrigger from './sider-trigger.vue'
export default siderTrigger

View File

@ -0,0 +1,21 @@
.trans{
transition: transform .2s ease;
}
@size: 40px;
.sider-trigger-a{
padding: 6px;
width: @size;
height: @size;
display: inline-block;
text-align: center;
color: #5c6b77;
margin-top: 12px;
i{
.trans;
vertical-align: top;
}
&.collapsed i{
transform: rotateZ(90deg);
.trans;
}
}

View File

@ -0,0 +1,27 @@
<template>
<a @click="handleChange" type="text" :class="['sider-trigger-a', collapsed ? 'collapsed' : '']"><Icon :type="icon" :size="size" /></a>
</template>
<script>
export default {
name: 'siderTrigger',
props: {
collapsed: Boolean,
icon: {
type: String,
default: 'navicon-round'
},
size: {
type: Number,
default: 26
}
},
methods: {
handleChange () {
this.$emit('on-change', !this.collapsed)
}
}
}
</script>
<style lang="less">
@import './sider-trigger.less';
</style>

View File

@ -0,0 +1,34 @@
<template>
<Dropdown @on-click="handleClick" transer placement="right-start">
<a class="drop-menu-a" type="text" :style="{textAlign: !hideTitle ? 'left' : ''}"><Icon :size="rootIconSize" :color="textColor" :type="parentItem.icon"/><span class="menu-title" v-if="!hideTitle">{{ showTitle(parentItem) }}</span><Icon v-if="!hideTitle" :size="10" :color="textColor" type="chevron-right" style="float: right;margin-top: 4px;"/></a>
<DropdownMenu slot="list">
<template v-for="child in children">
<collapsed-menu v-if="showChildren(child)" :icon-size="iconSize" :parent-item="child" :key="`drop-${child.name}`"></collapsed-menu>
<DropdownItem v-else :key="`drop-${child.name}`" :name="child.name"><Icon :size="iconSize" :type="child.icon"/><span class="menu-title">{{ showTitle(child) }}</span></DropdownItem>
</template>
</DropdownMenu>
</Dropdown>
</template>
<script>
import mixin from './mixin'
import itemMixin from './item-mixin'
export default {
name: 'collapsedMenu',
mixins: [ mixin, itemMixin ],
props: {
hideTitle: {
type: Boolean,
default: false
},
rootIconSize: {
type: Number,
default: 16
}
},
methods: {
handleClick (name) {
this.$emit('on-click', name)
}
}
}
</script>

View File

@ -0,0 +1,2 @@
import sideMenu from './side-menu.vue'
export default sideMenu

View File

@ -0,0 +1,21 @@
export default {
props: {
parentItem: {
type: Object,
default: () => {}
},
theme: String,
iconSize: Number
},
computed: {
parentName () {
return this.parentItem.name
},
children () {
return this.parentItem.children
},
textColor () {
return this.theme === 'dark' ? '#fff' : '#495060'
}
}
}

View File

@ -0,0 +1,10 @@
export default {
methods: {
showTitle (item) {
return this.$config.useI18n ? this.$t(item.name) : ((item.meta && item.meta.title) || item.name)
},
showChildren (item) {
return item.children && item.children.length !== 0
}
}
}

View File

@ -0,0 +1,20 @@
<template>
<Submenu :name="`${parentName}`">
<template slot="title">
<Icon :type="parentItem.icon"/>
<span>{{ showTitle(parentItem) }}</span>
</template>
<template v-for="item in children">
<side-menu-item v-if="item.children && item.children.length !== 0" :key="`menu-${item.name}`" :parent-item="item"></side-menu-item>
<menu-item v-else :name="`${item.name}`" :key="`menu-${item.name}`"><Icon :type="item.icon"/><span>{{ showTitle(item) }}</span></menu-item>
</template>
</Submenu>
</template>
<script>
import mixin from './mixin'
import itemMixin from './item-mixin'
export default {
name: 'sideMenuItem',
mixins: [ mixin, itemMixin ]
}
</script>

View File

@ -0,0 +1,31 @@
.side-menu-wrapper{
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
.menu-collapsed{
padding-top: 10px;
a.drop-menu-a{
display: inline-block;
padding: 6px 15px;
width: 100%;
text-align: center;
color: #495060;
}
.ivu-dropdown{
width: 100%;
.ivu-dropdown-rel button{
width: 100%;
}
}
.menu-title{
margin-left: 6px;
}
.ivu-select-dropdown{
width: 160px;
margin-left: 4px;
}
}
}

View File

@ -0,0 +1,93 @@
<template>
<div class="side-menu-wrapper">
<slot></slot>
<Menu ref="menu" v-show="!collapsed" :active-name="activeName" :open-names="openedNames" :accordion="accordion" :theme="theme" width="auto" @on-select="handleSelect">
<template v-for="item in menuList">
<side-menu-item v-if="showChildren(item)" :key="`menu-${item.name}`" :parent-item="item"></side-menu-item>
<menu-item v-else :name="`${item.name}`" :key="`menu-${item.name}`"><Icon :type="item.icon"/><span>{{ showTitle(item) }}</span></menu-item>
</template>
</Menu>
<div class="menu-collapsed" v-show="collapsed" :list="menuList">
<collapsed-menu @on-click="handleSelect" hide-title :root-icon-size="rootIconSize" :icon-size="iconSize" :theme="theme" v-for="item in menuList" :parent-item="item" :key="`drop-menu-${item.name}`"></collapsed-menu>
</div>
</div>
</template>
<script>
import sideMenuItem from './side-menu-item.vue'
import collapsedMenu from './collapsed-menu.vue'
import { getIntersection } from '@/libs/tools'
import mixin from './mixin'
export default {
name: 'sideMenu',
mixins: [ mixin ],
components: {
sideMenuItem,
collapsedMenu
},
props: {
menuList: {
type: Array,
default () {
return []
}
},
collapsed: {
type: Boolean
},
theme: {
type: String,
default: 'dark'
},
rootIconSize: {
type: Number,
default: 20
},
iconSize: {
type: Number,
default: 16
},
accordion: Boolean,
activeName: {
type: String,
default: ''
},
openNames: {
type: Array,
default: () => []
}
},
data () {
return {
openedNames: []
}
},
methods: {
handleSelect (name) {
this.$emit('on-select', name)
},
getOpenedNamesByActiveName (name) {
return this.$route.matched.map(item => item.name).filter(item => item !== name)
}
},
watch: {
activeName (name) {
if (this.accordion) this.openedNames = this.getOpenedNamesByActiveName(name)
else this.openedNames = getIntersection(this.openedNames, this.getOpenedNamesByActiveName(name))
},
openNames (newNames) {
this.openedNames = newNames
},
openedNames () {
this.$nextTick(() => {
this.$refs.menu.updateOpened()
})
}
},
mounted () {
this.openedNames = getIntersection(this.openedNames, this.getOpenedNamesByActiveName(name))
}
}
</script>
<style lang="less">
@import './side-menu.less';
</style>

View File

@ -0,0 +1,2 @@
import tagsNav from './tags-nav.vue'
export default tagsNav

View File

@ -0,0 +1,68 @@
.no-select{
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.size{
width: 100%;
height: 100%;
}
.tags-nav{
position: relative;
border-top: 1px solid #F0F0F0;
border-bottom: 1px solid #F0F0F0;
.no-select;
.size;
.close-con{
position: absolute;
right: 0;
top: 0;
height: 100%;
width: 32px;
background: #fff;
text-align: center;
z-index: 10;
}
.btn-con{
position: absolute;
top: 0px;
height: 100%;
background: #fff;
padding-top: 3px;
z-index: 10;
button{
padding: 6px 10px;
text-align: center;
}
&.left-btn{
left: 0px;
}
&.right-btn{
right: 32px;
border-right: 1px solid #F0F0F0;
}
}
.scroll-outer{
position: absolute;
left: 28px;
right: 61px;
top: 0;
bottom: 0;
box-shadow: 0px 0 3px 2px rgba(100,100,100,.1) inset;
.scroll-body{
height: ~"calc(100% - 1px)";
display: inline-block;
padding: 1px 4px 0;
position: absolute;
overflow: visible;
white-space: nowrap;
transition: left .3s ease;
.ivu-tag-dot-inner{
transition: background .2s ease;
}
}
}
}

View File

@ -0,0 +1,109 @@
<template>
<div class="tags-nav">
<div class="close-con">
<Dropdown transfer @on-click="handleTagsOption" style="margin-top:7px;">
<Button size="small" type="text">
<Icon :size="18" type="ios-close-outline"></Icon>
</Button>
<DropdownMenu slot="list">
<DropdownItem name="close-all">关闭所有</DropdownItem>
<DropdownItem name="close-others">关闭其他</DropdownItem>
</DropdownMenu>
</Dropdown>
</div>
<div class="btn-con left-btn">
<Button icon="chevron-left" type="text" @click="handleScroll(240)"></Button>
</div>
<div class="btn-con right-btn">
<Button icon="chevron-right" type="text" @click="handleScroll(-240)"></Button>
</div>
<div class="scroll-outer" ref="scrollOuter" @DOMMouseScroll="handlescroll" @mousewheel="handlescroll">
<div ref="scrollBody" class="scroll-body" :style="{left: tagBodyLeft + 'px'}">
<transition-group name="taglist-moving-animation">
<Tag
type="dot"
v-for="item in list"
ref="tagsPageOpened"
:key="`tag-nav-${item.name}`"
:name="item.name"
@on-close="handleClose"
@click.native="handleClick(item)"
:closable="item.name==='home_index'?false:true"
:color="item.name === value.name ? 'blue' : 'default'"
>{{ showTitleInside(item) }}</Tag>
</transition-group>
</div>
</div>
</div>
</template>
<script>
import { showTitle } from '@/libs/util'
export default {
name: 'tagsNav',
props: {
value: Object,
list: {
type: Array,
default () {
return []
}
}
},
data () {
return {
tagBodyLeft: 0
}
},
methods: {
handlescroll (e) {
var type = e.type
let delta = 0
if (type === 'DOMMouseScroll' || type === 'mousewheel') {
delta = (e.wheelDelta) ? e.wheelDelta : -(e.detail || 0) * 40
}
this.handleScroll(delta)
},
handleScroll (offset) {
if (offset > 0) {
this.tagBodyLeft = Math.min(0, this.tagBodyLeft + offset)
} else {
if (this.$refs.scrollOuter.offsetWidth < this.$refs.scrollBody.offsetWidth) {
if (this.tagBodyLeft < -(this.$refs.scrollBody.offsetWidth - this.$refs.scrollOuter.offsetWidth)) {
this.tagBodyLeft = this.tagBodyLeft
} else {
this.tagBodyLeft = Math.max(this.tagBodyLeft + offset, this.$refs.scrollOuter.offsetWidth - this.$refs.scrollBody.offsetWidth)
}
} else {
this.tagBodyLeft = 0
}
}
},
handleTagsOption (type) {
if (type === 'close-all') {
// home
let res = this.list.filter(item => item.name === 'home')
this.$emit('on-close', res, 'all')
} else {
// home
let res = this.list.filter(item => item.name === this.value.name || item.name === 'home')
this.$emit('on-close', res, 'others')
}
},
handleClose (e, name) {
let res = this.list.filter(item => item.name !== name)
this.$emit('on-close', name, res)
},
handleClick (item) {
this.$emit('input', item)
},
showTitleInside (item) {
return showTitle(item, this)
}
}
}
</script>
<style lang="less">
@import './tags-nav.less';
</style>

View File

@ -0,0 +1,2 @@
import user from './user.vue'
export default user

Some files were not shown because too many files have changed in this diff Show More