DeptTree2.vue 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  1. <template>
  2. <el-col
  3. :class="{ leftcontent: true, collapsed: isCollapsed }"
  4. :span="isCollapsed ? 0 : 4"
  5. :xs="24"
  6. >
  7. <ContentWrap class="h-[85vh]">
  8. <div
  9. class="dept-tree"
  10. :class="{ 'is-collapsed': isCollapsed }"
  11. style="overflow-y: auto; overflow-x: auto"
  12. >
  13. <div class="head-container" style="display: flex; flex-direction: row">
  14. <el-input
  15. v-model="deptName"
  16. class="mb-18px"
  17. style="height: 35px"
  18. clearable
  19. placeholder="请输入部门名称"
  20. >
  21. <template #prefix>
  22. <Icon icon="ep:search" />
  23. </template>
  24. </el-input>
  25. </div>
  26. <div class="tree-container">
  27. <el-tree
  28. v-show="!isCollapsed"
  29. ref="treeRef"
  30. :data="deptList"
  31. :expand-on-click-node="false"
  32. :filter-node-method="filterNode"
  33. :props="defaultProps"
  34. :default-expanded-keys="firstLevelKeys"
  35. highlight-current
  36. node-key="id"
  37. @node-click="handleNodeClick"
  38. @node-contextmenu="handleRightClick"
  39. />
  40. </div>
  41. </div>
  42. <div
  43. v-show="menuVisible"
  44. class="custom-menu"
  45. :style="{ left: menuX + 'px', top: menuY + 'px' }"
  46. >
  47. <ul>
  48. <li @click="handleMenuClick('add')">新增子节点</li>
  49. <li @click="handleMenuClick('edit')">重命名</li>
  50. <li @click="handleMenuClick('delete')">删除</li>
  51. </ul>
  52. </div>
  53. </ContentWrap>
  54. </el-col>
  55. <!-- 切换按钮移到外部,始终可见 -->
  56. <button
  57. :class="isCollapsed ? 'tree-toggle--outside' : 'tree-toggle--outside2'"
  58. type="button"
  59. :aria-label="isCollapsed ? '展开组织树' : '收起组织树'"
  60. @click="toggleCollapsed"
  61. :title="isCollapsed ? '展开' : '收起'"
  62. >
  63. <img class="tree-toggle__img" :src="isCollapsed ? hideimage : showimage" alt="" />
  64. </button>
  65. </template>
  66. <script lang="ts" setup>
  67. import { ElTree } from 'element-plus'
  68. import * as DeptApi from '@/api/system/dept'
  69. import { defaultProps, handleTree } from '@/utils/tree'
  70. import { useTreeStore } from '@/store/modules/usersTreeStore'
  71. import hideimage from '@/assets/imgs/leftTree-hide.png'
  72. import showimage from '@/assets/imgs/leftTree-show.png'
  73. defineOptions({ name: 'SystemUserDeptTree' })
  74. type Props = {
  75. collapsed?: boolean
  76. collapsible?: boolean
  77. }
  78. const props = defineProps<Props>()
  79. const emits = defineEmits<{
  80. (e: 'node-click', row: any): void
  81. (e: 'update:collapsed', value: boolean): void
  82. (e: 'toggle', value: boolean): void
  83. }>()
  84. const collapsible = computed(() => props.collapsible !== false)
  85. const collapsedLocal = ref(false)
  86. const isCollapsed = computed(() => (props.collapsed ?? collapsedLocal.value) === true)
  87. const deptName = ref('')
  88. const deptList = ref<Tree[]>([]) // 树形结构
  89. const treeRef = ref<InstanceType<typeof ElTree>>()
  90. const menuVisible = ref(false)
  91. const menuX = ref(0)
  92. const menuY = ref(0)
  93. const firstLevelKeys = ref([])
  94. let selectedNode = null
  95. const treeStore = useTreeStore()
  96. watch(
  97. () => props.collapsed,
  98. (val) => {
  99. if (typeof val === 'boolean') collapsedLocal.value = val
  100. },
  101. { immediate: true }
  102. )
  103. const toggleCollapsed = () => {
  104. const next = !isCollapsed.value
  105. collapsedLocal.value = next
  106. emits('update:collapsed', next)
  107. emits('toggle', next)
  108. }
  109. const handleRightClick = (event, { node, data }) => {
  110. event.preventDefault()
  111. menuX.value = event.clientX
  112. menuY.value = event.clientY
  113. selectedNode = data // 存储当前操作的节点数据 ‌:ml-citation{ref="7" data="citationList"}
  114. //menuVisible.value = true;
  115. }
  116. const treeContainer = ref(null)
  117. const setHeight = () => {
  118. if (!treeContainer.value) return
  119. const windowHeight = window.innerHeight
  120. treeContainer.value.style.height = `${windowHeight * 0.78}px` // 60px 底部预留
  121. }
  122. const handleMenuClick = (action) => {
  123. switch (action) {
  124. case 'add':
  125. // 调用新增节点逻辑 ‌:ml-citation{ref="4" data="citationList"}
  126. break
  127. case 'edit':
  128. // 调用编辑节点逻辑 ‌:ml-citation{ref="7" data="citationList"}
  129. break
  130. case 'delete':
  131. // 调用删除节点逻辑 ‌:ml-citation{ref="4" data="citationList"}
  132. break
  133. }
  134. menuVisible.value = false
  135. }
  136. /** 获得部门树 */
  137. const getTree = async () => {
  138. const res = await DeptApi.getSimpleDeptList()
  139. deptList.value = []
  140. deptList.value.push(...handleTree(res))
  141. firstLevelKeys.value = deptList.value.map((node) => node.id)
  142. }
  143. /** 基于名字过滤 */
  144. const filterNode = (name: string, data: Tree) => {
  145. if (!name) return true
  146. return data.name.includes(name)
  147. }
  148. /** 处理部门被点击 */
  149. const handleNodeClick = async (row: { [key: string]: any }) => {
  150. emits('node-click', row)
  151. treeStore.setSelectedId(row.id)
  152. }
  153. /** 监听deptName */
  154. watch(deptName, (val) => {
  155. treeRef.value!.filter(val)
  156. })
  157. /** 初始化 */
  158. onMounted(async () => {
  159. await getTree()
  160. // setHeight()
  161. // window.addEventListener('resize', setHeight)
  162. })
  163. onUnmounted(() => {
  164. window.removeEventListener('resize', setHeight)
  165. })
  166. </script>
  167. <style lang="scss" scoped>
  168. .dept-tree {
  169. height: 85vh;
  170. overflow-y: auto;
  171. &.is-collapsed {
  172. overflow-y: hidden;
  173. }
  174. }
  175. .custom-menu {
  176. position: fixed;
  177. background: white;
  178. border: 1px solid #ccc;
  179. box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.1);
  180. z-index: 1000;
  181. }
  182. .custom-menu ul {
  183. list-style: none;
  184. padding: 0;
  185. margin: 0;
  186. }
  187. .custom-menu li {
  188. padding: 8px 20px;
  189. cursor: pointer;
  190. }
  191. .custom-menu li:hover {
  192. background: #f5f5f5;
  193. }
  194. .tree-container {
  195. overflow-y: auto;
  196. max-height: calc(85vh - 100px);
  197. min-width: 100%;
  198. // border: 1px solid #e4e7ed;
  199. border-radius: 4px;
  200. position: relative;
  201. overflow-x: visible;
  202. }
  203. // 外部按钮样式 - 定位到左侧边缘
  204. .tree-toggle--outside {
  205. position: absolute;
  206. top: 30vh;
  207. left: 0;
  208. transform: translate(-50%, 0%);
  209. z-index: 10;
  210. background: transparent;
  211. border: none;
  212. cursor: pointer;
  213. padding: 0;
  214. line-height: 0;
  215. transition: color 0.3s ease;
  216. }
  217. .tree-toggle--outside2 {
  218. position: absolute;
  219. top: 38%;
  220. left: 16.5%;
  221. transform: translate(-50%, 0%);
  222. z-index: 10;
  223. background: transparent;
  224. border: none;
  225. cursor: pointer;
  226. padding: 0;
  227. line-height: 0;
  228. transition: color 0.3s ease;
  229. }
  230. .tree-toggle--outside:hover {
  231. filter: brightness(0.9);
  232. transition: color 0.3s ease;
  233. }
  234. .tree-toggle--outside2:hover {
  235. filter: brightness(0.9);
  236. transition: color 0.3s ease;
  237. }
  238. .tree-toggle__img {
  239. display: block;
  240. width: auto;
  241. height: auto;
  242. max-width: 100%;
  243. max-height: 100%;
  244. user-select: none;
  245. }
  246. .dept-tree.is-collapsed .tree-container {
  247. overflow-y: auto;
  248. }
  249. .leftcontent {
  250. transition: width 0.3s ease;
  251. position: relative;
  252. }
  253. .leftcontent.collapsed {
  254. width: 0 !important;
  255. overflow: hidden;
  256. }
  257. // 小屏幕下隐藏按钮
  258. @media (max-width: 768px) {
  259. .tree-toggle--outside,
  260. .tree-toggle--outside2 {
  261. display: none;
  262. }
  263. }
  264. ::-webkit-scrollbar {
  265. width: 5px; /* 设置滚动条宽度 */
  266. }
  267. ::-webkit-scrollbar-thumb {
  268. background-color: darkgrey; /* 设置滚动条颜色 */
  269. border-radius: 10px; /* 设置圆角 */
  270. }
  271. ::-webkit-scrollbar-track {
  272. background: transparent; /* 设置轨道颜色 */
  273. }
  274. </style>