merge master

This commit is contained in:
Pan 2019-03-17 17:35:23 +08:00
commit 60bddfedec
36 changed files with 1805 additions and 350 deletions

38
src/api/role.js Normal file
View File

@ -0,0 +1,38 @@
import request from '@/utils/request'
export function getRoutes() {
return request({
url: '/routes',
method: 'get'
})
}
export function getRoles() {
return request({
url: '/roles',
method: 'get'
})
}
export function deleteRole(id) {
return request({
url: `/roles/${id}`,
method: 'delete'
})
}
export function addRole(data) {
return request({
url: '/roles',
method: 'post',
data
})
}
export function updateRole(key, data) {
return request({
url: `/roles/${key}`,
method: 'put',
data
})
}

View File

@ -34,8 +34,8 @@ export default {
}
},
computed: {
routers() {
return this.$store.getters.permission_routers
routes() {
return this.$store.getters.permission_routes
},
lang() {
return this.$store.getters.language
@ -43,10 +43,10 @@ export default {
},
watch: {
lang() {
this.searchPool = this.generateRouters(this.routers)
this.searchPool = this.generateRoutes(this.routes)
},
routers() {
this.searchPool = this.generateRouters(this.routers)
routes() {
this.searchPool = this.generateRoutes(this.routes)
},
searchPool(list) {
this.initFuse(list)
@ -60,7 +60,7 @@ export default {
}
},
mounted() {
this.searchPool = this.generateRouters(this.routers)
this.searchPool = this.generateRoutes(this.routes)
},
methods: {
click() {
@ -101,10 +101,10 @@ export default {
},
// Filter out the routes that can be displayed in the sidebar
// And generate the internationalized title
generateRouters(routers, basePath = '/', prefixTitle = []) {
generateRoutes(routes, basePath = '/', prefixTitle = []) {
let res = []
for (const router of routers) {
for (const router of routes) {
// skip hidden router
if (router.hidden) { continue }
@ -126,11 +126,11 @@ export default {
}
}
// recursive child routers
// recursive child routes
if (router.children) {
const tempRouters = this.generateRouters(router.children, data.path, data.title)
if (tempRouters.length >= 1) {
res = [...res, ...tempRouters]
const tempRoutes = this.generateRoutes(router.children, data.path, data.title)
if (tempRoutes.length >= 1) {
res = [...res, ...tempRoutes]
}
}
}

View File

@ -0,0 +1,220 @@
- [Enlgish](#Brief)
# 中文
## 写在前面
此组件仅提供一个创建 `TreeTable` 的解决思路。它基于`element-ui`的 table 组件实现,通过`el-table`的`row-style`方法,在里面判断元素是否需要隐藏或者显示,从而实现`TreeTable`的展开与收起。
并且本组件充分利用 `vue` 插槽的特性来方便用户自定义。
`evel.js` 里面,`addAttrs` 方法会给数据添加几个属性,`treeTotable` 会对数组扁平化。这些操作都不会破坏源数据,只是会新增属性。
## Props 说明
| Attribute | Description | Type | Default |
| :--------------: | :--------------------------------- | :-----: | :------: |
| data | 原始展示数据 | Array | [] |
| columns | 列属性 | Array | [] |
| defaultExpandAll | 默认是否全部展开 | Boolean | false |
| defaultChildren | 指定子树为节点对象的某个属性值 | String | children | |
| indent | 相邻级节点间的水平缩进,单位为像素 | Number | 50 |
> 任何 `el-table` 的属性都支持,例如`border`、`fit`、`size`或者`@select`、`@cell-click`等方法。详情属性见`el-table`文档。
---
### 代码示例
```html
<tree-table :data="data" :columns="columns" border>
```
#### data(**必填**)
```js
const data = [
{
name:'1'
children: [
{
name: '1-1'
},
{
name: '1-2'
}
]
},
{
name: `2`
}
]
```
#### columns(**必填**)
- label: 显示在表头的文字
- key: 对应 data 的 key。treeTable 将显示相应的 value
- expand: `true` or `false`。若为 true则在该列显示展开收起图标
- checkbox: `true` or `false`。若为 true则在该列显示`checkbox`
- width: 每列的宽度,为一个数字(可选)。例如`200`
- align: 对齐方式 `left/center/right`
- header-align: 表头对齐方式 `left/center/right`
```javascript
const columns = [
{
label: 'Checkbox',
checkbox: true
},
{
label: '',
key: 'id',
expand: true
},
{
label: 'Event',
key: 'event',
width: 200,
align: 'left'
},
{
label: 'Scope',
key: 'scope'
}
]
```
> 树表组件将会根据 columns 的 key 属性生成具名插槽,如果你需要对列数据进行自定义,通过插槽即可实现
```html
<template slot="your key" slot-scope="{scope}">
<el-tag>level: {{ scope.row._level }}</el-tag>
<el-tag>expand: {{ scope.row._expand }}</el-tag>
<el-tag>select: {{ scope.row._select }}</el-tag>
</template>
```
## Events
目前提供了几个方法,不过只是`beta`版本,之后很可能会修改。
```js
this.$refs.TreeTable.addChild(row, data) //添加子元素
this.$refs.TreeTable.addBrother(row, data) //添加兄弟元素
this.$refs.TreeTable.delete(row) //删除该元素
```
## 其他
如果有其他的需求,请参考[el-table](http://element-cn.eleme.io/#/en-US/component/table)的 api 自行修改 index.vue
# English
## Brief
This component only provides a solution for creating `TreeTable`. It is based on the `element-ui` table component. It uses the `row-style` method of `el-table` to determine whether the element needs to be hidden or displayed.
And this component makes full use of the features of the `vue` slot to make it user-friendly.
In `evel.js`, the `addAttrs` method adds several properties to the data, and `treeTotable` flattens the array. None of these operations will destroy the source data, just add properties.
## Props
| Attribute | Description | Type | Default |
| :--------------: | :----------------------------------------------------------- | :-----: | :------: |
| data | original display data | Array | [] |
| columns | column attribute | Array | [] |
| defaultExpandAll | whether to expand all nodes by default | Boolean | false |
| defaultChildren | specify which node object is used as the node's subtree | String | children | |
| indent | horizontal indentation of nodes in adjacent levels in pixels | Number | 50 |
> Any of the `el-table` properties are supported, such as `border`, `fit`, `size` or `@select`, `@cell-click`. See the ʻel-table` documentation for details.
---
### Example
```html
<tree-table :data="data" :columns="columns" border>
```
#### data(**Required**)
```js
const data = [
{
name:'1'
children: [
{
name: '1-1'
},
{
name: '1-2'
}
]
},
{
name: `2`
}
]
```
#### columns(**Required**)
- label: text displayed in the header
- key: data.key will show in column
- expand: `true` or `false`
- checkbox: `true` or `false`
- width: column width 。such as `200`
- align: alignment `left/center/right`
- header-align: alignment of the table header `left/center/right`
```javascript
const columns = [
{
label: 'Checkbox',
checkbox: true
},
{
label: '',
key: 'id',
expand: true
},
{
label: 'Event',
key: 'event',
width: 200,
align: 'left'
},
{
label: 'Scope',
key: 'scope'
}
]
```
> The tree table component will generate a named slot based on the key property of columns. If you need to customize the column data, you can do it through the slot.
```html
<template slot="your key" slot-scope="{scope}">
<el-tag>level: {{ scope.row._level }}</el-tag>
<el-tag>expand: {{ scope.row._expand }}</el-tag>
<el-tag>select: {{ scope.row._select }}</el-tag>
</template>
```
## Events
Several methods are currently available, but only the `beta` version, which is likely to be modified later.
```js
this.$refs.TreeTable.addChild(row, data) //Add child elements
this.$refs.TreeTable.addBrother(row, data) //Add a sibling element
this.$refs.TreeTable.delete(row) //Delete the element
```
## Other
If you have other requirements, please refer to the [el-table](http://element-cn.eleme.io/#/en-US/component/table) api to modify the index.vue

View File

@ -1,29 +1,48 @@
/**
* @Author: jianglei
* @Date: 2017-10-12 12:06:49
*/
'use strict'
import Vue from 'vue'
export default function treeToArray(data, expandAll, parent = null, level = null) {
// Flattened array
export default function treeToArray(data, children = 'children') {
let tmp = []
Array.from(data).forEach(function(record) {
if (record._expanded === undefined) {
Vue.set(record, '_expanded', expandAll)
}
let _level = 1
if (level !== undefined && level !== null) {
_level = level + 1
}
Vue.set(record, '_level', _level)
// 如果有父元素
if (parent) {
Vue.set(record, 'parent', parent)
}
tmp.push(record)
if (record.children && record.children.length > 0) {
const children = treeToArray(record.children, expandAll, record, _level)
tmp = tmp.concat(children)
data.forEach((item, index) => {
Vue.set(item, '_index', index)
tmp.push(item)
if (item[children] && item[children].length > 0) {
const res = treeToArray(item[children], children)
tmp = tmp.concat(res)
}
})
return tmp
}
export function addAttrs(data, { parent = null, preIndex = false, level = 0, expand = false, children = 'children', show = true, select = false } = {}) {
data.forEach((item, index) => {
const _id = (preIndex ? `${preIndex}-${index}` : index) + ''
Vue.set(item, '_id', _id)
Vue.set(item, '_level', level)
Vue.set(item, '_expand', expand)
Vue.set(item, '_parent', parent)
Vue.set(item, '_show', show)
Vue.set(item, '_select', select)
if (item[children] && item[children].length > 0) {
addAttrs(item[children], {
parent: item,
level: level + 1,
expand,
preIndex: _id,
children,
status,
select
})
}
})
}
export function cleanParentAttr(data, children = 'children') {
data.forEach(item => {
item._parent = null
if (item[children] && item[children].length > 0) {
addAttrs(item[children], children)
}
})
return data
}

View File

@ -1,128 +1,193 @@
<template>
<el-table :data="formatData" :row-style="showRow" v-bind="$attrs">
<el-table-column v-if="columns.length===0" width="150">
<el-table :data="tableData" :row-style="showRow" v-bind="$attrs" v-on="$listeners">
<slot name="selection" />
<slot name="pre-column" />
<el-table-column
v-for="item in columns"
:key="item.key"
:label="item.label"
:width="item.width"
:align="item.align||'center'"
:header-align="item.headerAlign"
>
<template slot-scope="scope">
<span v-for="space in scope.row._level" :key="space" class="ms-tree-space" />
<span v-if="iconShow(0,scope.row)" class="tree-ctrl" @click="toggleExpanded(scope.$index)">
<i v-if="!scope.row._expanded" class="el-icon-plus" />
<i v-else class="el-icon-minus" />
</span>
{{ scope.$index }}
<slot :scope="scope" :name="item.key">
<template v-if="item.expand">
<span :style="{'padding-left':+scope.row._level*indent + 'px'} " />
<span v-show="showSperadIcon(scope.row)" class="tree-ctrl" @click="toggleExpanded(scope.$index)">
<i v-if="!scope.row._expand" class="el-icon-plus" />
<i v-else class="el-icon-minus" />
</span>
</template>
<template v-if="item.checkbox">
<el-checkbox
v-if="scope.row[defaultChildren]&&scope.row[defaultChildren].length>0"
v-model="scope.row._select"
:style="{'padding-left':+scope.row._level*indent + 'px'} "
:indeterminate="scope.row._select"
@change="handleCheckAllChange(scope.row)"
/>
<el-checkbox
v-else
v-model="scope.row._select"
:style="{'padding-left':+scope.row._level*indent + 'px'} "
@change="handleCheckAllChange(scope.row)"
/>
</template>
{{ scope.row[item.key] }}
</slot>
</template>
</el-table-column>
<el-table-column v-for="(column, index) in columns" v-else :key="column.value" :label="column.text" :width="column.width">
<template slot-scope="scope">
<!-- Todo -->
<!-- eslint-disable-next-line vue/no-confusing-v-for-v-if -->
<!-- eslint-disable-next-line -->
<span v-for="space in scope.row._level" v-if="index === 0" :key="space" class="ms-tree-space" />
<span v-if="iconShow(index,scope.row)" class="tree-ctrl" @click="toggleExpanded(scope.$index)">
<i v-if="!scope.row._expanded" class="el-icon-plus" />
<i v-else class="el-icon-minus" />
</span>
{{ scope.row[column.value] }}
</template>
</el-table-column>
<slot />
</el-table>
</template>
<script>
/**
Auth: Lei.j1ang
Created: 2018/1/19-13:59
*/
import treeToArray from './eval'
import treeToArray, { addAttrs } from './eval.js'
export default {
name: 'TreeTable',
props: {
/* eslint-disable */
data: {
type: [Array, Object],
required: true
type: Array,
required: true,
default: () => []
},
columns: {
type: Array,
default: () => []
},
evalFunc: Function,
evalArgs: Array,
expandAll: {
defaultExpandAll: {
type: Boolean,
default: false
},
defaultChildren: {
type: String,
default: 'children'
},
indent: {
type: Number,
default: 50
}
},
data() {
return {
guard: 1
}
},
computed: {
//
formatData: function() {
let tmp
if (!Array.isArray(this.data)) {
tmp = [this.data]
} else {
tmp = this.data
children() {
return this.defaultChildren
},
tableData() {
const data = this.data
if (this.data.length === 0) {
return []
}
const func = this.evalFunc || treeToArray
const args = this.evalArgs ? Array.concat([tmp, this.expandAll], this.evalArgs) : [tmp, this.expandAll]
return func.apply(null, args)
addAttrs(data, {
expand: this.defaultExpandAll,
children: this.defaultChildren
})
const retval = treeToArray(data, this.defaultChildren)
return retval
}
},
methods: {
showRow: function(row) {
const show = (row.row.parent ? (row.row.parent._expanded && row.row.parent._show) : true)
row.row._show = show
return show ? 'animation:treeTableShow 1s;-webkit-animation:treeTableShow 1s;' : 'display:none;'
addBrother(row, data) {
if (row._parent) {
row._parent.children.push(data)
} else {
this.data.push(data)
}
},
//
toggleExpanded: function(trIndex) {
const record = this.formatData[trIndex]
record._expanded = !record._expanded
addChild(row, data) {
if (!row.children) {
this.$set(row, 'children', [])
}
row.children.push(data)
},
//
iconShow(index, record) {
return (index === 0 && record.children && record.children.length > 0)
delete(row) {
const { _index, _parent } = row
if (_parent) {
_parent.children.splice(_index, 1)
} else {
this.data.splice(_index, 1)
}
},
getData() {
return this.tableData
},
showRow: function({ row }) {
const parent = row._parent
const show = parent ? parent._expand && parent._show : true
row._show = show
return show
? 'animation:treeTableShow 1s;-webkit-animation:treeTableShow 1s;'
: 'display:none;'
},
showSperadIcon(record) {
return record[this.children] && record[this.children].length > 0
},
toggleExpanded(trIndex) {
const record = this.tableData[trIndex]
const expand = !record._expand
record._expand = expand
},
handleCheckAllChange(row) {
this.selcetRecursion(row, row._select, this.defaultChildren)
this.isIndeterminate = row._select
},
selcetRecursion(row, select, children = 'children') {
if (select) {
this.$set(row, '_expand', true)
this.$set(row, '_show', true)
}
const sub_item = row[children]
if (sub_item && sub_item.length > 0) {
sub_item.map(child => {
child._select = select
this.selcetRecursion(child, select, children)
})
}
},
updateTreeNode(item) {
return new Promise(resolve => {
const { _id, _parent } = item
const index = _id.split('-').slice(-1)[0] // get last index
if (_parent) {
_parent.children.splice(index, 1, item)
resolve(this.data)
} else {
this.data.splice(index, 1, item)
resolve(this.data)
}
})
}
}
}
</script>
<style rel="stylesheet/css">
@keyframes treeTableShow {
from {opacity: 0;}
to {opacity: 1;}
}
@-webkit-keyframes treeTableShow {
from {opacity: 0;}
to {opacity: 1;}
}
</style>
<style lang="scss" rel="stylesheet/scss" scoped>
$color-blue: #2196F3;
$space-width: 18px;
.ms-tree-space {
position: relative;
top: 1px;
display: inline-block;
font-style: normal;
font-weight: 400;
line-height: 1;
width: $space-width;
height: 14px;
&::before {
content: ""
}
<style>
@keyframes treeTableShow {
from {
opacity: 0;
}
.processContainer{
width: 100%;
height: 100%;
to {
opacity: 1;
}
table td {
line-height: 26px;
}
@-webkit-keyframes treeTableShow {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.tree-ctrl{
position: relative;
cursor: pointer;
color: $color-blue;
margin-left: -$space-width;
}
.tree-ctrl {
position: relative;
cursor: pointer;
color: #2196f3;
}
</style>

View File

@ -1,89 +1,220 @@
- [Enlgish](#Brief)
# 中文
## 写在前面
此组件仅提供一个创建TreeTable的解决思路
## prop说明
#### *data*
**必填**
此组件仅提供一个创建 `TreeTable` 的解决思路。它基于`element-ui`的 table 组件实现,通过`el-table`的`row-style`方法,在里面判断元素是否需要隐藏或者显示,从而实现`TreeTable`的展开与收起。
原始数据,要求是一个数组或者对象
```javascript
[{
key1: value1,
key2: value2,
children: [{
key1: value1
并且本组件充分利用 `vue` 插槽的特性来方便用户自定义。
`evel.js` 里面,`addAttrs` 方法会给数据添加几个属性,`treeTotable` 会对数组扁平化。这些操作都不会破坏源数据,只是会新增属性。
## Props 说明
| Attribute | Description | Type | Default |
| :--------------: | :--------------------------------- | :-----: | :------: |
| data | 原始展示数据 | Array | [] |
| columns | 列属性 | Array | [] |
| defaultExpandAll | 默认是否全部展开 | Boolean | false |
| defaultChildren | 指定子树为节点对象的某个属性值 | String | children | |
| indent | 相邻级节点间的水平缩进,单位为像素 | Number | 50 |
> 任何 `el-table` 的属性都支持,例如`border`、`fit`、`size`或者`@select`、`@cell-click`等方法。详情属性见`el-table`文档。
---
### 代码示例
```html
<tree-table :data="data" :columns="columns" border>
```
#### data(**必填**)
```js
const data = [
{
name:'1'
children: [
{
name: '1-1'
},
{
key1: value1
}]
},
{
key1: value1
}]
```
或者
```javascript
{
key1: value1,
key2: value2,
children: [{
key1: value1
name: '1-2'
}
]
},
{
name: `2`
}
]
```
#### columns(**必填**)
- label: 显示在表头的文字
- key: 对应 data 的 key。treeTable 将显示相应的 value
- expand: `true` or `false`。若为 true则在该列显示展开收起图标
- checkbox: `true` or `false`。若为 true则在该列显示`checkbox`
- width: 每列的宽度,为一个数字(可选)。例如`200`
- align: 对齐方式 `left/center/right`
- header-align: 表头对齐方式 `left/center/right`
```javascript
const columns = [
{
label: 'Checkbox',
checkbox: true
},
{
label: '',
key: 'id',
expand: true
},
{
label: 'Event',
key: 'event',
width: 200,
align: 'left'
},
{
label: 'Scope',
key: 'scope'
}
]
```
> 树表组件将会根据 columns 的 key 属性生成具名插槽,如果你需要对列数据进行自定义,通过插槽即可实现
```html
<template slot="your key" slot-scope="{scope}">
<el-tag>level: {{ scope.row._level }}</el-tag>
<el-tag>expand: {{ scope.row._expand }}</el-tag>
<el-tag>select: {{ scope.row._select }}</el-tag>
</template>
```
## Events
目前提供了几个方法,不过只是`beta`版本,之后很可能会修改。
```js
this.$refs.TreeTable.addChild(row, data) //添加子元素
this.$refs.TreeTable.addBrother(row, data) //添加兄弟元素
this.$refs.TreeTable.delete(row) //删除该元素
```
## 其他
如果有其他的需求,请参考[el-table](http://element-cn.eleme.io/#/en-US/component/table)的 api 自行修改 index.vue
# English
## Brief
This component only provides a solution for creating `TreeTable`. It is based on the `element-ui` table component. It uses the `row-style` method of `el-table` to determine whether the element needs to be hidden or displayed.
And this component makes full use of the features of the `vue` slot to make it user-friendly.
In `evel.js`, the `addAttrs` method adds several properties to the data, and `treeTotable` flattens the array. None of these operations will destroy the source data, just add properties.
## Props
| Attribute | Description | Type | Default |
| :--------------: | :----------------------------------------------------------- | :-----: | :------: |
| data | original display data | Array | [] |
| columns | column attribute | Array | [] |
| defaultExpandAll | whether to expand all nodes by default | Boolean | false |
| defaultChildren | specify which node object is used as the node's subtree | String | children | |
| indent | horizontal indentation of nodes in adjacent levels in pixels | Number | 50 |
> Any of the `el-table` properties are supported, such as `border`, `fit`, `size` or `@select`, `@cell-click`. See the ʻel-table` documentation for details.
---
### Example
```html
<tree-table :data="data" :columns="columns" border>
```
#### data(**Required**)
```js
const data = [
{
name:'1'
children: [
{
name: '1-1'
},
{
key1: value1
}]
}
```
name: '1-2'
}
]
},
{
name: `2`
}
]
```
#### columns
列属性,要求是一个数组
#### columns(**Required**)
1. text: 显示在表头的文字
2. value: 对应data的key。treeTable将显示相应的value
3. width: 每列的宽度,为一个数字(可选)
- label: text displayed in the header
- key: data.key will show in column
- expand: `true` or `false`
- checkbox: `true` or `false`
- width: column width 。such as `200`
- align: alignment `left/center/right`
- header-align: alignment of the table header `left/center/right`
如果你想要每个字段都有自定义的样式或者嵌套其他组件columns可不提供直接像在el-table一样写即可如果没有自定义内容提供columns将更加的便捷方便
```javascript
const columns = [
{
label: 'Checkbox',
checkbox: true
},
{
label: '',
key: 'id',
expand: true
},
{
label: 'Event',
key: 'event',
width: 200,
align: 'left'
},
{
label: 'Scope',
key: 'scope'
}
]
```
如果你有几个字段是需要自定义的几个不需要那么可以将不需要自定义的字段放入columns将需要自定义的内容放入到slot中详情见后文
```javascript
[{
value:string,
text:string,
width:number
},{
value:string,
text:string,
width:number
}]
```
> The tree table component will generate a named slot based on the key property of columns. If you need to customize the column data, you can do it through the slot.
#### expandAll
是否默认全部展开boolean值默认为false
```html
<template slot="your key" slot-scope="{scope}">
<el-tag>level: {{ scope.row._level }}</el-tag>
<el-tag>expand: {{ scope.row._expand }}</el-tag>
<el-tag>select: {{ scope.row._select }}</el-tag>
</template>
```
#### evalFunc
解析函数function非必须
## Events
如果不提供,将使用默认的[evalFunc](./eval.js)
Several methods are currently available, but only the `beta` version, which is likely to be modified later.
如果提供了evalFunc,那么会用提供的evalFunc去解析data并返回treeTable渲染所需要的值。如何编写一个evalFunc请参考[*eval.js*](https://github.com/PanJiaChen/vue-element-admin/blob/master/src/components/TreeTable/eval.js)或[*customEval.js*](https://github.com/PanJiaChen/vue-element-admin/blob/master/src/views/table/treeTable/customEval.js)
```js
this.$refs.TreeTable.addChild(row, data) //Add child elements
this.$refs.TreeTable.addBrother(row, data) //Add a sibling element
this.$refs.TreeTable.delete(row) //Delete the element
```
#### evalArgs
解析函数的参数,是一个数组
## Other
**请注意自定义的解析函数参数第一个为this.data第二个参数为 this.expandAll,你不需要在evalArgs填写。一定记住这两个参数是强制性的并且位置不可颠倒** *this.data为需要解析的数据this.expandAll为是否默认展开*
如你的解析函数需要的参数为`(this.data, this.expandAll,1,2,3,4)`,那么你只需要将`[1,2,3,4]`赋值给`evalArgs`就可以了
如果你的解析函数参数只有`(this.data, this.expandAll)`,那么就可以不用填写evalArgs了
具体可参考[*customEval.js*](https://github.com/PanJiaChen/vue-element-admin/blob/master/src/views/table/treeTable/customEval.js)的函数参数和[customTreeTable](https://github.com/PanJiaChen/vue-element-admin/blob/master/src/views/table/treeTable/customTreeTable.vue)的`evalArgs`属性值
## slot
这是一个自定义列的插槽。
默认情况下treeTable只有一行行展示数据的功能。但是一般情况下我们会要给行加上一个操作按钮或者根据当行数据展示不同的样式这时我们就需要自定义列了。请参考[customTreeTable](https://github.com/PanJiaChen/vue-element-admin/blob/master/src/views/table/treeTable/customTreeTable.vue)[实例效果](https://panjiachen.github.io/vue-element-admin/#/table/tree-table)
`slot`和`columns属性`可同时存在,columns里面的数据列会在slot自定义列的左边展示
## 其他
如果有其他的需求,请参考[el-table](http://element-cn.eleme.io/#/en-US/component/table)的api自行修改index.vue
If you have other requirements, please refer to the [el-table](http://element-cn.eleme.io/#/en-US/component/table) api to modify the index.vue

View File

@ -0,0 +1,42 @@
import { addResizeListener, removeResizeListener } from 'element-ui/src/utils/resize-event'
/**
* How to use
* <el-table height="100px" v-el-height-adaptive-table="{bottomOffset: 30}">...</el-table>
* el-table height is must be set
* bottomOffset: 30(default) // The height of the table from the bottom of the page.
*/
const doResize = (el, binding, vnode) => {
const { componentInstance: $table } = vnode
const { value } = binding
if (!$table.height) {
throw new Error(`el-$table must set the height. Such as height='100px'`)
}
const bottomOffset = (value && value.bottomOffset) || 30
if (!$table) return
const height = window.innerHeight - el.getBoundingClientRect().top - bottomOffset
$table.layout.setHeight(height)
$table.doLayout()
}
export default {
bind(el, binding, vnode) {
el.resizeListener = () => {
doResize(el, binding, vnode)
}
addResizeListener(el, el.resizeListener)
},
inserted(el, binding, vnode) {
doResize(el, binding, vnode)
},
unbind(el) {
removeResizeListener(el, el.resizeListener)
}
}

View File

@ -0,0 +1,14 @@
import adaptive from './adaptive'
const install = function(Vue) {
Vue.directive('el-height-adaptive-table', adaptive)
}
if (window.Vue) {
window['el-height-adaptive-table'] = adaptive
Vue.use(install); // eslint-disable-line
}
adaptive.install = install
export default adaptive

View File

@ -1,42 +1,73 @@
import './waves.css'
export default {
bind(el, binding) {
el.addEventListener('click', e => {
const customOpts = Object.assign({}, binding.value)
const opts = Object.assign({
const context = '@@wavesContext'
function handleClick(el, binding) {
function handle(e) {
const customOpts = Object.assign({}, binding.value)
const opts = Object.assign(
{
ele: el, // 波纹作用元素
type: 'hit', // hit 点击位置扩散 center中心点扩展
color: 'rgba(0, 0, 0, 0.15)' // 波纹颜色
}, customOpts)
const target = opts.ele
if (target) {
target.style.position = 'relative'
target.style.overflow = 'hidden'
const rect = target.getBoundingClientRect()
let ripple = target.querySelector('.waves-ripple')
if (!ripple) {
ripple = document.createElement('span')
ripple.className = 'waves-ripple'
ripple.style.height = ripple.style.width = Math.max(rect.width, rect.height) + 'px'
target.appendChild(ripple)
} else {
ripple.className = 'waves-ripple'
}
switch (opts.type) {
case 'center':
ripple.style.top = (rect.height / 2 - ripple.offsetHeight / 2) + 'px'
ripple.style.left = (rect.width / 2 - ripple.offsetWidth / 2) + 'px'
break
default:
ripple.style.top = (e.pageY - rect.top - ripple.offsetHeight / 2 - document.documentElement.scrollTop || document.body.scrollTop) + 'px'
ripple.style.left = (e.pageX - rect.left - ripple.offsetWidth / 2 - document.documentElement.scrollLeft || document.body.scrollLeft) + 'px'
}
ripple.style.backgroundColor = opts.color
ripple.className = 'waves-ripple z-active'
return false
},
customOpts
)
const target = opts.ele
if (target) {
target.style.position = 'relative'
target.style.overflow = 'hidden'
const rect = target.getBoundingClientRect()
let ripple = target.querySelector('.waves-ripple')
if (!ripple) {
ripple = document.createElement('span')
ripple.className = 'waves-ripple'
ripple.style.height = ripple.style.width = Math.max(rect.width, rect.height) + 'px'
target.appendChild(ripple)
} else {
ripple.className = 'waves-ripple'
}
}, false)
switch (opts.type) {
case 'center':
ripple.style.top = rect.height / 2 - ripple.offsetHeight / 2 + 'px'
ripple.style.left = rect.width / 2 - ripple.offsetWidth / 2 + 'px'
break
default:
ripple.style.top =
(e.pageY - rect.top - ripple.offsetHeight / 2 - document.documentElement.scrollTop ||
document.body.scrollTop) + 'px'
ripple.style.left =
(e.pageX - rect.left - ripple.offsetWidth / 2 - document.documentElement.scrollLeft ||
document.body.scrollLeft) + 'px'
}
ripple.style.backgroundColor = opts.color
ripple.className = 'waves-ripple z-active'
return false
}
}
if (!el[context]) {
el[context] = {
removeHandle: handle
}
} else {
el[context].removeHandle = handle
}
return handle
}
export default {
bind(el, binding) {
el.addEventListener('click', handleClick(el, binding), false)
},
update(el, binding) {
el.removeEventListener('click', el[context].removeHandle, false)
el.addEventListener('click', handleClick(el, binding), false)
},
unbind(el) {
el.removeEventListener('click', el[context].removeHandle, false)
el[context] = null
delete el[context]
}
}

View File

@ -0,0 +1 @@
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M44.8 0h79.543C126.78 0 128 1.422 128 4.267v23.466c0 2.845-1.219 4.267-3.657 4.267H44.8c-2.438 0-3.657-1.422-3.657-4.267V4.267C41.143 1.422 42.362 0 44.8 0zm22.857 48h56.686c2.438 0 3.657 1.422 3.657 4.267v23.466c0 2.845-1.219 4.267-3.657 4.267H67.657C65.22 80 64 78.578 64 75.733V52.267C64 49.422 65.219 48 67.657 48zm0 48h56.686c2.438 0 3.657 1.422 3.657 4.267v23.466c0 2.845-1.219 4.267-3.657 4.267H67.657C65.22 128 64 126.578 64 123.733v-23.466C64 97.422 65.219 96 67.657 96zM50.286 68.267c2.02 0 3.657-1.91 3.657-4.267 0-2.356-1.638-4.267-3.657-4.267H17.37V32h6.4c2.02 0 3.658-1.91 3.658-4.267V4.267C27.429 1.91 25.79 0 23.77 0H3.657C1.637 0 0 1.91 0 4.267v23.466C0 30.09 1.637 32 3.657 32h6.4v80c0 2.356 1.638 4.267 3.657 4.267h36.572c2.02 0 3.657-1.91 3.657-4.267 0-2.356-1.638-4.267-3.657-4.267H17.37V68.267h32.915z"/></svg>

After

Width:  |  Height:  |  Size: 906 B

View File

@ -6,6 +6,7 @@ export default {
guide: 'Guide',
permission: 'Permission',
pagePermission: 'Page Permission',
rolePermission: 'Role Permission',
directivePermission: 'Directive Permission',
icons: 'Icons',
components: 'Components',
@ -56,6 +57,7 @@ export default {
excel: 'Excel',
exportExcel: 'Export Excel',
selectExcel: 'Export Selected',
mergeHeader: 'Merge Header',
uploadExcel: 'Upload Excel',
zip: 'Zip',
pdf: 'PDF',
@ -86,9 +88,14 @@ export default {
github: 'Github Repository'
},
permission: {
addRole: 'New Role',
editPermission: 'Edit Permission',
roles: 'Your roles',
switchRoles: 'Switch roles',
tips: 'In some cases it is not suitable to use v-permission, such as element Tab component or el-table-column and other asynchronous rendering dom cases which can only be achieved by manually setting the v-if.'
tips: 'In some cases it is not suitable to use v-permission, such as element Tab component or el-table-column and other asynchronous rendering dom cases which can only be achieved by manually setting the v-if.',
delete: 'Delete',
confirm: 'Confirm',
cancel: 'Cancel'
},
guide: {
description: 'The guide page is useful for some people who entered the project for the first time. You can briefly introduce the features of the project. Demo is based on ',

View File

@ -5,6 +5,7 @@ export default {
documentation: 'Documentación',
guide: 'Guía',
permission: 'Permisos',
rolePermission: 'Permisos de rol',
pagePermission: 'Permisos de la página',
directivePermission: 'Permisos de la directiva',
icons: 'Iconos',
@ -56,6 +57,7 @@ export default {
excel: 'Excel',
exportExcel: 'Exportar a Excel',
selectExcel: 'Export seleccionado',
mergeHeader: 'Merge Header',
uploadExcel: 'Subir Excel',
zip: 'Zip',
pdf: 'PDF',
@ -86,9 +88,14 @@ export default {
github: 'Repositorio Github'
},
permission: {
addRole: 'Nuevo rol',
editPermission: 'Permiso de edición',
roles: 'Tus permisos',
switchRoles: 'Cambiar permisos',
tips: 'In some cases it is not suitable to use v-permission, such as element Tab component or el-table-column and other asynchronous rendering dom cases which can only be achieved by manually setting the v-if.'
tips: 'In some cases it is not suitable to use v-permission, such as element Tab component or el-table-column and other asynchronous rendering dom cases which can only be achieved by manually setting the v-if.',
delete: 'Borrar',
confirm: 'Confirmar',
cancel: 'Cancelar'
},
guide: {
description: 'The guide page is useful for some people who entered the project for the first time. You can briefly introduce the features of the project. Demo is based on ',

View File

@ -5,6 +5,7 @@ export default {
documentation: '文档',
guide: '引导页',
permission: '权限测试页',
rolePermission: '角色权限',
pagePermission: '页面权限',
directivePermission: '指令权限',
icons: '图标',
@ -54,9 +55,10 @@ export default {
page404: '404',
errorLog: '错误日志',
excel: 'Excel',
exportExcel: 'Export Excel',
selectExcel: 'Export Selected',
uploadExcel: 'Upload Excel',
exportExcel: '导出 Excel',
selectExcel: '导出 已选择项',
mergeHeader: '导出 多级表头',
uploadExcel: '上传 Excel',
zip: 'Zip',
pdf: 'PDF',
exportZip: 'Export Zip',
@ -86,9 +88,14 @@ export default {
github: 'Github 地址'
},
permission: {
addRole: '新增角色',
editPermission: '编辑权限',
roles: '你的权限',
switchRoles: '切换权限',
tips: '在某些情况下,不适合使用 v-permission。例如Element-UI 的 Tab 组件或 el-table-column 以及其它动态渲染 dom 的场景。你只能通过手动设置 v-if 来实现。'
tips: '在某些情况下,不适合使用 v-permission。例如Element-UI 的 Tab 组件或 el-table-column 以及其它动态渲染 dom 的场景。你只能通过手动设置 v-if 来实现。',
delete: '删除',
confirm: '确定',
cancel: '取消'
},
guide: {
description: '引导页对于一些第一次进入项目的人很有用,你可以简单介绍下项目的功能。本 Demo 是基于',

View File

@ -3,6 +3,7 @@ import loginAPI from './login'
import articleAPI from './article'
import remoteSearchAPI from './remoteSearch'
import transactionAPI from './transaction'
import roleAPI from './role'
// 修复在使用 MockJS 情况下,设置 withCredentials = true且未被拦截的跨域请求丢失 Cookies 的问题
// https://github.com/nuysoft/Mock/issues/300
@ -23,6 +24,13 @@ Mock.mock(/\/login\/login/, 'post', loginAPI.loginByUsername)
Mock.mock(/\/login\/logout/, 'post', loginAPI.logout)
Mock.mock(/\/user\/info\.*/, 'get', loginAPI.getUserInfo)
// 角色相关
Mock.mock(/\/routes/, 'get', roleAPI.getRoutes)
Mock.mock(/\/roles/, 'get', roleAPI.getRoles)
Mock.mock(/\/roles$/, 'post', roleAPI.addRole)
Mock.mock(/\/roles\/[A-Za-z0-9]+/, 'put', roleAPI.updateRole)
Mock.mock(/\/roles\/[A-Za-z0-9]+/, 'delete', roleAPI.deleteRole)
// 文章相关
Mock.mock(/\/article\/list/, 'get', articleAPI.getList)
Mock.mock(/\/article\/detail/, 'get', articleAPI.getArticle)

61
src/mock/role.js Normal file
View File

@ -0,0 +1,61 @@
import Mock from 'mockjs'
import { deepClone } from '@/utils'
import { filterAsyncRoutes } from '@/store/modules/permission'
import { asyncRoutes, constantRoutes } from '@/router'
const routes = deepClone([...constantRoutes, ...asyncRoutes])
const roles = [
{
key: 'admin',
name: 'admin',
description: 'Super Administrator. Have access to view all pages.',
routes: routes
},
{
key: 'editor',
name: 'editor',
description: 'Normal Editor. Can see all pages except permission page',
routes: filterAsyncRoutes(routes, ['editor'])
},
{
key: 'visitor',
name: 'visitor',
description: 'Just a visitor. Can only see the home page and the document page',
routes: [{
path: '',
redirect: 'dashboard',
children: [
{
path: 'dashboard',
name: 'Dashboard',
meta: { title: 'dashboard', icon: 'dashboard' }
}
]
}]
}
]
export default {
getRoutes() {
return routes
},
getRoles() {
return roles
},
addRole() {
return Mock.mock('@integer(300, 5000)')
},
updateRole() {
const res = {
data: 'success'
}
return res
},
deleteRole() {
const res = {
data: 'success'
}
return res
}
}

View File

@ -2,41 +2,49 @@ import router from './router'
import store from './store'
import { Message } from 'element-ui'
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css'// progress bar style
import { getToken } from '@/utils/auth' // getToken from cookie
import 'nprogress/nprogress.css' // progress bar style
import { getToken } from '@/utils/auth' // get token from cookie
NProgress.configure({ showSpinner: false })// NProgress Configuration
NProgress.configure({ showSpinner: false }) // NProgress Configuration
// permission judge function
function hasPermission(roles, permissionRoles) {
if (roles.indexOf('admin') >= 0) return true // admin permission passed directly
if (roles.includes('admin')) return true // admin permission passed directly
if (!permissionRoles) return true
return roles.some(role => permissionRoles.indexOf(role) >= 0)
}
const whiteList = ['/login', '/auth-redirect']// no redirect whitelist
const whiteList = ['/login', '/auth-redirect'] // no redirect whitelist
router.beforeEach((to, from, next) => {
NProgress.start() // start progress bar
if (getToken()) { // determine if there has token
if (getToken()) {
// determine if there has token
/* has token*/
if (to.path === '/login') {
next({ path: '/' })
NProgress.done() // if current page is dashboard will not trigger afterEach hook, so manually handle it
} else {
if (store.getters.roles.length === 0) { // 判断当前用户是否已拉取完user_info信息
store.dispatch('GetUserInfo').then(res => { // 拉取user_info
const roles = res.data.roles // note: roles must be a array! such as: ['editor','develop']
store.dispatch('GenerateRoutes', { roles }).then(() => { // 根据roles权限生成可访问的路由表
router.addRoutes(store.getters.addRouters) // 动态添加可访问路由表
next({ ...to, replace: true }) // hack方法 确保addRoutes已完成 ,set the replace: true so the navigation will not leave a history record
if (store.getters.roles.length === 0) {
// 判断当前用户是否已拉取完user_info信息
store
.dispatch('GetUserInfo')
.then(res => {
// 拉取user_info
const roles = res.data.roles // note: roles must be a object array! such as: [{id: '1', name: 'editor'}, {id: '2', name: 'developer'}]
store.dispatch('GenerateRoutes', { roles }).then(accessRoutes => {
// 根据roles权限生成可访问的路由表
router.addRoutes(accessRoutes) // 动态添加可访问路由表
next({ ...to, replace: true }) // hack方法 确保addRoutes已完成 ,set the replace: true so the navigation will not leave a history record
})
})
}).catch((err) => {
store.dispatch('FedLogOut').then(() => {
Message.error(err)
next({ path: '/' })
.catch(err => {
store.dispatch('FedLogOut').then(() => {
Message.error(err)
next({ path: '/' })
})
})
})
} else {
// 没有动态改变权限的需求可直接next() 删除下方权限判断 ↓
if (hasPermission(store.getters.roles, to.meta.roles)) {
@ -49,7 +57,8 @@ router.beforeEach((to, from, next) => {
}
} else {
/* has no token*/
if (whiteList.indexOf(to.path) !== -1) { // 在免登录白名单,直接进入
if (whiteList.indexOf(to.path) !== -1) {
// 在免登录白名单,直接进入
next()
} else {
next(`/login?redirect=${to.path}`) // 否则全部重定向到登录页

View File

@ -10,6 +10,7 @@ import Layout from '@/views/layout/Layout'
import componentsRouter from './modules/components'
import chartsRouter from './modules/charts'
import tableRouter from './modules/table'
import treeTableRouter from './modules/tree-table'
import nestedRouter from './modules/nested'
/** note: sub-menu only appear when children.length>=1
@ -32,7 +33,7 @@ import nestedRouter from './modules/nested'
affix: true if true, the tag will affix in the tags-view
}
**/
export const constantRouterMap = [
export const constantRoutes = [
{
path: '/redirect',
component: Layout,
@ -80,7 +81,6 @@ export const constantRouterMap = [
{
path: '/documentation',
component: Layout,
redirect: '/documentation/index',
children: [
{
path: 'index',
@ -108,10 +108,10 @@ export const constantRouterMap = [
export default new Router({
// mode: 'history', // require service support
scrollBehavior: () => ({ y: 0 }),
routes: constantRouterMap
routes: constantRoutes
})
export const asyncRouterMap = [
export const asyncRoutes = [
{
path: '/permission',
component: Layout,
@ -140,6 +140,15 @@ export const asyncRouterMap = [
title: 'directivePermission'
// if do not set roles, means: this page does not require permission
}
},
{
path: 'role',
component: () => import('@/views/permission/role'),
name: 'RolePermission',
meta: {
title: 'rolePermission',
roles: ['admin']
}
}
]
},
@ -162,6 +171,7 @@ export const asyncRouterMap = [
chartsRouter,
nestedRouter,
tableRouter,
treeTableRouter,
{
path: '/example',
@ -269,6 +279,12 @@ export const asyncRouterMap = [
name: 'SelectExcel',
meta: { title: 'selectExcel' }
},
{
path: 'export-merge-header',
component: () => import('@/views/excel/mergeHeader'),
name: 'MergeHeader',
meta: { title: 'mergeHeader' }
},
{
path: 'upload-excel',
component: () => import('@/views/excel/uploadExcel'),

View File

@ -30,18 +30,6 @@ const tableRouter = {
name: 'InlineEditTable',
meta: { title: 'inlineEditTable' }
},
{
path: 'tree-table',
component: () => import('@/views/table/treeTable/treeTable'),
name: 'TreeTableDemo',
meta: { title: 'treeTable' }
},
{
path: 'custom-tree-table',
component: () => import('@/views/table/treeTable/customTreeTable'),
name: 'CustomTreeTableDemo',
meta: { title: 'customTreeTable' }
},
{
path: 'complex-table',
component: () => import('@/views/table/complexTable'),

View File

@ -0,0 +1,29 @@
/** When your routing table is too long, you can split it into small modules**/
import Layout from '@/views/layout/Layout'
const treeTableRouter = {
path: '/tree-table',
component: Layout,
redirect: '/table/complex-table',
name: 'TreeTable',
meta: {
title: 'treeTable',
icon: 'tree-table'
},
children: [
{
path: 'index',
component: () => import('@/views/tree-table/index'),
name: 'TreeTableDemo',
meta: { title: 'treeTable' }
},
{
path: 'custom',
component: () => import('@/views/tree-table/custom'),
name: 'CustomTreeTableDemo',
meta: { title: 'customTreeTable' }
}
]
}
export default treeTableRouter

View File

@ -12,8 +12,8 @@ const getters = {
status: state => state.user.status,
roles: state => state.user.roles,
setting: state => state.user.setting,
permission_routers: state => state.permission.routers,
addRouters: state => state.permission.addRouters,
permission_routes: state => state.permission.routes,
addRoutes: state => state.permission.addRoutes,
errorLogs: state => state.errorLog.logs
}
export default getters

View File

@ -1,4 +1,4 @@
import { asyncRouterMap, constantRouterMap } from '@/router'
import { asyncRoutes, constantRoutes } from '@/router'
/**
* 通过meta.role判断是否与当前用户权限匹配
@ -15,17 +15,17 @@ function hasPermission(roles, route) {
/**
* 递归过滤异步路由表返回符合用户角色权限的路由表
* @param routes asyncRouterMap
* @param routes asyncRoutes
* @param roles
*/
function filterAsyncRouter(routes, roles) {
export function filterAsyncRoutes(routes, roles) {
const res = []
routes.forEach(route => {
const tmp = { ...route }
if (hasPermission(roles, tmp)) {
if (tmp.children) {
tmp.children = filterAsyncRouter(tmp.children, roles)
tmp.children = filterAsyncRoutes(tmp.children, roles)
}
res.push(tmp)
}
@ -36,27 +36,27 @@ function filterAsyncRouter(routes, roles) {
const permission = {
state: {
routers: [],
addRouters: []
routes: [],
addRoutes: []
},
mutations: {
SET_ROUTERS: (state, routers) => {
state.addRouters = routers
state.routers = constantRouterMap.concat(routers)
SET_ROUTES: (state, routes) => {
state.addRoutes = routes
state.routes = constantRoutes.concat(routes)
}
},
actions: {
GenerateRoutes({ commit }, data) {
return new Promise(resolve => {
const { roles } = data
let accessedRouters
let accessedRoutes
if (roles.includes('admin')) {
accessedRouters = asyncRouterMap
accessedRoutes = asyncRoutes
} else {
accessedRouters = filterAsyncRouter(asyncRouterMap, roles)
accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
}
commit('SET_ROUTERS', accessedRouters)
resolve()
commit('SET_ROUTES', accessedRoutes)
resolve(accessedRoutes)
})
}
}

View File

@ -83,19 +83,26 @@
.hideSidebar {
.sidebar-container {
width: 36px !important;
width: 54px !important;
}
.main-container {
margin-left: 36px;
margin-left: 54px;
}
.svg-icon {
margin-right: 0px;
}
.submenu-title-noDropdown {
padding-left: 10px !important;
padding: 0 !important;
position: relative;
.el-tooltip {
padding: 0 10px !important;
padding: 0 !important;
.svg-icon {
margin-left: 20px;
}
}
}
@ -103,7 +110,10 @@
overflow: hidden;
&>.el-submenu__title {
padding-left: 10px !important;
padding: 0 !important;
.svg-icon {
margin-left: 20px;
}
.el-submenu__icon-arrow {
display: none;

View File

@ -19,9 +19,10 @@ $menuHover:#263445;
$subMenuBg:#1f2d3d;
$subMenuHover:#001528;
$sideBarWidth: 180px;
$sideBarWidth: 210px;
// the :export directive is the magic sauce for webpack
// https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass
:export {
menuText: $menuText;
menuActiveText: $menuActiveText;

View File

@ -136,7 +136,8 @@ export function param2Obj(url) {
decodeURIComponent(search)
.replace(/"/g, '\\"')
.replace(/&/g, '","')
.replace(/=/g, '":"') +
.replace(/=/g, '":"')
.replace(/\+/g, ' ') +
'"}'
)
}

View File

@ -145,9 +145,11 @@ export function export_table_to_excel(id) {
}
export function export_json_to_excel({
multiHeader,
header,
data,
filename,
merges,
autoWidth = true,
bookType= 'xlsx'
} = {}) {
@ -155,10 +157,22 @@ export function export_json_to_excel({
filename = filename || 'excel-list'
data = [...data]
data.unshift(header);
for (let header of multiHeader) {
data.unshift(header)
}
var ws_name = "SheetJS";
var wb = new Workbook(),
ws = sheet_from_array_of_arrays(data);
if (merges.length > 0) {
if (!ws['!merges']) ws['!merges'] = [];
merges.forEach(item => {
ws['!merges'].push(XLSX.utils.decode_range(item))
})
}
if (autoWidth) {
/*设置worksheet每列的最大宽度*/
const colWidth = data.map(row => row.map(val => {

View File

@ -46,12 +46,10 @@
<script>
import { fetchList } from '@/api/article'
import { parseTime } from '@/utils'
// options components
import FilenameOption from './components/FilenameOption'
import AutoWidthOption from './components/AutoWidthOption'
import BookTypeOption from './components/BookTypeOption'
export default {
name: 'ExportExcel',
components: { FilenameOption, AutoWidthOption, BookTypeOption },
@ -114,4 +112,3 @@ export default {
padding: 0 12px 0 30px;
}
</style>

View File

@ -0,0 +1,101 @@
<template>
<div class="app-container">
<el-button :loading="downloadLoading" style="margin-bottom:20px" type="primary" icon="document" @click="handleDownload">Export</el-button>
<el-table
ref="multipleTable"
v-loading="listLoading"
:data="list"
element-loading-text="Loading"
border
fit
highlight-current-row
>
<el-table-column align="center" label="Id" width="95">
<template slot-scope="scope">
{{ scope.$index }}
</template>
</el-table-column>
<el-table-column label="Main Information" align="center">
<el-table-column label="Title">
<template slot-scope="scope">
{{ scope.row.title }}
</template>
</el-table-column>
<el-table-column label="Author" width="110" align="center">
<template slot-scope="scope">
<el-tag>{{ scope.row.author }}</el-tag>
</template>
</el-table-column>
<el-table-column label="Readings" width="115" align="center">
<template slot-scope="scope">
{{ scope.row.pageviews }}
</template>
</el-table-column>
</el-table-column>
<el-table-column align="center" label="Date" width="220">
<template slot-scope="scope">
<i class="el-icon-time" />
<span>{{ scope.row.timestamp | parseTime('{y}-{m}-{d} {h}:{i}') }}</span>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script>
import { fetchList } from '@/api/article'
import { parseTime } from '@/utils'
export default {
name: 'MergeHeader',
data() {
return {
list: null,
listLoading: true,
downloadLoading: false
}
},
created() {
this.fetchData()
},
methods: {
fetchData() {
this.listLoading = true
fetchList(this.listQuery).then(response => {
this.list = response.data.items
this.listLoading = false
})
},
handleDownload() {
this.downloadLoading = true
import('@/vendor/Export2Excel').then(excel => {
const multiHeader = [['Id', 'Main Information', '', '', 'Date']]
const header = ['', 'Title', 'Author', 'Readings', '']
const filterVal = ['id', 'title', 'author', 'pageviews', 'display_time']
const list = this.list
const data = this.formatJson(filterVal, list)
const merges = ['A1:A2', 'B1:D1', 'E1:E2']
excel.export_json_to_excel({
multiHeader,
header,
merges,
data
})
this.downloadLoading = false
})
},
formatJson(filterVal, jsonData) {
return jsonData.map(v => filterVal.map(j => {
if (j === 'timestamp') {
return parseTime(v[j])
} else {
return v[j]
}
}))
}
}
}
</script>

View File

@ -9,7 +9,7 @@
</app-link>
</template>
<el-submenu v-else ref="subMenu" :index="resolvePath(item.path)">
<el-submenu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body>
<template slot="title">
<item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="generateTitle(item.meta.title)" />
</template>

View File

@ -6,9 +6,10 @@
:background-color="variables.menuBg"
:text-color="variables.menuText"
:active-text-color="variables.menuActiveText"
:collapse-transition="false"
mode="vertical"
>
<sidebar-item v-for="route in permission_routers" :key="route.path" :item="route" :base-path="route.path" />
<sidebar-item v-for="route in permission_routes" :key="route.path" :item="route" :base-path="route.path" />
</el-menu>
</el-scrollbar>
</template>
@ -22,7 +23,7 @@ export default {
components: { SidebarItem },
computed: {
...mapGetters([
'permission_routers',
'permission_routes',
'sidebar'
]),
variables() {

View File

@ -47,8 +47,8 @@ export default {
visitedViews() {
return this.$store.state.tagsView.visitedViews
},
routers() {
return this.$store.state.permission.routers
routes() {
return this.$store.state.permission.routes
}
},
watch: {
@ -95,7 +95,7 @@ export default {
return tags
},
initTags() {
const affixTags = this.affixTags = this.filterAffixTags(this.routers)
const affixTags = this.affixTags = this.filterAffixTags(this.routes)
for (const tag of affixTags) {
// Must have tag name
if (tag.name) {

View File

@ -0,0 +1,268 @@
<template>
<div class="app-container">
<el-button type="primary" @click="handleAddRole">{{ $t('permission.addRole') }}</el-button>
<el-table :data="rolesList" style="width: 100%;margin-top:30px;" border>
<el-table-column align="center" label="Role Key" width="220">
<template slot-scope="scope">{{ scope.row.key }}</template>
</el-table-column>
<el-table-column align="center" label="Role Name" width="220">
<template slot-scope="scope">{{ scope.row.name }}</template>
</el-table-column>
<el-table-column align="header-center" label="Description">
<template slot-scope="scope">{{ scope.row.description }}</template>
</el-table-column>
<el-table-column align="center" label="Operations">
<template slot-scope="scope">
<el-button type="primary" size="small" @click="handleEdit(scope)">{{ $t('permission.editPermission') }}</el-button>
<el-button type="danger" size="small" @click="handleDelete(scope)">{{ $t('permission.delete') }}</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog :visible.sync="dialogVisible" :title="dialogType==='edit'?'Edit Role':'New Role'">
<el-form :model="role" label-width="80px" label-position="left">
<el-form-item label="Name">
<el-input v-model="role.name" placeholder="Role Name" />
</el-form-item>
<el-form-item label="Desc">
<el-input
v-model="role.description"
:autosize="{ minRows: 2, maxRows: 4}"
type="textarea"
placeholder="Role Description"
/>
</el-form-item>
<el-form-item label="Menus">
<el-tree ref="tree" :check-strictly="checkStrictly" :data="routesData" :props="defaultProps" show-checkbox node-key="path" class="permission-tree" />
</el-form-item>
</el-form>
<div style="text-align:right;">
<el-button type="danger" @click="dialogVisible=false">{{ $t('permission.cancel') }}</el-button>
<el-button type="primary" @click="confirmRole">{{ $t('permission.confirm') }}</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import path from 'path'
import { deepClone } from '@/utils'
import { getRoutes, getRoles, addRole, deleteRole, updateRole } from '@/api/role'
import i18n from '@/lang'
const defaultRole = {
key: '',
name: '',
description: '',
routes: []
}
export default {
data() {
return {
role: Object.assign({}, defaultRole),
routes: [],
rolesList: [],
dialogVisible: false,
dialogType: 'new',
checkStrictly: false,
defaultProps: {
children: 'children',
label: 'title'
}
}
},
computed: {
routesData() {
return this.routes
}
},
created() {
// Mock: get all routes and roles list from server
this.getRoutes()
this.getRoles()
},
methods: {
async getRoutes() {
const res = await getRoutes()
this.serviceRoutes = res.data
const routes = this.generateRoutes(res.data)
this.routes = this.i18n(routes)
},
async getRoles() {
const res = await getRoles()
this.rolesList = res.data
},
i18n(routes) {
const app = routes.map(route => {
route.title = i18n.t(`route.${route.title}`)
if (route.children) {
route.children = this.i18n(route.children)
}
return route
})
return app
},
// Reshape the routes structure so that it looks the same as the sidebar
generateRoutes(routes, basePath = '/') {
const res = []
for (let route of routes) {
// skip some route
if (route.hidden) { continue }
const onlyOneShowingChild = this.onlyOneShowingChild(route.children, route)
if (route.children && onlyOneShowingChild && !route.alwaysShow) {
route = onlyOneShowingChild
}
const data = {
path: path.resolve(basePath, route.path),
title: route.meta && route.meta.title
}
// recursive child routes
if (route.children) {
data.children = this.generateRoutes(route.children, data.path)
}
res.push(data)
}
return res
},
generateArr(routes) {
let data = []
routes.forEach(route => {
data.push(route)
if (route.children) {
const temp = this.generateArr(route.children)
if (temp.length > 0) {
data = [...data, ...temp]
}
}
})
return data
},
handleAddRole() {
this.role = Object.assign({}, defaultRole)
if (this.$refs.tree) {
this.$refs.tree.setCheckedNodes([])
}
this.dialogType = 'new'
this.dialogVisible = true
},
handleEdit(scope) {
this.dialogType = 'edit'
this.dialogVisible = true
this.checkStrictly = true
this.role = deepClone(scope.row)
this.$nextTick(() => {
const routes = this.generateRoutes(this.role.routes)
this.$refs.tree.setCheckedNodes(this.generateArr(routes))
// set checked state of a node not affects its father and child nodes
this.checkStrictly = false
})
},
handleDelete({ $index, row }) {
this.$confirm('Confirm to remove the role?', 'Warning', {
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
type: 'warning'
})
.then(async() => {
await deleteRole(row.id)
this.rolesList.splice($index, 1)
this.$message({
type: 'success',
message: 'Delete succed!'
})
})
.catch(err => { console.error(err) })
},
generateTree(routes, basePath = '/', checkedKeys) {
const res = []
for (const route of routes) {
const routePath = path.resolve(basePath, route.path)
// recursive child routes
if (route.children) {
route.children = this.generateTree(route.children, routePath, checkedKeys)
}
if (checkedKeys.includes(routePath) || (route.children && route.children.length >= 1)) {
res.push(route)
}
}
return res
},
async confirmRole() {
const isEdit = this.dialogType === 'edit'
const checkedKeys = this.$refs.tree.getCheckedKeys()
this.role.routes = this.generateTree(deepClone(this.serviceRoutes), '/', checkedKeys)
if (isEdit) {
await updateRole(this.role.key, this.role)
for (let index = 0; index < this.rolesList.length; index++) {
if (this.rolesList[index].key === this.role.key) {
this.rolesList.splice(index, 1, Object.assign({}, this.role))
break
}
}
} else {
const { data } = await addRole(this.role)
this.role.key = data
this.rolesList.push(this.role)
}
const { description, key, name } = this.role
this.dialogVisible = false
this.$notify({
title: 'Success',
dangerouslyUseHTMLString: true,
message: `
<div>Role Key: ${key}</div>
<div>Role Nmae: ${name}</div>
<div>Description: ${description}</div>
`,
type: 'success'
})
},
// reference: src/view/layout/components/Sidebar/SidebarItem.vue
onlyOneShowingChild(children = [], parent) {
let onlyOneChild = null
const showingChildren = children.filter(item => !item.hidden)
// When there is only one child route, the child route is displayed by default
if (showingChildren.length === 1) {
onlyOneChild = showingChildren[0]
onlyOneChild.path = path.resolve(parent.path, onlyOneChild.path)
return onlyOneChild
}
// Show parent if there are no child route to display
if (showingChildren.length === 0) {
onlyOneChild = { ... parent, path: '', noShowingChildren: true }
return onlyOneChild
}
return false
}
}
}
</script>
<style lang="scss" scoped>
.app-container {
.roles-table {
margin-top: 30px;
}
.permission-tree {
margin-bottom: 30px;
}
}
</style>

View File

@ -1,48 +0,0 @@
/**
* @Author: jianglei
* @Date: 2017-10-12 12:06:49
*/
'use strict'
import Vue from 'vue'
export default function treeToArray(data, expandAll, parent, level, item) {
const marLTemp = []
let tmp = []
Array.from(data).forEach(function(record) {
if (record._expanded === undefined) {
Vue.set(record, '_expanded', expandAll)
}
let _level = 1
if (level !== undefined && level !== null) {
_level = level + 1
}
Vue.set(record, '_level', _level)
// 如果有父元素
if (parent) {
Vue.set(record, 'parent', parent)
// 如果父元素有偏移量需要计算在this的偏移量中
// 偏移量还与前面同级元素有关,需要加上前面所有元素的长度和
if (!marLTemp[_level]) {
marLTemp[_level] = 0
}
Vue.set(record, '_marginLeft', marLTemp[_level] + parent._marginLeft)
Vue.set(record, '_width', record[item] / parent[item] * parent._width)
// 在本次计算过偏移量后加上自己长度,以供下一个元素使用
marLTemp[_level] += record._width
} else {
// 如果为根
// 初始化偏移量存储map
marLTemp[record.id] = []
// map中是一个数组存储的是每级的长度和
// 初始情况下为0
marLTemp[record.id][_level] = 0
Vue.set(record, '_marginLeft', 0)
Vue.set(record, '_width', 1)
}
tmp.push(record)
if (record.children && record.children.length > 0) {
const children = treeToArray(record.children, expandAll, record, _level, item)
tmp = tmp.concat(children)
}
})
return tmp
}

View File

@ -0,0 +1,51 @@
const data = [
{
name: '1',
timeLine: 100,
children: [
{
name: '1-1',
timeLine: 20
},
{
name: '1-2',
timeLine: 60,
children: [
{
name: '1-2-1',
timeLine: 35
},
{
name: '1-2-2',
timeLine: 25
}
]
}
]
},
{
name: '2',
timeLine: 80,
children: [
{
name: '2-1',
timeLine: 30
},
{
name: '2-2',
timeLine: 50
},
{
name: '2-3',
timeLine: 60
}
]
},
{
name: '3',
timeLine: 40
}
]
export default data

View File

@ -0,0 +1,158 @@
<template>
<div>
<div class="app-container">
<el-button type="primary" size="small" style="margin:0 0 20px 0;">
<a href="https://github.com/PanJiaChen/vue-element-admin/tree/master/src/components/TreeTable" target="_blank">Documentation</a>
</el-button>
<tree-table
ref="TreeTable"
:data="tableData"
:default-expand-all="true"
:columns="columns"
border
default-children="children"
@selection-change="selectChange"
>
<template slot="selection">
<el-table-column type="selection" align="center" width="55" />
</template>
<template slot="pre-column">
<el-table-column type="expand" width="55">
<template>
<el-tag type="info">
Here is just a placeholder slot, you can display anything.
</el-tag>
</template>
</el-table-column>
</template>
<template slot="timeline" slot-scope="{scope}">
<el-tooltip :content="scope.row.timeLine+'ms'" effect="dark" placement="left">
<div class="processContainer">
<div
:style="{ width:(scope.row.timeLine||0) * 3+'px',
background:scope.row.timeLine>50?'rgba(233,0,0,.5)':'rgba(0,0,233,0.5)',
marginLeft:scope.row._level * 50+'px' }"
class="process"
>
<span style="display:inline-block" />
</div>
</div>
</el-tooltip>
</template>
<template slot="append" slot-scope="{scope}">
<el-button
size="mini"
type="primary"
@click="addMenuItem(scope.row,'brother')"
>Append Brother
</el-button>
<el-button
size="mini"
type="primary"
@click="addMenuItem(scope.row,'children')"
>Append Child
</el-button>
</template>
<template slot="operation" slot-scope="{scope}">
<el-button size="mini" type="success" @click="editItem(scope.row)">Edit</el-button>
<el-button size="mini" type="danger" @click="deleteItem(scope.row)">Delete</el-button>
</template>
</tree-table>
</div>
<el-dialog :visible.sync="dialogFormVisible" title="Edit">
<el-form :model="tempItem" label-width="100px" style="width:600px">
<el-form-item label="Name">
<el-input v-model.trim="tempItem.name" placeholder="Name" />
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogFormVisible = false">Cancel</el-button>
<el-button type="primary" @click="updateItem">Confirm</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
import TreeTable from '@/components/TreeTable'
import data from './data.js'
export default {
components: { TreeTable },
data() {
return {
tableData: [],
tempItem: {},
dialogFormVisible: false,
columns: [
{
label: 'Name',
key: 'name',
expand: true
},
{
label: 'Timeline',
key: 'timeline'
},
{
label: 'Append',
key: 'append',
width: 300
},
{
label: 'Operation',
key: 'operation',
width: 160
}
]
}
},
created() {
this.getData()
},
methods: {
getData() {
this.tableData = data
},
editItem(row) {
this.tempItem = Object.assign({}, row)
this.dialogFormVisible = true
},
async updateItem() {
await this.$refs.TreeTable.updateTreeNode(this.tempItem)
this.dialogFormVisible = false
},
addMenuItem(row, type) {
if (type === 'children') {
this.$refs.TreeTable.addChild(row, { name: 'child', timeLine: this.randomNum() })
}
if (type === 'brother') {
this.$refs.TreeTable.addBrother(row, { name: 'brother', timeLine: this.randomNum() })
}
},
deleteItem(row) {
this.$refs.TreeTable.delete(row)
},
selectChange(val) {
console.log(val)
},
randomNum() {
// return 1~100
const max = 100
const min = 1
return Math.floor(Math.random() * (max - min + 1) + min)
}
}
}
</script>

View File

@ -0,0 +1,80 @@
const data = [
{
id: 0,
event: 'Event-0',
timeLine: 50
},
{
id: 1,
event: 'Event-1',
timeLine: 100,
children: [
{
id: 2,
event: 'Event-2',
timeLine: 10
},
{
id: 3,
event: 'Event-3',
timeLine: 90,
children: [
{
id: 4,
event: 'Event-4',
timeLine: 5
},
{
id: 5,
event: 'Event-5',
timeLine: 10
},
{
id: 6,
event: 'Event-6',
timeLine: 75,
children: [
{
id: 7,
event: 'Event-7',
timeLine: 50,
children: [
{
id: 71,
event: 'Event-7-1',
timeLine: 25
},
{
id: 72,
event: 'Event-7-2',
timeLine: 5
},
{
id: 73,
event: 'Event-7-3',
timeLine: 20
}
]
},
{
id: 8,
event: 'Event-8',
timeLine: 25
}
]
}
]
}
]
}
]
export default data

View File

@ -0,0 +1,128 @@
<template>
<div class="app-container">
<div style="margin-bottom:20px;">
<el-button type="primary" size="small" class="option-item">
<a href="https://github.com/PanJiaChen/vue-element-admin/tree/master/src/components/TreeTable" target="_blank">Documentation</a>
</el-button>
<div class="option-item">
<el-tag>Expand All</el-tag>
<el-switch
v-model="defaultExpandAll"
active-color="#13ce66"
inactive-color="#ff4949"
@change="reset"
/>
</div>
<div class="option-item">
<el-tag>Show Checkbox</el-tag>
<el-switch
v-model="showCheckbox"
active-color="#13ce66"
inactive-color="#ff4949"
/>
</div>
</div>
<tree-table :key="key" :default-expand-all="defaultExpandAll" :data="data" :columns="columns" border>
<template slot="scope" slot-scope="{scope}">
<el-tag>level: {{ scope.row._level }}</el-tag>
<el-tag>expand: {{ scope.row._expand }}</el-tag>
<el-tag>select: {{ scope.row._select }}</el-tag>
</template>
<template slot="operation" slot-scope="{scope}">
<el-button type="primary" size="" @click="click(scope)">Click</el-button>
</template>
</tree-table>
</div>
</template>
<script>
import treeTable from '@/components/TreeTable'
import data from './data'
export default {
name: 'TreeTableDemo',
components: { treeTable },
data() {
return {
defaultExpandAll: false,
showCheckbox: true,
key: 1,
columns: [
{
label: 'Checkbox',
checkbox: true
},
{
label: '',
key: 'id',
expand: true
},
{
label: 'Event',
key: 'event',
width: 200,
align: 'left'
},
{
label: 'Scope',
key: 'scope'
},
{
label: 'Operation',
key: 'operation'
}
],
data: data
}
},
watch: {
showCheckbox(val) {
if (val) {
this.columns.unshift({
label: 'Checkbox',
checkbox: true
})
} else {
this.columns.shift()
}
this.reset()
}
},
methods: {
reset() {
++this.key
},
click(scope) {
console.log(scope)
const row = scope.row
const message = Object.keys(row)
.map(i => {
return `<p>${i}: ${row[i]}</p>`
})
.join('')
this.$notify({
title: 'Success',
dangerouslyUseHTMLString: true,
message: message,
type: 'success'
})
}
}
}
</script>
<style scoped>
.option-item{
display: inline-block;
margin-right: 15px;
}
</style>