<template>
|
<div class="dept-tree">
|
<div class="dept-tree__header">
|
<el-text>{{ t('组织架构') }}</el-text>
|
|
<div class="dept-tree__op">
|
<div class="btns" v-if="isDrag">
|
<div class="item" @click="treeOrder(true)">
|
<cl-svg name="success" />
|
</div>
|
|
<div class="item" @click="treeOrder(false)">
|
<cl-svg name="fail" />
|
</div>
|
</div>
|
|
<template v-else>
|
<div class="item" @click="refresh()">
|
<el-tooltip :content="t('刷新')">
|
<cl-svg name="refresh" />
|
</el-tooltip>
|
</div>
|
|
<div class="item" v-if="drag && !browser.isMini" @click="isDrag = true">
|
<el-tooltip :content="t('拖动排序')">
|
<cl-svg name="sort" />
|
</el-tooltip>
|
</div>
|
</template>
|
</div>
|
</div>
|
|
<div class="dept-tree__container" @contextmenu.stop.prevent="onContextMenu">
|
<el-scrollbar>
|
<el-tree
|
v-loading="loading"
|
node-key="id"
|
default-expand-all
|
:data="list"
|
:props="{
|
label: 'name'
|
}"
|
highlight-current
|
:draggable="isDrag"
|
:allow-drag="allowDrag"
|
:allow-drop="allowDrop"
|
:expand-on-click-node="false"
|
@node-contextmenu="onContextMenu"
|
@node-click="rowClick"
|
>
|
<template #default="{ node, data }">
|
<div class="dept-tree__node">
|
<span
|
class="dept-tree__node-label"
|
:class="{
|
'is-active': data.id == ViewGroup?.selected?.id
|
}"
|
>{{ node.label }}</span
|
>
|
<span
|
v-if="browser.isMini"
|
class="dept-tree__node-icon"
|
@click="onContextMenu($event, data, node)"
|
>
|
<el-icon>
|
<more-filled />
|
</el-icon>
|
</span>
|
</div>
|
</template>
|
</el-tree>
|
</el-scrollbar>
|
</div>
|
|
<cl-form ref="Form" />
|
</div>
|
</template>
|
|
<script lang="ts" setup>
|
defineOptions({
|
name: 'dept-list'
|
});
|
|
import { nextTick, onMounted, ref } from 'vue';
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
import { useCool } from '/@/cool';
|
import { deepTree, revDeepTree } from '/@/cool/utils';
|
import { isArray } from 'lodash-es';
|
import { ContextMenu, useForm } from '@cool-vue/crud';
|
import { Refresh as RefreshIcon, Operation, MoreFilled } from '@element-plus/icons-vue';
|
import { checkPerm } from '/$/base';
|
import { useViewGroup } from '/@/plugins/view';
|
import { useI18n } from 'vue-i18n';
|
|
const props = defineProps({
|
drag: {
|
type: Boolean,
|
default: true
|
},
|
level: {
|
type: Number,
|
default: 99
|
}
|
});
|
|
const emit = defineEmits(['refresh', 'user-add']);
|
|
const { service, browser } = useCool();
|
const Form = useForm();
|
const { ViewGroup } = useViewGroup();
|
const { t } = useI18n();
|
|
// 树形列表
|
const list = ref<Eps.BaseSysDepartmentEntity[]>([]);
|
|
// 加载中
|
const loading = ref(false);
|
|
// 是否能拖动
|
const isDrag = ref(false);
|
|
// 允许托的规则
|
function allowDrag({ data }: any) {
|
return data.parentId;
|
}
|
|
// 允许放的规则
|
function allowDrop(_: any, dropNode: any) {
|
return dropNode.data.parentId;
|
}
|
|
// 刷新
|
async function refresh() {
|
loading.value = true;
|
isDrag.value = false;
|
|
await service.base.sys.department.list().then(res => {
|
list.value = deepTree(res);
|
|
if (!ViewGroup.value?.selected) {
|
rowClick();
|
}
|
});
|
|
loading.value = false;
|
}
|
|
// 获取 ids
|
function rowClick(item?: Eps.BaseSysDepartmentEntity) {
|
if (!item) {
|
item = list.value[0];
|
}
|
|
if (item) {
|
const ids = item.children ? revDeepTree(item.children).map(e => e.id) : [];
|
ids.unshift(item.id);
|
|
// 选择
|
ViewGroup.value?.select(item);
|
|
nextTick(() => {
|
// 刷新列表
|
emit('refresh', { page: 1, departmentIds: ids });
|
});
|
}
|
}
|
|
// 编辑部门
|
function rowEdit(item: Eps.BaseSysDepartmentEntity) {
|
const method = item.id ? 'update' : 'add';
|
|
Form.value?.open({
|
title: t('编辑部门'),
|
width: '550px',
|
props: {
|
labelWidth: '100px'
|
},
|
items: [
|
{
|
label: t('部门名称'),
|
prop: 'name',
|
component: {
|
name: 'el-input'
|
},
|
required: true
|
},
|
{
|
label: t('上级部门'),
|
prop: 'parentName',
|
component: {
|
name: 'el-input',
|
props: {
|
disabled: true
|
}
|
}
|
},
|
{
|
label: t('排序'),
|
prop: 'orderNum',
|
component: {
|
name: 'el-input-number',
|
props: {
|
'controls-position': 'right',
|
min: 0,
|
max: 100
|
}
|
}
|
}
|
],
|
form: {
|
...item
|
},
|
on: {
|
submit(data, { done, close }) {
|
service.base.sys.department[method]({
|
id: item.id,
|
parentId: item.parentId,
|
name: data.name,
|
orderNum: data.orderNum
|
})
|
.then(() => {
|
ElMessage.success(t('新增部门 “{name}” 成功', { name: data.name }));
|
close();
|
refresh();
|
})
|
.catch(err => {
|
ElMessage.error(err.message);
|
done();
|
});
|
}
|
}
|
});
|
}
|
|
// 删除部门
|
function rowDel(item: Eps.BaseSysDepartmentEntity) {
|
async function del(f: boolean) {
|
await service.base.sys.department
|
.delete({
|
ids: [item.id],
|
deleteUser: f
|
})
|
.then(() => {
|
// 删除当前
|
if (ViewGroup.value?.selected?.id == item.id) {
|
rowClick();
|
}
|
|
if (f) {
|
ElMessage.success(t('删除成功'));
|
} else {
|
ElMessageBox.confirm(
|
t('“{name}” 部门的用户已成功转移到 “{parentName}” 部门。', {
|
name: item.name,
|
parentName: item.parentName
|
}),
|
t('删除成功')
|
);
|
}
|
});
|
|
refresh();
|
}
|
|
ElMessageBox.confirm(
|
t('此操作将会删除 “{name}” 部门的所有用户,是否确认?', { name: item.name }),
|
t('提示'),
|
{
|
type: 'warning',
|
confirmButtonText: t('直接删除'),
|
cancelButtonText: t('保留用户'),
|
distinguishCancelAndClose: true
|
}
|
)
|
.then(() => {
|
del(true);
|
})
|
.catch(action => {
|
if (action == 'cancel') {
|
del(false);
|
}
|
});
|
}
|
|
// 部门排序
|
function treeOrder(f: boolean) {
|
if (f) {
|
ElMessageBox.confirm(t('部门架构已发生改变,是否保存?'), t('提示'), {
|
type: 'warning'
|
})
|
.then(async () => {
|
const ids: any[] = [];
|
|
function deep(list: any[], pid: any) {
|
list.forEach(e => {
|
e.parentId = pid;
|
ids.push(e);
|
|
if (e.children && isArray(e.children)) {
|
deep(e.children, e.id);
|
}
|
});
|
}
|
|
deep(list.value, null);
|
|
await service.base.sys.department
|
.order(
|
ids.map((e, i) => {
|
return {
|
id: e.id,
|
parentId: e.parentId,
|
orderNum: i
|
};
|
})
|
)
|
.then(() => {
|
ElMessage.success(t('更新排序成功'));
|
})
|
.catch(err => {
|
ElMessage.error(err.message);
|
});
|
|
refresh();
|
isDrag.value = false;
|
})
|
.catch(() => null);
|
} else {
|
refresh();
|
}
|
}
|
|
// 右键菜单
|
function onContextMenu(e: any, d?: any, n?: any) {
|
if (!d) {
|
d = list.value[0] || {};
|
}
|
|
// 权限
|
const perm = service.base.sys.department.permission;
|
|
ContextMenu.open(e, {
|
list: [
|
{
|
label: t('新增'),
|
hidden: (n && n.level >= props.level) || !checkPerm(perm.add),
|
callback(done) {
|
rowEdit({
|
name: '',
|
parentName: d.name,
|
parentId: d.id
|
});
|
done();
|
}
|
},
|
{
|
label: t('编辑'),
|
hidden: !checkPerm(perm.update),
|
callback(done) {
|
rowEdit(d);
|
done();
|
}
|
},
|
{
|
label: t('删除'),
|
hidden: !d.parentId || !checkPerm(perm.delete),
|
callback(done) {
|
rowDel(d);
|
done();
|
}
|
},
|
{
|
label: t('新增成员'),
|
hidden: !checkPerm(perm.add),
|
callback(done) {
|
emit('user-add', d);
|
done();
|
}
|
}
|
]
|
});
|
}
|
|
onMounted(function () {
|
refresh();
|
});
|
</script>
|
|
<style lang="scss" scoped>
|
.dept-tree {
|
height: 100%;
|
width: 100%;
|
|
:deep(.el-tree-node__label) {
|
display: block;
|
height: 100%;
|
width: 100%;
|
}
|
|
&__header {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
height: 40px;
|
padding: 0 10px;
|
border-bottom: 1px solid var(--el-border-color-extra-light);
|
}
|
|
&__op {
|
display: flex;
|
align-items: center;
|
|
.item {
|
display: flex;
|
justify-content: center;
|
align-items: center;
|
list-style: none;
|
margin-left: 5px;
|
cursor: pointer;
|
border-radius: 6px;
|
font-size: 16px;
|
height: 26px;
|
width: 26px;
|
color: var(--el-text-color-primary);
|
|
.cl-svg {
|
outline: none;
|
}
|
|
&:hover {
|
background-color: var(--el-fill-color-light);
|
}
|
}
|
|
.btns {
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
|
.item {
|
&:hover {
|
&:first-child {
|
color: var(--el-color-success);
|
}
|
|
&:last-child {
|
color: var(--el-color-danger);
|
}
|
}
|
}
|
}
|
}
|
|
&__container {
|
height: calc(100% - 40px);
|
padding: 10px;
|
|
:deep(.el-tree-node__content) {
|
height: 38px;
|
border-radius: 4px;
|
|
.el-tree-node__expand-icon {
|
margin-left: 5px;
|
}
|
}
|
}
|
|
&__node {
|
display: flex;
|
align-items: center;
|
height: 100%;
|
width: 100%;
|
box-sizing: border-box;
|
|
&-label {
|
display: flex;
|
align-items: center;
|
flex: 1;
|
height: 100%;
|
font-size: 14px;
|
overflow: hidden;
|
text-overflow: ellipsis;
|
white-space: nowrap;
|
|
&.is-active {
|
color: var(--el-color-primary);
|
}
|
}
|
|
&-icon {
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
background-color: #eee;
|
height: 26px;
|
width: 26px;
|
text-align: center;
|
margin-right: 5px;
|
border-radius: 6px;
|
}
|
}
|
}
|
</style>
|