| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197 |
- <script lang="ts" setup>
- import { defaultProps, handleTree } from '@/utils/tree'
- import { ElTree } from 'element-plus'
- import * as DeptApi from '@/api/system/dept'
- import { Search, CaretLeft, CaretRight } from '@element-plus/icons-vue'
- interface Tree {
- id: number
- name: string
- children?: Tree[]
- sort?: number
- }
- const props = defineProps({
- deptId: { type: Number, required: true },
- modelValue: { type: Number, default: undefined },
- topId: { type: Number, required: true },
- title: { type: String, default: '部门' },
- initSelect: { type: Boolean, default: true },
- showTitle: { type: Boolean, default: true }
- })
- const emits = defineEmits(['update:modelValue', 'node-click'])
- // --- 状态控制 ---
- const isCollapsed = ref(false)
- const deptName = ref('')
- const deptList = ref<Tree[]>([])
- const treeRef = ref<InstanceType<typeof ElTree>>()
- const expandedKeys = ref<number[]>([])
- // --- 逻辑处理 (保持不变) ---
- const sortTreeBySort = (treeNodes: Tree[]) => {
- if (!treeNodes || !Array.isArray(treeNodes)) return treeNodes
- const sortedNodes = [...treeNodes].sort((a, b) => (a.sort ?? 999999) - (b.sort ?? 999999))
- sortedNodes.forEach((node) => {
- if (node.children) node.children = sortTreeBySort(node.children)
- })
- return sortedNodes
- }
- const loadTree = async () => {
- try {
- let id = props.deptId
- if (id !== props.topId) {
- const depts = await DeptApi.specifiedSimpleDepts(props.topId)
- if (depts.length && !depts.find((item) => item.id === props.deptId)) id = props.topId
- }
- const res = await DeptApi.specifiedSimpleDepts(id)
- if (props.initSelect && props.modelValue && !res.some((item) => item.id === props.modelValue)) {
- emits('update:modelValue', id)
- }
- deptList.value = sortTreeBySort(handleTree(res))
- nextTick(() => {
- const targetKey = props.modelValue ?? (props.initSelect ? id : null)
- if (targetKey && treeRef.value) {
- treeRef.value.setCurrentKey(targetKey)
- if (!expandedKeys.value.includes(targetKey)) expandedKeys.value.push(targetKey)
- else if (deptList.value.length > 0)
- expandedKeys.value = deptList.value.map((item) => item.id)
- }
- })
- } catch (e) {
- console.error(e)
- }
- }
- const handleNodeClick = (data: Tree) => {
- emits('update:modelValue', data.id)
- emits('node-click', data)
- }
- const filterNode = (val: string, data: Tree) => !val || data.name.includes(val)
- watch(deptName, (val) => treeRef.value?.filter(val))
- watch(() => props.deptId, loadTree)
- watch(
- () => props.modelValue,
- (val) => {
- if (val && treeRef.value) treeRef.value.setCurrentKey(val)
- if (val && !expandedKeys.value.includes(val)) {
- expandedKeys.value.push(val)
- }
- },
- { immediate: true }
- )
- onMounted(loadTree)
- </script>
- <template>
- <div
- 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"
- :class="[isCollapsed ? 'is-collapsed' : 'p-4']"
- >
- <div v-show="!isCollapsed" class="h-full flex flex-col gap-4 overflow-hidden w-full">
- <h1 v-if="showTitle" class="text-lg font-medium truncate shrink-0">{{ props.title }}</h1>
- <div class="shrink-0">
- <el-input v-model="deptName" placeholder="请输入部门名称" clearable :prefix-icon="Search" />
- </div>
- <div class="flex-1 relative overflow-hidden">
- <el-auto-resizer class="absolute">
- <template #default="{ height }">
- <el-scrollbar :style="{ height: `${height}px` }">
- <el-tree
- ref="treeRef"
- :data="deptList"
- :props="defaultProps"
- :expand-on-click-node="false"
- :filter-node-method="filterNode"
- node-key="id"
- highlight-current
- :default-expanded-keys="expandedKeys"
- @node-click="handleNodeClick"
- />
- </el-scrollbar>
- </template>
- </el-auto-resizer>
- </div>
- </div>
- <div class="collapse-handle" @click="isCollapsed = !isCollapsed">
- <el-icon size="12">
- <CaretLeft v-if="!isCollapsed" />
- <CaretRight v-else />
- </el-icon>
- </div>
- </div>
- </template>
- <style scoped>
- .dept-aside-container {
- /* 关键点 1:初始宽度设为 15% */
- width: 14vw; /* 或者使用百分比,但在 Grid 容器内使用 vw 更稳定 */
- height: calc(
- 100vh - 20px - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height)
- );
- min-width: 200px; /* 防止在极小屏幕下 15% 太窄看不清 */
- box-sizing: border-box;
- flex-shrink: 0;
- }
- /* 关键点 2:折叠状态 */
- .dept-aside-container.is-collapsed {
- width: 0 !important;
- min-width: 0 !important;
- padding: 0 !important;
- margin-right: -16px; /* 抵消父级 grid 的 gap-x-4,让右侧内容贴合 */
- overflow: visible !important;
- pointer-events: none; /* 折叠后不响应鼠标事件,除了 handle */
- /* opacity: 0; */
- box-shadow: none;
- }
- /* 即使父级折叠,handle 也要可见并可点击 */
- .collapse-handle {
- position: absolute;
- top: 50%;
- right: -14px;
- z-index: 200;
- display: flex;
- width: 14px;
- height: 60px;
- color: var(--el-text-color-secondary);
- pointer-events: auto;
- cursor: pointer;
- background-color: var(--el-bg-color);
- border: 1px solid var(--el-border-color-light);
- border-left: none;
- border-radius: 0 12px 12px 0;
- transform: translateY(-50%);
- box-shadow: 2px 0 6px rgb(0 0 0 / 5%);
- transition: right 0.3s;
- align-items: center;
- justify-content: center;
- }
- .is-collapsed .collapse-handle {
- right: -8px; /* 在边缘露出一半 */
- border-left: 1px solid var(--el-border-color-light);
- }
- .collapse-handle:hover {
- color: var(--el-color-primary);
- background-color: var(--el-fill-color-light);
- }
- .truncate {
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- </style>
|