index.vue 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. <script lang="ts" setup>
  2. import { defaultProps, handleTree } from '@/utils/tree'
  3. import { ElTree } from 'element-plus'
  4. import * as DeptApi from '@/api/system/dept'
  5. import { Search } from '@element-plus/icons-vue'
  6. const props = defineProps({
  7. deptId: {
  8. type: Number,
  9. required: true
  10. },
  11. modelValue: {
  12. type: Number,
  13. default: undefined
  14. },
  15. topId: {
  16. type: Number,
  17. required: true
  18. },
  19. title: {
  20. type: String,
  21. default: '部门'
  22. },
  23. initSelect: {
  24. type: Boolean,
  25. default: true
  26. },
  27. showTitle: {
  28. type: Boolean,
  29. default: true
  30. }
  31. })
  32. const emits = defineEmits(['update:modelValue', 'node-click'])
  33. const deptName = ref('')
  34. const deptList = ref<Tree[]>([])
  35. const treeRef = ref<InstanceType<typeof ElTree>>()
  36. const expandedKeys = ref<number[]>([])
  37. const sortTreeBySort = (treeNodes: Tree[]) => {
  38. if (!treeNodes || !Array.isArray(treeNodes)) return treeNodes
  39. const sortedNodes = [...treeNodes].sort((a, b) => {
  40. const sortA = a.sort != null ? a.sort : 999999
  41. const sortB = b.sort != null ? b.sort : 999999
  42. return sortA - sortB
  43. })
  44. sortedNodes.forEach((node) => {
  45. if (node.children && Array.isArray(node.children)) {
  46. node.children = sortTreeBySort(node.children)
  47. }
  48. })
  49. return sortedNodes
  50. }
  51. const loadTree = async () => {
  52. try {
  53. let id = props.deptId
  54. if (id !== props.topId) {
  55. const depts = await DeptApi.specifiedSimpleDepts(props.topId)
  56. const self = depts.find((item) => item.id === props.deptId)
  57. if (depts.length && !self) {
  58. id = props.topId
  59. }
  60. }
  61. if (props.initSelect) {
  62. emits('update:modelValue', id)
  63. }
  64. const res = await DeptApi.specifiedSimpleDepts(id)
  65. deptList.value = sortTreeBySort(handleTree(res))
  66. // 加载完成后,如果有选中值,尝试高亮并展开
  67. nextTick(() => {
  68. if (props.modelValue && treeRef.value) {
  69. treeRef.value.setCurrentKey(props.modelValue)
  70. expandedKeys.value = [props.modelValue] // 默认展开选中的节点
  71. } else if (deptList.value.length > 0) {
  72. // 默认展开第一级
  73. expandedKeys.value = deptList.value.map((item) => item.id)
  74. }
  75. })
  76. } catch (error) {
  77. console.error('加载部门树失败:', error)
  78. }
  79. }
  80. const handleNodeClick = (data: Tree) => {
  81. // 1. 更新 v-model
  82. emits('update:modelValue', data.id)
  83. // 2. 抛出点击事件供父组件其他用途
  84. emits('node-click', data)
  85. }
  86. /** 筛选节点逻辑 */
  87. const filterNode = (value: string, data: Tree) => {
  88. if (!value) return true
  89. return data.name.includes(value)
  90. }
  91. /** 监听输入框进行过滤 */
  92. watch(deptName, (val) => {
  93. treeRef.value?.filter(val)
  94. })
  95. watch(
  96. () => props.deptId,
  97. (newVal, oldVal) => {
  98. if (newVal !== oldVal) {
  99. loadTree()
  100. }
  101. }
  102. )
  103. watch(
  104. () => props.modelValue,
  105. (newVal) => {
  106. if (newVal && treeRef.value) {
  107. // 设置高亮
  108. treeRef.value.setCurrentKey(newVal)
  109. // 自动展开该节点 (将新ID加入展开数组)
  110. if (!expandedKeys.value.includes(newVal)) {
  111. expandedKeys.value.push(newVal)
  112. }
  113. }
  114. },
  115. { immediate: true }
  116. )
  117. /** 初始化 */
  118. onMounted(() => {
  119. console.log('props :>> ', props)
  120. loadTree()
  121. })
  122. </script>
  123. <template>
  124. <div class="gap-4 flex flex-col h-full">
  125. <h1 v-if="showTitle" class="text-lg font-medium">{{ props.title }}</h1>
  126. <el-input
  127. v-model="deptName"
  128. size="default"
  129. placeholder="请输入部门名称"
  130. clearable
  131. :prefix-icon="Search"
  132. />
  133. <div class="flex-1 relative">
  134. <el-auto-resizer class="absolute">
  135. <template #default="{ height }">
  136. <el-scrollbar :style="{ height: `${height}px` }">
  137. <el-tree
  138. ref="treeRef"
  139. :data="deptList"
  140. :props="defaultProps"
  141. :expand-on-click-node="false"
  142. :filter-node-method="filterNode"
  143. node-key="id"
  144. highlight-current
  145. :default-expanded-keys="expandedKeys"
  146. @node-click="handleNodeClick"
  147. />
  148. </el-scrollbar>
  149. </template>
  150. </el-auto-resizer>
  151. </div>
  152. </div>
  153. </template>