| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300 |
- <template>
- <el-col
- :class="{ leftcontent: true, collapsed: isCollapsed }"
- :span="isCollapsed ? 0 : 4"
- :xs="24"
- >
- <ContentWrap class="h-[85vh]">
- <div
- class="dept-tree"
- :class="{ 'is-collapsed': isCollapsed }"
- style="overflow-y: auto; overflow-x: auto"
- >
- <div class="head-container" style="display: flex; flex-direction: row">
- <el-input
- v-model="deptName"
- class="mb-18px"
- style="height: 35px"
- clearable
- placeholder="请输入部门名称"
- >
- <template #prefix>
- <Icon icon="ep:search" />
- </template>
- </el-input>
- </div>
- <div class="tree-container">
- <el-tree
- v-show="!isCollapsed"
- ref="treeRef"
- :data="deptList"
- :expand-on-click-node="false"
- :filter-node-method="filterNode"
- :props="defaultProps"
- :default-expanded-keys="firstLevelKeys"
- highlight-current
- node-key="id"
- @node-click="handleNodeClick"
- @node-contextmenu="handleRightClick"
- />
- </div>
- </div>
- <div
- v-show="menuVisible"
- class="custom-menu"
- :style="{ left: menuX + 'px', top: menuY + 'px' }"
- >
- <ul>
- <li @click="handleMenuClick('add')">新增子节点</li>
- <li @click="handleMenuClick('edit')">重命名</li>
- <li @click="handleMenuClick('delete')">删除</li>
- </ul>
- </div>
- </ContentWrap>
- </el-col>
- <!-- 切换按钮移到外部,始终可见 -->
- <button
- :class="isCollapsed ? 'tree-toggle--outside' : 'tree-toggle--outside2'"
- type="button"
- :aria-label="isCollapsed ? '展开组织树' : '收起组织树'"
- @click="toggleCollapsed"
- :title="isCollapsed ? '展开' : '收起'"
- >
- <img class="tree-toggle__img" :src="isCollapsed ? hideimage : showimage" alt="" />
- </button>
- </template>
- <script lang="ts" setup>
- import { ElTree } from 'element-plus'
- import * as DeptApi from '@/api/system/dept'
- import { defaultProps, handleTree } from '@/utils/tree'
- import { useTreeStore } from '@/store/modules/usersTreeStore'
- import hideimage from '@/assets/imgs/leftTree-hide.png'
- import showimage from '@/assets/imgs/leftTree-show.png'
- defineOptions({ name: 'SystemUserDeptTree' })
- type Props = {
- collapsed?: boolean
- collapsible?: boolean
- }
- const props = defineProps<Props>()
- const emits = defineEmits<{
- (e: 'node-click', row: any): void
- (e: 'update:collapsed', value: boolean): void
- (e: 'toggle', value: boolean): void
- }>()
- const collapsible = computed(() => props.collapsible !== false)
- const collapsedLocal = ref(false)
- const isCollapsed = computed(() => (props.collapsed ?? collapsedLocal.value) === true)
- const deptName = ref('')
- const deptList = ref<Tree[]>([]) // 树形结构
- const treeRef = ref<InstanceType<typeof ElTree>>()
- const menuVisible = ref(false)
- const menuX = ref(0)
- const menuY = ref(0)
- const firstLevelKeys = ref([])
- let selectedNode = null
- const treeStore = useTreeStore()
- watch(
- () => props.collapsed,
- (val) => {
- if (typeof val === 'boolean') collapsedLocal.value = val
- },
- { immediate: true }
- )
- const toggleCollapsed = () => {
- const next = !isCollapsed.value
- collapsedLocal.value = next
- emits('update:collapsed', next)
- emits('toggle', next)
- }
- const handleRightClick = (event, { node, data }) => {
- event.preventDefault()
- menuX.value = event.clientX
- menuY.value = event.clientY
- selectedNode = data // 存储当前操作的节点数据 :ml-citation{ref="7" data="citationList"}
- //menuVisible.value = true;
- }
- const treeContainer = ref(null)
- const setHeight = () => {
- if (!treeContainer.value) return
- const windowHeight = window.innerHeight
- treeContainer.value.style.height = `${windowHeight * 0.78}px` // 60px 底部预留
- }
- const handleMenuClick = (action) => {
- switch (action) {
- case 'add':
- // 调用新增节点逻辑 :ml-citation{ref="4" data="citationList"}
- break
- case 'edit':
- // 调用编辑节点逻辑 :ml-citation{ref="7" data="citationList"}
- break
- case 'delete':
- // 调用删除节点逻辑 :ml-citation{ref="4" data="citationList"}
- break
- }
- menuVisible.value = false
- }
- /** 获得部门树 */
- const getTree = async () => {
- const res = await DeptApi.getSimpleDeptList()
- deptList.value = []
- deptList.value.push(...handleTree(res))
- firstLevelKeys.value = deptList.value.map((node) => node.id)
- }
- /** 基于名字过滤 */
- const filterNode = (name: string, data: Tree) => {
- if (!name) return true
- return data.name.includes(name)
- }
- /** 处理部门被点击 */
- const handleNodeClick = async (row: { [key: string]: any }) => {
- emits('node-click', row)
- treeStore.setSelectedId(row.id)
- }
- /** 监听deptName */
- watch(deptName, (val) => {
- treeRef.value!.filter(val)
- })
- /** 初始化 */
- onMounted(async () => {
- await getTree()
- // setHeight()
- // window.addEventListener('resize', setHeight)
- })
- onUnmounted(() => {
- window.removeEventListener('resize', setHeight)
- })
- </script>
- <style lang="scss" scoped>
- .dept-tree {
- height: 85vh;
- overflow-y: auto;
- &.is-collapsed {
- overflow-y: hidden;
- }
- }
- .custom-menu {
- position: fixed;
- background: white;
- border: 1px solid #ccc;
- box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.1);
- z-index: 1000;
- }
- .custom-menu ul {
- list-style: none;
- padding: 0;
- margin: 0;
- }
- .custom-menu li {
- padding: 8px 20px;
- cursor: pointer;
- }
- .custom-menu li:hover {
- background: #f5f5f5;
- }
- .tree-container {
- overflow-y: auto;
- max-height: calc(85vh - 100px);
- min-width: 100%;
- // border: 1px solid #e4e7ed;
- border-radius: 4px;
- position: relative;
- overflow-x: visible;
- }
- // 外部按钮样式 - 定位到左侧边缘
- .tree-toggle--outside {
- position: absolute;
- top: 30vh;
- left: 0;
- transform: translate(-50%, 0%);
- z-index: 10;
- background: transparent;
- border: none;
- cursor: pointer;
- padding: 0;
- line-height: 0;
- transition: color 0.3s ease;
- }
- .tree-toggle--outside2 {
- position: absolute;
- top: 38%;
- left: 16.5%;
- transform: translate(-50%, 0%);
- z-index: 10;
- background: transparent;
- border: none;
- cursor: pointer;
- padding: 0;
- line-height: 0;
- transition: color 0.3s ease;
- }
- .tree-toggle--outside:hover {
- filter: brightness(0.9);
- transition: color 0.3s ease;
- }
- .tree-toggle--outside2:hover {
- filter: brightness(0.9);
- transition: color 0.3s ease;
- }
- .tree-toggle__img {
- display: block;
- width: auto;
- height: auto;
- max-width: 100%;
- max-height: 100%;
- user-select: none;
- }
- .dept-tree.is-collapsed .tree-container {
- overflow-y: auto;
- }
- .leftcontent {
- transition: width 0.3s ease;
- position: relative;
- }
- .leftcontent.collapsed {
- width: 0 !important;
- overflow: hidden;
- }
- // 小屏幕下隐藏按钮
- @media (max-width: 768px) {
- .tree-toggle--outside,
- .tree-toggle--outside2 {
- display: none;
- }
- }
- ::-webkit-scrollbar {
- width: 5px; /* 设置滚动条宽度 */
- }
- ::-webkit-scrollbar-thumb {
- background-color: darkgrey; /* 设置滚动条颜色 */
- border-radius: 10px; /* 设置圆角 */
- }
- ::-webkit-scrollbar-track {
- background: transparent; /* 设置轨道颜色 */
- }
- </style>
|