parent
90097bfe0e
commit
0e8f4dc504
@ -1,187 +1,143 @@ |
||||
<template> |
||||
<div class="org-tree-container"> |
||||
<div :class="{horizontal, collapsable}" class="org-tree"> |
||||
<div class="org-tree" :class="{horizontal, collapsable}"> |
||||
<org-tree-node |
||||
v-bind="$listeners" |
||||
:button-render="buttonRender" |
||||
:collapsable="collapsable" |
||||
:data="dataCloned" |
||||
:horizontal="horizontal" |
||||
:label-class-name="labelClassName" |
||||
:label-width="labelWidth" |
||||
:node-render="nodeRender" |
||||
:props="props" |
||||
@on-expand="handleExpand" |
||||
@on-node-click="handleNodeClick" |
||||
:data="dataCloned" |
||||
:props="props" |
||||
:horizontal="horizontal" |
||||
:label-width="labelWidth" |
||||
:collapsable="collapsable" |
||||
:label-class-name="labelClassName" |
||||
:node-render="nodeRender" |
||||
:button-render="buttonRender" |
||||
@on-expand="handleExpand" |
||||
@on-node-click="handleNodeClick" |
||||
/> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
|
||||
<script> |
||||
import render from './node' |
||||
import {deepClone as clonedeep} from '@/utils' |
||||
import OrgTreeNode from "./OrgTreeNode" |
||||
|
||||
export default { |
||||
name: 'OrgTree', |
||||
export default { |
||||
name: 'OrgTree', |
||||
|
||||
components: { |
||||
OrgTreeNode: { |
||||
render, |
||||
functional: true |
||||
} |
||||
}, |
||||
components: {OrgTreeNode}, |
||||
|
||||
props: { |
||||
data: {required: true}, |
||||
props: { |
||||
data: { |
||||
type: Object, |
||||
required: true |
||||
}, |
||||
props: { |
||||
type: Object, |
||||
default: () => ({ |
||||
id: 'id', |
||||
label: 'label', |
||||
expand: 'expand', |
||||
children: 'children' |
||||
}) |
||||
}, |
||||
horizontal: Boolean, |
||||
collapsable: Boolean, |
||||
nodeRender: Function, |
||||
buttonRender: Function, |
||||
labelWidth: [String, Number], |
||||
labelClassName: [Function, String], |
||||
expandAll: { |
||||
type: Boolean, |
||||
default: false |
||||
} |
||||
type: Object, |
||||
default: () => ({ |
||||
id: 'id', |
||||
label: 'label', |
||||
expand: 'expand', |
||||
children: 'children' |
||||
}) |
||||
}, |
||||
horizontal: Boolean, |
||||
collapsable: Boolean, |
||||
nodeRender: Function, |
||||
buttonRender: Function, |
||||
labelWidth: [String, Number], |
||||
labelClassName: [Function, String], |
||||
expandAll: Boolean |
||||
}, |
||||
|
||||
data() { |
||||
return { |
||||
flatData: {}, |
||||
dataCloned: {} |
||||
} |
||||
}, |
||||
data() { |
||||
return { |
||||
flatData: {}, |
||||
dataCloned: {} |
||||
} |
||||
}, |
||||
|
||||
watch: { |
||||
data(newData) { |
||||
this._handleData(newData) |
||||
this._mapData(this.dataCloned, item => { |
||||
const {expand} = this.flatData[item[this.prop_id]] || {} |
||||
if (expand) this.$set(item, this.prop_expand, true) |
||||
}) |
||||
this._toggleExpand(this.dataCloned, this.expandAll) |
||||
}, |
||||
expandAll(status) { |
||||
this._toggleExpand(this.dataCloned, status) |
||||
} |
||||
watch: { |
||||
data(newData) { |
||||
this._cloneData(newData) |
||||
this._mapData(this.dataCloned, item => { |
||||
const {expand} = this.flatData[item[this.props.id]] || {} |
||||
if (expand) this.$set(item, this.props.expand, true) |
||||
}) |
||||
this._toggleExpand(this.dataCloned, this.expandAll) |
||||
}, |
||||
expandAll(status) { |
||||
this._toggleExpand(this.dataCloned, status) |
||||
} |
||||
}, |
||||
|
||||
computed: { |
||||
prop_id() { |
||||
return this.props.id |
||||
}, |
||||
prop_label() { |
||||
return this.props.label |
||||
}, |
||||
prop_expand() { |
||||
return this.props.expand |
||||
}, |
||||
prop_children() { |
||||
return this.props.children |
||||
} |
||||
methods: { |
||||
_cloneData(newData) { |
||||
this.dataCloned = JSON.parse(JSON.stringify(newData)) |
||||
}, |
||||
|
||||
methods: { |
||||
_handleData(data) { |
||||
this._cloneData(data) |
||||
}, |
||||
_cloneData(newData) { |
||||
this.dataCloned = clonedeep(newData) |
||||
}, |
||||
_setFlatData(data) { |
||||
this.flatData[data[this.prop_id]] = data |
||||
}, |
||||
/** |
||||
* @description 工具方法,用于遍历树状数据的每个节点, fn为在该节点做的操作,其有一个参数即当前节点数据 |
||||
*/ |
||||
_mapData(data, fn) { |
||||
fn(data) |
||||
const children = data[this.prop_children] |
||||
if (children) { |
||||
children.forEach(child => { |
||||
this._mapData(child, fn) |
||||
}) |
||||
} |
||||
}, |
||||
/** |
||||
* @description 用来便利所有节点数据,将树状数据扁平化存放到flatData,用于数据更新后展开状态的恢复 |
||||
*/ |
||||
_updateExpandStatus() { |
||||
this._mapData(this.dataCloned, this._setFlatData) |
||||
}, |
||||
collapse(list) { |
||||
var _this = this |
||||
list.forEach(child => { |
||||
if (child[this.prop_expand]) { |
||||
child[this.prop_expand] = false |
||||
_setFlatData(data) { |
||||
this.flatData[data[this.props.id]] = data |
||||
}, |
||||
/** |
||||
* @description 工具方法,用于遍历树状数据的每个节点, fn为在该节点做的操作,其有一个参数即当前节点数据 |
||||
*/ |
||||
_mapData(data, fn) { |
||||
fn(data) |
||||
const children = data[this.props.children] |
||||
children && children.forEach(child => this._mapData(child, fn)) |
||||
}, |
||||
/** |
||||
* @description 用来便利所有节点数据,将树状数据扁平化存放到flatData,用于数据更新后展开状态的恢复 |
||||
*/ |
||||
_updateExpandStatus() { |
||||
this._mapData(this.dataCloned, this._setFlatData) |
||||
}, |
||||
_toggleExpand(data, status) { |
||||
if (Array.isArray(data)) { |
||||
data.forEach(item => { |
||||
this.$set(item, this.props.expand, status) |
||||
const children = item[this.props.children] |
||||
if (children) { |
||||
this._toggleExpand(children, status) |
||||
} |
||||
const children = child[this.prop_children] |
||||
children && _this.collapse(children) |
||||
}) |
||||
}, |
||||
handleExpand(data) { |
||||
if (this.prop_expand in data) { |
||||
data[this.prop_expand] = !data[this.prop_expand] |
||||
const children = data[this.prop_children] |
||||
if (!data[this.prop_expand] && children) { |
||||
this.collapse(children) |
||||
} |
||||
} |
||||
else { |
||||
this.$set(data, this.prop_expand, true) |
||||
} |
||||
else { |
||||
this.$set(data, this.props.expand, status) |
||||
const children = data[this.props.children] |
||||
if (children) { |
||||
this._toggleExpand(children, status) |
||||
} |
||||
this.$emit('on-expand', data, data[this.prop_expand]) |
||||
this._updateExpandStatus() |
||||
}, |
||||
_toggleExpand(data, status) { |
||||
var _this = this |
||||
if (Array.isArray(data)) { |
||||
data.forEach(item => { |
||||
_this.$set(item, this.prop_expand, status) |
||||
const children = item[this.prop_children] |
||||
if (children) { |
||||
_this._toggleExpand(children, status) |
||||
} |
||||
}) |
||||
} |
||||
}, |
||||
collapse(list) { |
||||
list.forEach(child => { |
||||
if (child[this.props.expand]) { |
||||
child[this.props.expand] = false |
||||
} |
||||
else { |
||||
_this.$set(data, this.prop_expand, status) |
||||
const children = data[this.prop_children] |
||||
if (children) { |
||||
_this._toggleExpand(children, status) |
||||
} |
||||
const children = child[this.props.children] |
||||
children && this.collapse(children) |
||||
}) |
||||
}, |
||||
handleExpand(data) { |
||||
if (this.props.expand in data) { |
||||
data[this.props.expand] = !data[this.props.expand] |
||||
const children = data[this.props.children] |
||||
if (!data[this.props.expand] && children) { |
||||
this.collapse(children) |
||||
} |
||||
}, |
||||
handleNodeClick(e, data) { |
||||
this.$emit('on-node-click', e, data, () => { |
||||
this.handleExpand(data) |
||||
}) |
||||
}, |
||||
toggleExpand() { |
||||
this._toggleExpand(this.dataCloned, this.expandAll) |
||||
this._updateExpandStatus() |
||||
} |
||||
}, |
||||
else this.$set(data, this.props.expand, true) |
||||
|
||||
mounted() { |
||||
this._handleData(this.data) |
||||
this.$emit('on-expand', data, data[this.props.expand]) |
||||
this._updateExpandStatus() |
||||
this._toggleExpand(this.dataCloned, this.expandAll) |
||||
}, |
||||
handleNodeClick(e, data) { |
||||
this.$emit('on-node-click', e, data, () => this.handleExpand(data)) |
||||
} |
||||
}, |
||||
|
||||
mounted() { |
||||
this._cloneData(this.data) |
||||
this._updateExpandStatus() |
||||
this._toggleExpand(this.dataCloned, this.expandAll) |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<style lang="scss" src="./tree.scss"></style> |
||||
|
||||
@ -1,261 +1,261 @@ |
||||
.org-tree-container { |
||||
display: inline-block; |
||||
padding: 15px; |
||||
} |
||||
|
||||
.org-tree { |
||||
display: table; |
||||
text-align: center; |
||||
|
||||
&:before, &:after { |
||||
content: ''; |
||||
.org-tree { |
||||
display: table; |
||||
} |
||||
|
||||
&:after { |
||||
clear: both; |
||||
} |
||||
} |
||||
|
||||
.org-tree-node, |
||||
.org-tree-node-children { |
||||
position: relative; |
||||
margin: 0; |
||||
padding: 0; |
||||
list-style-type: none; |
||||
|
||||
&:before, &:after { |
||||
transition: all .35s; |
||||
} |
||||
} |
||||
|
||||
.org-tree-node-label { |
||||
position: relative; |
||||
display: inline-block; |
||||
z-index: 1; |
||||
|
||||
.org-tree-node-label-inner { |
||||
padding: 10px 15px; |
||||
text-align: center; |
||||
border-radius: 3px; |
||||
box-shadow: 0 1px 5px rgba(0, 0, 0, .15); |
||||
} |
||||
} |
||||
|
||||
.org-tree-node-btn { |
||||
position: absolute; |
||||
top: 100%; |
||||
left: 50%; |
||||
width: 20px; |
||||
height: 20px; |
||||
z-index: 10; |
||||
margin-left: -10px; |
||||
margin-top: 10px; |
||||
background-color: #fff; |
||||
border: 1px solid #ccc; |
||||
border-radius: 50%; |
||||
box-shadow: 0 0 2px rgba(0, 0, 0, .15); |
||||
cursor: pointer; |
||||
transition: all .35s ease; |
||||
|
||||
&:hover { |
||||
background-color: #e7e8e9; |
||||
transform: scale(1.15); |
||||
} |
||||
|
||||
&:before, &:after { |
||||
content: ''; |
||||
position: absolute; |
||||
} |
||||
|
||||
&:before { |
||||
top: 50%; |
||||
left: 4px; |
||||
right: 4px; |
||||
height: 0; |
||||
border-top: 1px solid #ccc; |
||||
} |
||||
|
||||
&:after { |
||||
top: 4px; |
||||
left: 50%; |
||||
bottom: 4px; |
||||
width: 0; |
||||
border-left: 1px solid #ccc; |
||||
} |
||||
|
||||
&.expanded:after { |
||||
border: none; |
||||
} |
||||
} |
||||
|
||||
.org-tree-node { |
||||
padding-top: 20px; |
||||
display: table-cell; |
||||
vertical-align: top; |
||||
&:before, &:after { |
||||
content: ''; |
||||
display: table; |
||||
} |
||||
|
||||
&.is-leaf, &.collapsed { |
||||
padding-left: 10px; |
||||
padding-right: 10px; |
||||
&:after { |
||||
clear: both; |
||||
} |
||||
} |
||||
|
||||
&:before, &:after { |
||||
content: ''; |
||||
position: absolute; |
||||
top: 0; |
||||
left: 0; |
||||
width: 50%; |
||||
height: 19px; |
||||
} |
||||
.org-tree-node, |
||||
.org-tree-node-children { |
||||
position: relative; |
||||
margin: 0; |
||||
padding: 0; |
||||
list-style-type: none; |
||||
|
||||
&:after { |
||||
left: 50%; |
||||
border-left: 1px solid #ddd; |
||||
&:before, &:after { |
||||
transition: all .35s; |
||||
} |
||||
} |
||||
|
||||
&:not(:first-child):before, |
||||
&:not(:last-child):after { |
||||
border-top: 1px solid #ddd; |
||||
.org-tree-node-label { |
||||
position: relative; |
||||
display: inline-block; |
||||
z-index: 1; |
||||
|
||||
.org-tree-node-label-inner { |
||||
padding: 10px 15px; |
||||
text-align: center; |
||||
border-radius: 3px; |
||||
box-shadow: 0 1px 5px rgba(0, 0, 0, .15); |
||||
} |
||||
} |
||||
|
||||
} |
||||
|
||||
.collapsable .org-tree-node.collapsed { |
||||
padding-bottom: 30px; |
||||
|
||||
.org-tree-node-label:after { |
||||
content: ''; |
||||
.org-tree-node-btn { |
||||
position: absolute; |
||||
top: 100%; |
||||
left: 0; |
||||
width: 50%; |
||||
left: 50%; |
||||
width: 20px; |
||||
height: 20px; |
||||
border-right: 1px solid #ddd; |
||||
} |
||||
} |
||||
|
||||
.org-tree > .org-tree-node { |
||||
padding-top: 0; |
||||
z-index: 10; |
||||
margin-left: -10px; |
||||
margin-top: 10px; |
||||
background-color: #fff; |
||||
border: 1px solid #ccc; |
||||
border-radius: 50%; |
||||
box-shadow: 0 0 2px rgba(0, 0, 0, .15); |
||||
cursor: pointer; |
||||
transition: all .35s ease; |
||||
|
||||
&:hover { |
||||
background-color: #e7e8e9; |
||||
transform: scale(1.15); |
||||
} |
||||
|
||||
&:after { |
||||
border-left: 0; |
||||
} |
||||
} |
||||
&:before, &:after { |
||||
content: ''; |
||||
position: absolute; |
||||
} |
||||
|
||||
.org-tree-node-children { |
||||
padding-top: 20px; |
||||
display: table; |
||||
&:before { |
||||
top: 50%; |
||||
left: 4px; |
||||
right: 4px; |
||||
height: 0; |
||||
border-top: 1px solid #ccc; |
||||
} |
||||
|
||||
&:before { |
||||
content: ''; |
||||
position: absolute; |
||||
top: 0; |
||||
left: 50%; |
||||
width: 0; |
||||
height: 20px; |
||||
border-left: 1px solid #ddd; |
||||
} |
||||
&:after { |
||||
top: 4px; |
||||
left: 50%; |
||||
bottom: 4px; |
||||
width: 0; |
||||
border-left: 1px solid #ccc; |
||||
} |
||||
|
||||
&:after { |
||||
content: ''; |
||||
display: table; |
||||
clear: both; |
||||
&.expanded:after { |
||||
border: none; |
||||
} |
||||
} |
||||
} |
||||
|
||||
.horizontal { |
||||
.org-tree-node { |
||||
padding-top: 20px; |
||||
display: table-cell; |
||||
float: none; |
||||
padding-top: 0; |
||||
padding-left: 20px; |
||||
vertical-align: top; |
||||
|
||||
&.is-leaf, &.collapsed { |
||||
padding-top: 10px; |
||||
padding-bottom: 10px; |
||||
padding-left: 10px; |
||||
padding-right: 10px; |
||||
} |
||||
|
||||
&:before, &:after { |
||||
width: 19px; |
||||
height: 50%; |
||||
} |
||||
|
||||
&:after { |
||||
top: 50%; |
||||
content: ''; |
||||
position: absolute; |
||||
top: 0; |
||||
left: 0; |
||||
border-left: 0; |
||||
width: 50%; |
||||
height: 19px; |
||||
} |
||||
|
||||
&:only-child:before { |
||||
top: 1px; |
||||
border-bottom: 1px solid #ddd; |
||||
&:after { |
||||
left: 50%; |
||||
border-left: 1px solid #ddd; |
||||
} |
||||
|
||||
&:not(:first-child):before, |
||||
&:not(:last-child):after { |
||||
border-top: 0; |
||||
border-left: 1px solid #ddd; |
||||
} |
||||
|
||||
&:not(:only-child):after { |
||||
border-top: 1px solid #ddd; |
||||
} |
||||
|
||||
.org-tree-node-inner { |
||||
display: table; |
||||
} |
||||
|
||||
} |
||||
|
||||
.org-tree-node-label { |
||||
display: table-cell; |
||||
vertical-align: middle; |
||||
} |
||||
|
||||
&.collapsable .org-tree-node.collapsed { |
||||
padding-right: 30px; |
||||
.collapsable .org-tree-node.collapsed { |
||||
padding-bottom: 30px; |
||||
|
||||
.org-tree-node-label:after { |
||||
top: 0; |
||||
left: 100%; |
||||
width: 20px; |
||||
height: 50%; |
||||
border-right: 0; |
||||
border-bottom: 1px solid #ddd; |
||||
content: ''; |
||||
position: absolute; |
||||
top: 100%; |
||||
left: 0; |
||||
width: 50%; |
||||
height: 20px; |
||||
border-right: 1px solid #ddd; |
||||
} |
||||
} |
||||
|
||||
.org-tree-node-btn { |
||||
top: 50%; |
||||
left: 100%; |
||||
margin-top: -11px; |
||||
margin-left: 9px; |
||||
} |
||||
.org-tree > .org-tree-node { |
||||
padding-top: 0; |
||||
|
||||
& > .org-tree-node:only-child:before { |
||||
border-bottom: 0; |
||||
&:after { |
||||
border-left: 0; |
||||
} |
||||
} |
||||
|
||||
.org-tree-node-children { |
||||
display: table-cell; |
||||
padding-top: 0; |
||||
padding-left: 20px; |
||||
padding-top: 20px; |
||||
display: table; |
||||
|
||||
&:before { |
||||
top: 50%; |
||||
left: 0; |
||||
width: 20px; |
||||
height: 0; |
||||
border-left: 0; |
||||
border-top: 1px solid #ddd; |
||||
content: ''; |
||||
position: absolute; |
||||
top: 0; |
||||
left: 50%; |
||||
width: 0; |
||||
height: 20px; |
||||
border-left: 1px solid #ddd; |
||||
} |
||||
|
||||
&:after { |
||||
display: none; |
||||
content: ''; |
||||
display: table; |
||||
clear: both; |
||||
} |
||||
} |
||||
|
||||
.horizontal { |
||||
.org-tree-node { |
||||
display: table-cell; |
||||
float: none; |
||||
padding-top: 0; |
||||
padding-left: 20px; |
||||
|
||||
&.is-leaf, &.collapsed { |
||||
padding-top: 10px; |
||||
padding-bottom: 10px; |
||||
} |
||||
|
||||
&:before, &:after { |
||||
width: 19px; |
||||
height: 50%; |
||||
} |
||||
|
||||
&:after { |
||||
top: 50%; |
||||
left: 0; |
||||
border-left: 0; |
||||
} |
||||
|
||||
&:only-child:before { |
||||
top: 1px; |
||||
border-bottom: 1px solid #ddd; |
||||
} |
||||
|
||||
&:not(:first-child):before, |
||||
&:not(:last-child):after { |
||||
border-top: 0; |
||||
border-left: 1px solid #ddd; |
||||
} |
||||
|
||||
&:not(:only-child):after { |
||||
border-top: 1px solid #ddd; |
||||
} |
||||
|
||||
.org-tree-node-inner { |
||||
display: table; |
||||
} |
||||
|
||||
} |
||||
|
||||
.org-tree-node-label { |
||||
display: table-cell; |
||||
vertical-align: middle; |
||||
} |
||||
|
||||
&.collapsable .org-tree-node.collapsed { |
||||
padding-right: 30px; |
||||
|
||||
.org-tree-node-label:after { |
||||
top: 0; |
||||
left: 100%; |
||||
width: 20px; |
||||
height: 50%; |
||||
border-right: 0; |
||||
border-bottom: 1px solid #ddd; |
||||
} |
||||
} |
||||
|
||||
.org-tree-node-btn { |
||||
top: 50%; |
||||
left: 100%; |
||||
margin-top: -11px; |
||||
margin-left: 9px; |
||||
} |
||||
|
||||
& > .org-tree-node:only-child:before { |
||||
border-bottom: 0; |
||||
} |
||||
|
||||
& > .org-tree-node { |
||||
display: block; |
||||
.org-tree-node-children { |
||||
display: table-cell; |
||||
padding-top: 0; |
||||
padding-left: 20px; |
||||
|
||||
&:before { |
||||
top: 50%; |
||||
left: 0; |
||||
width: 20px; |
||||
height: 0; |
||||
border-left: 0; |
||||
border-top: 1px solid #ddd; |
||||
} |
||||
|
||||
&:after { |
||||
display: none; |
||||
} |
||||
|
||||
& > .org-tree-node { |
||||
display: block; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
@ -1,125 +1,127 @@ |
||||
<template> |
||||
<div :style="orgTreeStyle" class="org-tree-wrapper" @mousedown="mousedownView"> |
||||
<org-tree :data="data" :horizontal="horizontal" collapsable expand-all> |
||||
<org-tree :data="data" :horizontal="horizontal" collapsable expand-all @on-node-click="nodeClick"> |
||||
<div |
||||
slot-scope="data" |
||||
:class="{'has-children-label':data.children && data.children.length}" |
||||
class="custom-org-node" |
||||
@contextmenu.prevent="$emit('node-contextmenu',$event,data)" |
||||
slot-scope="data" |
||||
:class="{'has-children-label':data.children && data.children.length}" |
||||
class="custom-org-node" |
||||
@contextmenu.prevent="e => $emit('node-contextmenu',e,data)" |
||||
> |
||||
{{data.label}} |
||||
{{ data.label }} |
||||
</div> |
||||
</org-tree> |
||||
</div> |
||||
</template> |
||||
|
||||
<script> |
||||
import OrgTree from '@/components/OrgTree' |
||||
import OrgTree from '@/components/OrgTree' |
||||
|
||||
export default { |
||||
name: "OrgTreeView", |
||||
export default { |
||||
name: "OrgTreeView", |
||||
|
||||
components: {OrgTree}, |
||||
components: {OrgTree}, |
||||
|
||||
props: { |
||||
data: {type: Object, default: () => ({})}, |
||||
horizontal: Boolean, |
||||
collapsable: Boolean, |
||||
zoom: {type: Number, default: 1}, |
||||
}, |
||||
props: { |
||||
data: {type: Object, default: () => ({})}, |
||||
horizontal: Boolean, |
||||
collapsable: Boolean, |
||||
zoom: {type: Number, default: 1}, |
||||
}, |
||||
|
||||
data() { |
||||
return { |
||||
orgTreeOffsetLeft: 0, |
||||
orgTreeOffsetTop: 0, |
||||
initPageX: 0, |
||||
initPageY: 0, |
||||
oldMarginLeft: 0, |
||||
oldMarginTop: 0, |
||||
canMove: false, |
||||
expandAll: true |
||||
} |
||||
}, |
||||
|
||||
data() { |
||||
computed: { |
||||
orgTreeStyle() { |
||||
return { |
||||
orgTreeOffsetLeft: 0, |
||||
orgTreeOffsetTop: 0, |
||||
initPageX: 0, |
||||
initPageY: 0, |
||||
oldMarginLeft: 0, |
||||
oldMarginTop: 0, |
||||
canMove: false, |
||||
expandAll: true |
||||
transform: `translate(-50%, -50%) scale(${this.zoom}, ${this.zoom})`, |
||||
marginLeft: `${this.orgTreeOffsetLeft}px`, |
||||
marginTop: `${this.orgTreeOffsetTop}px` |
||||
} |
||||
}, |
||||
} |
||||
}, |
||||
|
||||
computed: { |
||||
orgTreeStyle() { |
||||
return { |
||||
transform: `translate(-50%, -50%) scale(${this.zoom}, ${this.zoom})`, |
||||
marginLeft: `${this.orgTreeOffsetLeft}px`, |
||||
marginTop: `${this.orgTreeOffsetTop}px` |
||||
} |
||||
} |
||||
methods: { |
||||
nodeClick(e, data, expand) { |
||||
expand() |
||||
}, |
||||
|
||||
methods: { |
||||
mousedownView(event) { |
||||
this.canMove = true |
||||
this.initPageX = event.pageX |
||||
this.initPageY = event.pageY |
||||
this.oldMarginLeft = this.orgTreeOffsetLeft |
||||
this.oldMarginTop = this.orgTreeOffsetTop |
||||
document.addEventListener('mousemove', this.mousemoveView) |
||||
document.addEventListener('mouseup', this.mouseupView) |
||||
}, |
||||
|
||||
mousemoveView(event) { |
||||
if (!this.canMove) return |
||||
const {pageX, pageY} = event |
||||
this.orgTreeOffsetLeft = this.oldMarginLeft + pageX - this.initPageX |
||||
this.orgTreeOffsetTop = this.oldMarginTop + pageY - this.initPageY |
||||
}, |
||||
|
||||
mouseupView() { |
||||
this.canMove = false |
||||
document.removeEventListener('mousemove', this.mousemoveView) |
||||
document.removeEventListener('mouseup', this.mouseupView) |
||||
} |
||||
mousedownView(event) { |
||||
this.canMove = true |
||||
this.initPageX = event.pageX |
||||
this.initPageY = event.pageY |
||||
this.oldMarginLeft = this.orgTreeOffsetLeft |
||||
this.oldMarginTop = this.orgTreeOffsetTop |
||||
document.addEventListener('mousemove', this.mousemoveView) |
||||
document.addEventListener('mouseup', this.mouseupView) |
||||
}, |
||||
mousemoveView(event) { |
||||
if (!this.canMove) return |
||||
const {pageX, pageY} = event |
||||
this.orgTreeOffsetLeft = this.oldMarginLeft + pageX - this.initPageX |
||||
this.orgTreeOffsetTop = this.oldMarginTop + pageY - this.initPageY |
||||
}, |
||||
mouseupView() { |
||||
this.canMove = false |
||||
document.removeEventListener('mousemove', this.mousemoveView) |
||||
document.removeEventListener('mouseup', this.mouseupView) |
||||
} |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<style lang="scss"> |
||||
.org-tree-wrapper { |
||||
cursor: move; |
||||
display: inline-block; |
||||
position: absolute; |
||||
left: 50%; |
||||
top: 50%; |
||||
transition: transform 0.2s ease-out; |
||||
z-index: 2; |
||||
.org-tree-wrapper { |
||||
cursor: move; |
||||
display: inline-block; |
||||
position: absolute; |
||||
left: 50%; |
||||
top: 50%; |
||||
transition: transform 0.2s ease-out; |
||||
z-index: 2; |
||||
|
||||
.org-tree-node-label { |
||||
.org-tree-node-label-inner { |
||||
padding: 0; |
||||
.org-tree-node-label { |
||||
.org-tree-node-label-inner { |
||||
padding: 0; |
||||
|
||||
.custom-org-node { |
||||
padding: 14px 41px; |
||||
background: #738699; |
||||
user-select: none; |
||||
word-wrap: normal; |
||||
white-space: nowrap; |
||||
border-radius: 4px; |
||||
color: #ffffff; |
||||
font-size: 14px; |
||||
font-weight: 500; |
||||
line-height: 20px; |
||||
transition: background 0.1s ease-in; |
||||
cursor: default; |
||||
.custom-org-node { |
||||
padding: 14px 41px; |
||||
background: #738699; |
||||
user-select: none; |
||||
word-wrap: normal; |
||||
white-space: nowrap; |
||||
border-radius: 4px; |
||||
color: #ffffff; |
||||
font-size: 14px; |
||||
font-weight: 500; |
||||
line-height: 20px; |
||||
transition: background 0.1s ease-in; |
||||
cursor: default; |
||||
|
||||
&:hover { |
||||
background: #5d6c7b; |
||||
transition: background 0.1s ease-in; |
||||
} |
||||
&:hover { |
||||
background: #5d6c7b; |
||||
transition: background 0.1s ease-in; |
||||
} |
||||
|
||||
&.has-children-label { |
||||
cursor: pointer; |
||||
} |
||||
&.has-children-label { |
||||
cursor: pointer; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
.horizontal .custom-org-node { |
||||
padding: 14px 19px; |
||||
} |
||||
.horizontal .custom-org-node { |
||||
padding: 14px 19px; |
||||
} |
||||
} |
||||
</style> |
||||
|
||||
@ -0,0 +1,190 @@ |
||||
<template> |
||||
<form-dialog :loading="loading" :title="title" :value="value" width="40%" @close="cancel" @open="open"> |
||||
<el-form |
||||
ref="form" |
||||
:model="form" |
||||
:rules="rules" |
||||
label-position="right" |
||||
label-width="100px" |
||||
status-icon |
||||
> |
||||
<el-form-item label="名 称:" prop="name"> |
||||
<el-input v-model="form.name" maxlength="20"/> |
||||
</el-form-item> |
||||
<el-form-item label="状 态:" prop="status"> |
||||
<el-radio-group v-model="form.status"> |
||||
<el-radio :label="1">启用</el-radio> |
||||
<el-radio :label="0">禁用</el-radio> |
||||
</el-radio-group> |
||||
</el-form-item> |
||||
<el-form-item label="数据范围:" prop="scope"> |
||||
<el-select v-model="form.scope"> |
||||
<el-option :value="1" label="全部"/> |
||||
<el-option :value="2" label="本部门"/> |
||||
<el-option :value="3" label="指定部门"/> |
||||
</el-select> |
||||
</el-form-item> |
||||
<el-form-item v-if="form.scope === 3" label="指定部门:"> |
||||
<el-tree |
||||
ref="dept" |
||||
:data="departments" |
||||
:props="{label:'name'}" |
||||
:default-checked-keys="form.departmentId" |
||||
node-key="id" |
||||
show-checkbox |
||||
/> |
||||
</el-form-item> |
||||
<el-form-item label="权 限:"> |
||||
<el-tree |
||||
ref="perm" |
||||
:data="resource" |
||||
:props="{label:'name'}" |
||||
node-key="id" |
||||
show-checkbox |
||||
/> |
||||
</el-form-item> |
||||
</el-form> |
||||
|
||||
<template v-slot:footer> |
||||
<el-dropdown placement="top" style="float: left" @command="treeCommand"> |
||||
<el-button plain size="small"> |
||||
权限树操作 |
||||
<i class="el-icon-arrow-up el-icon--right"/> |
||||
</el-button> |
||||
<el-dropdown-menu slot="dropdown"> |
||||
<el-dropdown-item :command="{action:'expand',level:0}">展开全部</el-dropdown-item> |
||||
<el-dropdown-item :command="{action:'collapse',level:0}">收起全部</el-dropdown-item> |
||||
<el-dropdown-item :command="{action:'expand',level:1}">仅展开一级</el-dropdown-item> |
||||
</el-dropdown-menu> |
||||
</el-dropdown> |
||||
<el-button plain size="small" @click="closeDialog">取 消</el-button> |
||||
<el-button size="small" type="primary" @click="confirm">确 定</el-button> |
||||
</template> |
||||
</form-dialog> |
||||
</template> |
||||
|
||||
<script> |
||||
import dialogMixin from "@/mixins/dialogMixin" |
||||
import FormDialog from '@/components/FormDialog' |
||||
import {getDepartments} from "@/api/system/department" |
||||
import {addRole, updateRole} from "@/api/system/role" |
||||
import {isEmpty, mergeObj} from '@/utils' |
||||
import {createTreeByWorker, elTreeControl} from '@/utils/tree' |
||||
|
||||
export default { |
||||
name: "EditDialog", |
||||
|
||||
mixins: [dialogMixin], |
||||
|
||||
components: {FormDialog}, |
||||
|
||||
props: { |
||||
value: {type: Boolean, default: false}, |
||||
type: {type: String, default: 'add'}, |
||||
data: {type: Object, default: () => ({})}, |
||||
}, |
||||
|
||||
data() { |
||||
return { |
||||
loading: false, |
||||
form: { |
||||
id: null, |
||||
name: null, |
||||
status: 1, |
||||
scope: 1, |
||||
departmentId: [], |
||||
resourceId: [] |
||||
}, |
||||
rules: { |
||||
name: [{required: true, message: '角色名称不能为空', trigger: 'change'}], |
||||
status: [{required: true, message: '角色状态不能为空', trigger: 'change'}], |
||||
scope: [{required: true, message: '数据范围不能为空', trigger: 'change'}], |
||||
}, |
||||
departments: [] |
||||
} |
||||
}, |
||||
|
||||
computed: { |
||||
title() { |
||||
if (isEmpty(this.type)) return '' |
||||
switch (this.type) { |
||||
case 'add': |
||||
return '添加角色' |
||||
case 'edit': |
||||
return '编辑角色' |
||||
} |
||||
}, |
||||
|
||||
confirmMessage() { |
||||
switch (this.type) { |
||||
case 'add': |
||||
return `确认添加新的角色【${this.form.name}】?` |
||||
case 'edit': |
||||
return `确认提交对【${this.data.name}】作出的修改?` |
||||
default: |
||||
return '' |
||||
} |
||||
}, |
||||
|
||||
resource() { |
||||
return this.$store.state.resource.tree |
||||
} |
||||
}, |
||||
|
||||
methods: { |
||||
treeCommand({action, level}) { |
||||
elTreeControl(this.$refs.perm, action, level) |
||||
}, |
||||
|
||||
open() { |
||||
if (this.type !== 'edit') return |
||||
this.$nextTick(() => { |
||||
mergeObj(this.form, this.data) |
||||
this.$refs.perm.setCheckedKeys(this.form.resourceId) |
||||
}) |
||||
}, |
||||
|
||||
clearForm() { |
||||
this.form.id = null |
||||
this.form.name = null |
||||
this.form.status = 1 |
||||
this.form.scope = 1 |
||||
this.form.departmentId = [] |
||||
this.form.resourceId = [] |
||||
this.$refs.perm.setCheckedKeys([]) |
||||
this.$refs.dept && this.$refs.dept.setCheckedKeys([]) |
||||
this.$nextTick(() => this.$refs.form.clearValidate()) |
||||
}, |
||||
|
||||
cancel() { |
||||
this.closeDialog() |
||||
this.clearForm() |
||||
}, |
||||
|
||||
confirm() { |
||||
if (this.loading) return |
||||
this.$refs.form.validate(v => { |
||||
if (!v) return |
||||
this.loading = true |
||||
const obj = { |
||||
...this.form, |
||||
departmentId: this.$refs.dept ? this.$refs.dept.getCheckedKeys().join(',') : [], |
||||
resourceId: this.$refs.perm.getCheckedKeys().join(',') |
||||
} |
||||
const promise = this.type === 'add' ? addRole(obj) : updateRole(obj) |
||||
promise |
||||
.then(({msg}) => this.$emit('success', msg)) |
||||
.finally(() => this.loading = false) |
||||
}) |
||||
} |
||||
}, |
||||
|
||||
mounted() { |
||||
getDepartments(false) |
||||
.then(data => createTreeByWorker(data)) |
||||
.then(data => { |
||||
this.departments = data |
||||
}) |
||||
} |
||||
} |
||||
</script> |
||||
@ -1,158 +0,0 @@ |
||||
<template> |
||||
<form-dialog :loading="loading" :title="title" :value="value" @close="cancel" @open="open"> |
||||
<el-form |
||||
ref="form" |
||||
:model="form" |
||||
:rules="rules" |
||||
label-position="right" |
||||
label-width="75px" |
||||
status-icon |
||||
> |
||||
<el-form-item label="名 称:" prop="name"> |
||||
<el-input v-model="form.name" maxlength="20"/> |
||||
</el-form-item> |
||||
<el-form-item label="状 态:" prop="status"> |
||||
<el-radio-group v-model="form.status"> |
||||
<el-radio :label="1">启用</el-radio> |
||||
<el-radio :label="0">禁用</el-radio> |
||||
</el-radio-group> |
||||
</el-form-item> |
||||
<el-form-item label="权 限:"> |
||||
<el-tree |
||||
ref="tree" |
||||
:data="resource" |
||||
:expand-on-click-node="false" |
||||
:props="{label:'name'}" |
||||
node-key="id" |
||||
show-checkbox |
||||
/> |
||||
</el-form-item> |
||||
</el-form> |
||||
|
||||
<template v-slot:footer> |
||||
<el-dropdown placement="top" style="float: left" @command="treeCommand"> |
||||
<el-button plain size="small"> |
||||
树形操作 |
||||
<i class="el-icon-arrow-up el-icon--right"/> |
||||
</el-button> |
||||
<el-dropdown-menu slot="dropdown"> |
||||
<el-dropdown-item :command="{action:'expand',level:0}">展开全部</el-dropdown-item> |
||||
<el-dropdown-item :command="{action:'collapse',level:0}">收起全部</el-dropdown-item> |
||||
<el-dropdown-item :command="{action:'expand',level:1}">仅展开一级</el-dropdown-item> |
||||
</el-dropdown-menu> |
||||
</el-dropdown> |
||||
<el-button plain size="small" @click="closeDialog">取 消</el-button> |
||||
<el-button size="small" type="primary" @click="confirm">确 定</el-button> |
||||
</template> |
||||
</form-dialog> |
||||
</template> |
||||
|
||||
<script> |
||||
import FormDialog from '@/components/FormDialog' |
||||
import dialogMixin from "@/mixins/dialogMixin" |
||||
import {addRole, updateRole} from "@/api/system/role" |
||||
import {isEmpty, mergeObj} from '@/utils' |
||||
import {elTreeControl} from '@/utils/tree' |
||||
|
||||
export default { |
||||
name: "EditDialog", |
||||
|
||||
mixins: [dialogMixin], |
||||
|
||||
components: {FormDialog}, |
||||
|
||||
props: { |
||||
value: {type: Boolean, default: false}, |
||||
type: {type: String, default: 'add'}, |
||||
data: {type: Object, default: () => ({})}, |
||||
}, |
||||
|
||||
data() { |
||||
return { |
||||
loading: false, |
||||
form: { |
||||
id: null, |
||||
name: null, |
||||
status: 1, |
||||
resource_id: [] |
||||
}, |
||||
rules: { |
||||
name: [{required: true, message: '角色名称不能为空', trigger: 'change'}], |
||||
status: [{required: true, message: '角色状态不能为空', trigger: 'change'}], |
||||
} |
||||
} |
||||
}, |
||||
|
||||
computed: { |
||||
title() { |
||||
if (isEmpty(this.type)) return '' |
||||
switch (this.type) { |
||||
case 'add': |
||||
return '添加角色' |
||||
case 'edit': |
||||
return '编辑角色' |
||||
} |
||||
}, |
||||
|
||||
confirmMessage() { |
||||
switch (this.type) { |
||||
case 'add': |
||||
return `确认添加新的角色【${this.form.name}】?` |
||||
case 'edit': |
||||
return `确认提交对【${this.data.name}】作出的修改?` |
||||
default: |
||||
return '' |
||||
} |
||||
}, |
||||
|
||||
resource() { |
||||
return this.$store.state.resource.tree |
||||
} |
||||
}, |
||||
|
||||
methods: { |
||||
treeCommand({action, level}) { |
||||
elTreeControl(this.$refs.tree, action, level) |
||||
}, |
||||
|
||||
open() { |
||||
this.$nextTick(() => { |
||||
if (this.type === 'edit') { |
||||
mergeObj(this.form, this.data) |
||||
this.$refs.tree.setCheckedKeys(this.form.resource_id) |
||||
} |
||||
}) |
||||
}, |
||||
|
||||
clearForm() { |
||||
this.form.id = null |
||||
this.form.name = null |
||||
this.form.status = 1 |
||||
this.form.resource_id = [] |
||||
this.$refs.tree.setCheckedKeys([]) |
||||
this.$nextTick(() => this.$refs.form.clearValidate()) |
||||
}, |
||||
|
||||
cancel() { |
||||
this.closeDialog() |
||||
this.clearForm() |
||||
}, |
||||
|
||||
confirm() { |
||||
if (this.loading) return |
||||
this.$refs.form.validate(v => { |
||||
if (!v) return |
||||
this.loading = true |
||||
let obj = { |
||||
...this.form, |
||||
resource_id: this.$refs.tree.getCheckedKeys().join(',') |
||||
} |
||||
let promise = this.type === 'add' ? addRole(obj) : updateRole(obj) |
||||
promise |
||||
.then(({msg}) => this.$emit('success', msg)) |
||||
.finally(() => this.loading = false) |
||||
}) |
||||
}, |
||||
} |
||||
} |
||||
</script> |
||||
@ -1,59 +0,0 @@ |
||||
<script type="text/jsx"> |
||||
import {createTree} from "@/utils/tree" |
||||
|
||||
export default { |
||||
name: "RoleResource", |
||||
|
||||
functional: true, |
||||
|
||||
props: { |
||||
ids: {type: Array, default: () => []}, |
||||
map: {type: Object, default: () => ({})} |
||||
}, |
||||
|
||||
render(h, context) { |
||||
const {ids, map} = context.props |
||||
let tree = [] |
||||
|
||||
for (let id of ids) map[id] && tree.push(map[id]) |
||||
|
||||
tree = createTree(tree) |
||||
|
||||
const nodes = [] |
||||
|
||||
const fun = (arr, left) => { |
||||
for (let r of arr) { |
||||
nodes.push( |
||||
<span style={'margin-left:' + left + 'px'}> |
||||
<svg-icon icon="check"/> |
||||
{r.name} |
||||
</span> |
||||
) |
||||
if (r.children.length > 0) { |
||||
if (r.children[0].children.length > 0) nodes.push(<p/>) |
||||
fun(r.children, left + 20) |
||||
} |
||||
} |
||||
nodes.push(<p/>) |
||||
} |
||||
|
||||
fun(tree, 0) |
||||
|
||||
return nodes |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<style lang="scss" scoped> |
||||
@import "~@/assets/styles/variables.scss"; |
||||
|
||||
span { |
||||
.svg-icon { |
||||
color: $--color-primary |
||||
} |
||||
} |
||||
|
||||
span + span { |
||||
margin-left: 10px; |
||||
} |
||||
</style> |
||||
@ -0,0 +1,39 @@ |
||||
<template> |
||||
<tree-select :value="value" :data="data" :props="{label:'name'}" popper-append-to-body @input="input"/> |
||||
</template> |
||||
|
||||
<script> |
||||
import TreeSelect from '@/components/TreeSelect' |
||||
import {getDepartments} from "@/api/system/department" |
||||
import {createTreeByWorker} from "@/utils/tree" |
||||
import {isEmpty} from "@/utils" |
||||
|
||||
export default { |
||||
name: "DepartmentSelector", |
||||
|
||||
components: {TreeSelect}, |
||||
|
||||
props: {value: Number}, |
||||
|
||||
data() { |
||||
return { |
||||
data: [] |
||||
} |
||||
}, |
||||
|
||||
methods: { |
||||
input(v, data) { |
||||
this.$emit('input', v) |
||||
this.$emit('get-name', data && data.fullname) |
||||
}, |
||||
}, |
||||
|
||||
mounted() { |
||||
getDepartments(false) |
||||
.then(data => createTreeByWorker(data)) |
||||
.then(data => { |
||||
this.data = data |
||||
}) |
||||
} |
||||
} |
||||
</script> |
||||
Loading…
Reference in new issue