diff --git a/frontend/src/views/DocList.vue b/frontend/src/views/DocList.vue index 6fb56e0..3e958f8 100644 --- a/frontend/src/views/DocList.vue +++ b/frontend/src/views/DocList.vue @@ -2,7 +2,7 @@
-
@@ -152,6 +152,7 @@ import { toast } from '@/utils/toast' import { formatBytes, formatDate } from '@/utils/format' import { useConfirm } from '@/composables/useConfirm' import { useDebounce } from '@/composables/useDebounce' +import { MoreIcon } from 'tdesign-icons-vue-next' const { confirm } = useConfirm() const { debounce } = useDebounce() @@ -182,6 +183,12 @@ const treeData = ref([]) const activedKeys = ref([]) const filterFolder = ref('') const folderTreeLoading = ref(false) +// 目录树宽度(可拖动,不持久化) +const treeWidth = ref(240) +const isResizing = ref(false) +const MIN_TREE_WIDTH = 200 +const MAX_TREE_WIDTH = 480 +let resizeCleanup: (() => void) | null = null // 表格排序状态(服务端排序) const sortInfo = ref<{ sortBy: string; descending: boolean } | null>(null) @@ -217,7 +224,7 @@ function stopPolling() { if (pollTimer) { clearInterval(pollTimer); pollTimer = null } } -onUnmounted(() => { stopPolling() }) +onUnmounted(() => { stopPolling(); if (resizeCleanup) resizeCleanup() }) // ==================== 表格列 ==================== const columns = [ @@ -373,6 +380,30 @@ function clearFolderFilter() { load(1) } +/** 目录树宽度拖拽 */ +function startResize(e: MouseEvent) { + e.preventDefault() + const startX = e.clientX + const startWidth = treeWidth.value + isResizing.value = true + const onMove = (ev: MouseEvent) => { + treeWidth.value = Math.min(MAX_TREE_WIDTH, Math.max(MIN_TREE_WIDTH, startWidth + (ev.clientX - startX))) + } + const onUp = () => { + document.removeEventListener('mousemove', onMove) + document.removeEventListener('mouseup', onUp) + document.body.style.userSelect = '' + document.body.style.cursor = '' + isResizing.value = false + resizeCleanup = null + } + document.addEventListener('mousemove', onMove) + document.addEventListener('mouseup', onUp) + document.body.style.userSelect = 'none' + document.body.style.cursor = 'col-resize' + resizeCleanup = onUp +} + /** 表格排序变化:更新排序状态并重新加载(服务端排序) */ function onSortChange(sort: any) { sortInfo.value = sort && sort.sortBy ? { sortBy: sort.sortBy, descending: !!sort.descending } : null @@ -380,6 +411,30 @@ function onSortChange(sort: any) { } // ==================== 目录操作 ==================== +/** 目录/分类节点操作菜单项 */ +function nodeActionOptions(node: any): any[] { + if (node.data.nodeType === 'folder') { + return [ + { content: '新建子目录', value: 'create' }, + { content: '重命名', value: 'rename' }, + { content: '删除', value: 'remove', theme: 'error', divider: true }, + ] + } + return [{ content: '新建目录', value: 'create' }] +} + +/** 目录/分类节点操作分发 */ +function onNodeAction(item: any, node: any) { + const d = node.data + if (item.value === 'create') { + openCreateFolder(d.categoryId, d.nodeType === 'folder' ? d.folderId : undefined) + } else if (item.value === 'rename') { + openRenameFolder(d) + } else if (item.value === 'remove') { + removeFolder(d) + } +} + function openCreateFolder(categoryId: any, parentId?: any) { folderDialog.value = { visible: true, @@ -573,13 +628,12 @@ loadData() .selected-count { font-size: 12px; font-weight: 600; color: var(--td-text-color-primary); } /* 左右分栏布局 */ -/* 卡片锁定一屏高度,左右各自独立滚动 */ -.doc-list-card { height: calc(100vh - 48px); display: flex; flex-direction: column; } +/* 卡片锁定一屏高度:左侧目录树独立滚动,右侧工具栏固定 + 表格内部滚动 */ +.doc-list-card { height: 100%; display: flex; flex-direction: column; } .doc-list-card :deep(.t-card__body) { flex: 1; min-height: 0; overflow: hidden; display: flex; flex-direction: column; } -.doc-layout { flex: 1; min-height: 0; display: flex; gap: 16px; align-items: stretch; } +.doc-layout { flex: 1; min-height: 0; display: flex; align-items: stretch; } .doc-tree-panel { - width: 240px; flex-shrink: 0; display: flex; flex-direction: column; @@ -587,18 +641,29 @@ loadData() padding-right: 12px; border-right: 1px solid var(--color-border); } -.tree-panel-body { flex: 1; min-height: 0; } -.doc-content { flex: 1; min-width: 0; overflow-y: auto; } +.tree-resizer { + flex-shrink: 0; + width: 6px; + margin-left: -6px; + cursor: col-resize; + background: transparent; + transition: background .2s; +} +.tree-resizer:hover, .tree-resizer.dragging { background: var(--color-primary); } +.tree-panel-body { flex: 1; min-height: 0; overflow-y: auto; overflow-x: hidden; } +.doc-content { flex: 1; min-width: 0; overflow: hidden; padding-left: 16px; display: flex; flex-direction: column; } +/* 表格宿主:撑满工具栏下方剩余高度,表格 max-height:100% 相对它解析,实现内部滚动与固定表头 */ +.doc-table-wrap { flex: 1; min-height: 0; } .tree-panel-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; } .tree-panel-title { font-size: 13px; font-weight: 600; } .tree-loading { padding: 16px; font-size: 13px; color: var(--td-text-color-placeholder); } /* 树节点 hover 操作按钮 */ -.tree-node { display: flex; align-items: center; justify-content: space-between; width: 100%; min-width: 0; } -.tree-label-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.tree-ops { display: none; align-items: center; gap: 0; flex-shrink: 0; } -.tree-node:hover .tree-ops { display: inline-flex; } +.tree-node { display: flex; align-items: center; width: 100%; min-width: 0; } +.tree-label-text { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.tree-ops { display: inline-flex; align-items: center; flex-shrink: 0; margin-left: 4px; visibility: hidden; } +.tree-node:hover .tree-ops { visibility: visible; } .dialog-footer { display: flex; justify-content: flex-end; gap: 8px; margin-top: 8px; } .move-tree-tip { font-size: 12px; color: var(--td-text-color-placeholder); margin-bottom: 8px; }