| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233 |
- <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
- }
- type RequestApi = 'specifiedSimpleDepts' | 'getSimpleDeptList'
- 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 },
- requestApi: {
- type: String as PropType<RequestApi>,
- default: 'specifiedSimpleDepts'
- }
- })
- 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 resolveFallbackId = (depts: Tree[], preferredId: number) => {
- if (depts.some((item) => item.id === preferredId)) return preferredId
- if (depts.some((item) => item.id === props.topId)) return props.topId
- return depts[0]?.id
- }
- const loadDeptData = async () => {
- if (props.requestApi === 'getSimpleDeptList') {
- const depts = (await DeptApi.getSimpleDeptList()) as Tree[]
- return {
- depts,
- currentId: resolveFallbackId(depts, props.deptId)
- }
- }
- let id = props.deptId
- if (id !== props.topId) {
- const depts = await DeptApi.specifiedSimpleDepts(props.topId)
- if (!depts.some((item) => Number(item.id) === Number(props.deptId))) id = props.topId
- }
- const depts = (await DeptApi.specifiedSimpleDepts(id)) as Tree[]
- return {
- depts,
- currentId: id
- }
- }
- const loadTree = async () => {
- try {
- const { depts, currentId } = await loadDeptData()
- const targetKey = props.modelValue ?? (props.initSelect ? currentId : null)
- // Only initialize the parent value when no external selection was provided.
- if (props.initSelect && props.modelValue === undefined && currentId !== undefined) {
- emits('update:modelValue', currentId)
- }
- if (targetKey && !expandedKeys.value.includes(targetKey)) {
- expandedKeys.value = [...expandedKeys.value, targetKey]
- }
- deptList.value = sortTreeBySort(handleTree(depts))
- nextTick(() => {
- if (targetKey && treeRef.value) {
- treeRef.value.setCurrentKey(targetKey)
- }
- })
- } 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, props.topId, props.requestApi], 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] rounded-md 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
- size="default"
- :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
- :current-node-key="modelValue"
- :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>
|