index.vue 6.7 KB

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