2026-06-14 23:57:42 +08:00
|
|
|
|
<script setup lang="ts">
|
2026-06-25 21:34:23 +08:00
|
|
|
|
import { computed } from 'vue';
|
|
|
|
|
|
|
2026-06-14 23:57:42 +08:00
|
|
|
|
defineOptions({ name: 'SubordinateSelector' });
|
|
|
|
|
|
|
|
|
|
|
|
interface Props {
|
|
|
|
|
|
loading?: boolean;
|
|
|
|
|
|
data?: Api.SystemManage.MySubordinateTreeNode | null;
|
|
|
|
|
|
emptyText?: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
|
|
|
|
loading: false,
|
|
|
|
|
|
data: null,
|
|
|
|
|
|
emptyText: '暂无下属数据'
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const selectedUserId = defineModel<string | null>('selectedUserId', {
|
|
|
|
|
|
default: null
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-06-25 21:34:23 +08:00
|
|
|
|
function sortSubordinateNodes(
|
|
|
|
|
|
nodes: Api.SystemManage.MySubordinateTreeNode[] | null | undefined
|
|
|
|
|
|
): Api.SystemManage.MySubordinateTreeNode[] | null {
|
|
|
|
|
|
if (!nodes?.length) return null;
|
|
|
|
|
|
|
|
|
|
|
|
return nodes
|
|
|
|
|
|
.map((node, index) => ({
|
|
|
|
|
|
...node,
|
|
|
|
|
|
children: sortSubordinateNodes(node.children),
|
|
|
|
|
|
originalIndex: index
|
|
|
|
|
|
}))
|
|
|
|
|
|
.sort((left, right) => {
|
|
|
|
|
|
const leftHasChildren = (left.children?.length ?? 0) > 0 ? 1 : 0;
|
|
|
|
|
|
const rightHasChildren = (right.children?.length ?? 0) > 0 ? 1 : 0;
|
|
|
|
|
|
|
|
|
|
|
|
if (leftHasChildren !== rightHasChildren) {
|
|
|
|
|
|
return rightHasChildren - leftHasChildren;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return left.originalIndex - right.originalIndex;
|
|
|
|
|
|
})
|
|
|
|
|
|
.map(({ originalIndex: _ignored, ...node }) => node);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const treeData = computed<Api.SystemManage.MySubordinateTreeNode[] | null>(() => {
|
|
|
|
|
|
if (!props.data) return null;
|
|
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
|
{
|
|
|
|
|
|
...props.data,
|
|
|
|
|
|
children: sortSubordinateNodes(props.data.children)
|
|
|
|
|
|
}
|
|
|
|
|
|
];
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-06-14 23:57:42 +08:00
|
|
|
|
function handleNodeClick(node: Api.SystemManage.MySubordinateTreeNode) {
|
|
|
|
|
|
selectedUserId.value = node.userId;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function renderNodeLabel(node: Api.SystemManage.MySubordinateTreeNode) {
|
|
|
|
|
|
const label = node.isRoot ? '全部下属' : node.userNickname;
|
|
|
|
|
|
return `${label}${node.subordinateCount ? `(${node.subordinateCount})` : ''}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
|
<ElCard class="subordinate-selector" body-class="subordinate-selector__body">
|
|
|
|
|
|
<template #header>
|
|
|
|
|
|
<div class="flex items-center justify-between gap-12px">
|
|
|
|
|
|
<span class="text-14px font-600">团队成员</span>
|
|
|
|
|
|
<ElTag v-if="props.data" effect="plain">{{ props.data.subordinateCount }}</ElTag>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-loading="props.loading" class="subordinate-selector__content">
|
|
|
|
|
|
<ElEmpty v-if="!props.data" :image-size="88" :description="props.emptyText" />
|
|
|
|
|
|
<ElTree
|
|
|
|
|
|
v-else
|
2026-06-25 21:34:23 +08:00
|
|
|
|
:data="treeData || []"
|
2026-06-14 23:57:42 +08:00
|
|
|
|
node-key="userId"
|
|
|
|
|
|
:current-node-key="selectedUserId || undefined"
|
|
|
|
|
|
:props="{ label: 'userNickname', children: 'children' }"
|
|
|
|
|
|
highlight-current
|
2026-06-21 18:22:44 +08:00
|
|
|
|
:default-expanded-keys="[props.data.userId]"
|
2026-06-14 23:57:42 +08:00
|
|
|
|
expand-on-click-node
|
|
|
|
|
|
class="subordinate-selector__tree"
|
|
|
|
|
|
@node-click="handleNodeClick"
|
|
|
|
|
|
>
|
|
|
|
|
|
<template #default="{ data: node }">
|
|
|
|
|
|
<span class="subordinate-selector__node-label">{{ renderNodeLabel(node) }}</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</ElTree>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</ElCard>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
|
|
|
.subordinate-selector {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
border: 1px solid var(--el-border-color-light);
|
|
|
|
|
|
box-shadow: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.subordinate-selector__body) {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
min-height: 0;
|
|
|
|
|
|
padding: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.subordinate-selector__content {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
min-height: 240px;
|
|
|
|
|
|
overflow: auto;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.subordinate-selector__tree {
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.subordinate-selector__node-label {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
color: var(--el-text-color-regular);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.subordinate-selector__tree .el-tree-node__content) {
|
|
|
|
|
|
height: 36px;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.subordinate-selector__tree .el-tree-node__content:hover) {
|
|
|
|
|
|
background: var(--el-fill-color-light);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.subordinate-selector__tree .el-tree-node.is-current > .el-tree-node__content) {
|
|
|
|
|
|
background: var(--el-color-primary-light-9);
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|