index.vue 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  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, CaretLeft, CaretRight } from '@element-plus/icons-vue'
  6. interface Tree {
  7. id: number
  8. name: string
  9. children?: Tree[]
  10. sort?: number
  11. }
  12. const props = defineProps({
  13. deptId: { type: Number, required: true },
  14. modelValue: { type: Number, default: undefined },
  15. topId: { type: Number, required: true },
  16. title: { type: String, default: '部门' },
  17. initSelect: { type: Boolean, default: true },
  18. showTitle: { type: Boolean, default: true }
  19. })
  20. const emits = defineEmits(['update:modelValue', 'node-click'])
  21. // --- 状态控制 ---
  22. const isCollapsed = ref(false)
  23. const deptName = ref('')
  24. const deptList = ref<Tree[]>([])
  25. const treeRef = ref<InstanceType<typeof ElTree>>()
  26. const expandedKeys = ref<number[]>([])
  27. // --- 逻辑处理 (保持不变) ---
  28. const sortTreeBySort = (treeNodes: Tree[]) => {
  29. if (!treeNodes || !Array.isArray(treeNodes)) return treeNodes
  30. const sortedNodes = [...treeNodes].sort((a, b) => (a.sort ?? 999999) - (b.sort ?? 999999))
  31. sortedNodes.forEach((node) => {
  32. if (node.children) node.children = sortTreeBySort(node.children)
  33. })
  34. return sortedNodes
  35. }
  36. const loadTree = async () => {
  37. try {
  38. let id = props.deptId
  39. if (id !== props.topId) {
  40. const depts = await DeptApi.specifiedSimpleDepts(props.topId)
  41. if (depts.length && !depts.find((item) => item.id === props.deptId)) id = props.topId
  42. }
  43. const res = await DeptApi.specifiedSimpleDepts(id)
  44. if (props.initSelect && props.modelValue && !res.some((item) => item.id === props.modelValue)) {
  45. emits('update:modelValue', id)
  46. }
  47. deptList.value = sortTreeBySort(handleTree(res))
  48. nextTick(() => {
  49. const targetKey = props.modelValue ?? (props.initSelect ? id : null)
  50. if (targetKey && treeRef.value) {
  51. treeRef.value.setCurrentKey(targetKey)
  52. if (!expandedKeys.value.includes(targetKey)) expandedKeys.value.push(targetKey)
  53. else if (deptList.value.length > 0)
  54. expandedKeys.value = deptList.value.map((item) => item.id)
  55. }
  56. })
  57. } catch (e) {
  58. console.error(e)
  59. }
  60. }
  61. const handleNodeClick = (data: Tree) => {
  62. emits('update:modelValue', data.id)
  63. emits('node-click', data)
  64. }
  65. const filterNode = (val: string, data: Tree) => !val || data.name.includes(val)
  66. watch(deptName, (val) => treeRef.value?.filter(val))
  67. watch(() => props.deptId, loadTree)
  68. watch(
  69. () => props.modelValue,
  70. (val) => {
  71. if (val && treeRef.value) treeRef.value.setCurrentKey(val)
  72. if (val && !expandedKeys.value.includes(val)) {
  73. expandedKeys.value.push(val)
  74. }
  75. },
  76. { immediate: true }
  77. )
  78. onMounted(loadTree)
  79. </script>
  80. <template>
  81. <div
  82. class="dept-aside-container relative bg-white dark:bg-[#1d1e1f] shadow rounded-lg row-span-4 transition-all duration-300 ease-in-out overflow-visible"
  83. :class="[isCollapsed ? 'is-collapsed' : 'p-4']"
  84. >
  85. <div v-show="!isCollapsed" class="h-full flex flex-col gap-4 overflow-hidden w-full">
  86. <h1 v-if="showTitle" class="text-lg font-medium truncate shrink-0">{{ props.title }}</h1>
  87. <div class="shrink-0">
  88. <el-input v-model="deptName" placeholder="请输入部门名称" clearable :prefix-icon="Search" />
  89. </div>
  90. <div class="flex-1 relative overflow-hidden">
  91. <el-auto-resizer class="absolute">
  92. <template #default="{ height }">
  93. <el-scrollbar :style="{ height: `${height}px` }">
  94. <el-tree
  95. ref="treeRef"
  96. :data="deptList"
  97. :props="defaultProps"
  98. :expand-on-click-node="false"
  99. :filter-node-method="filterNode"
  100. node-key="id"
  101. highlight-current
  102. :default-expanded-keys="expandedKeys"
  103. @node-click="handleNodeClick"
  104. />
  105. </el-scrollbar>
  106. </template>
  107. </el-auto-resizer>
  108. </div>
  109. </div>
  110. <div class="collapse-handle" @click="isCollapsed = !isCollapsed">
  111. <el-icon size="12">
  112. <CaretLeft v-if="!isCollapsed" />
  113. <CaretRight v-else />
  114. </el-icon>
  115. </div>
  116. </div>
  117. </template>
  118. <style scoped>
  119. .dept-aside-container {
  120. /* 关键点 1:初始宽度设为 15% */
  121. width: 14vw; /* 或者使用百分比,但在 Grid 容器内使用 vw 更稳定 */
  122. height: calc(
  123. 100vh - 20px - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height)
  124. );
  125. min-width: 200px; /* 防止在极小屏幕下 15% 太窄看不清 */
  126. box-sizing: border-box;
  127. flex-shrink: 0;
  128. }
  129. /* 关键点 2:折叠状态 */
  130. .dept-aside-container.is-collapsed {
  131. width: 0 !important;
  132. min-width: 0 !important;
  133. padding: 0 !important;
  134. margin-right: -16px; /* 抵消父级 grid 的 gap-x-4,让右侧内容贴合 */
  135. overflow: visible !important;
  136. pointer-events: none; /* 折叠后不响应鼠标事件,除了 handle */
  137. /* opacity: 0; */
  138. box-shadow: none;
  139. }
  140. /* 即使父级折叠,handle 也要可见并可点击 */
  141. .collapse-handle {
  142. position: absolute;
  143. top: 50%;
  144. right: -14px;
  145. z-index: 200;
  146. display: flex;
  147. width: 14px;
  148. height: 60px;
  149. color: var(--el-text-color-secondary);
  150. pointer-events: auto;
  151. cursor: pointer;
  152. background-color: var(--el-bg-color);
  153. border: 1px solid var(--el-border-color-light);
  154. border-left: none;
  155. border-radius: 0 12px 12px 0;
  156. transform: translateY(-50%);
  157. box-shadow: 2px 0 6px rgb(0 0 0 / 5%);
  158. transition: right 0.3s;
  159. align-items: center;
  160. justify-content: center;
  161. }
  162. .is-collapsed .collapse-handle {
  163. right: -8px; /* 在边缘露出一半 */
  164. border-left: 1px solid var(--el-border-color-light);
  165. }
  166. .collapse-handle:hover {
  167. color: var(--el-color-primary);
  168. background-color: var(--el-fill-color-light);
  169. }
  170. .truncate {
  171. overflow: hidden;
  172. text-overflow: ellipsis;
  173. white-space: nowrap;
  174. }
  175. </style>