Browse Source

feat(frontend): DocList 目录树支持拖拽宽度与更多操作菜单,接入 BaseTable

目录树宽度可拖拽调节(分隔条)并将节点操作收敛为 t-dropdown 更多菜单(新建子目录/重命名/删除);文档表格接入 BaseTable,改为固定表头 + 内容超高内部滚动(基于视口 calc 高度)。因与目录树样式在同一段 CSS 中交织,同文件一并提交
Spring-AI-1.1.2
wanghanlin 1 week ago
parent
commit
9896dec82a
  1. 119
      frontend/src/views/DocList.vue

119
frontend/src/views/DocList.vue

@ -2,7 +2,7 @@
<t-card class="doc-list-card" title="文档列表" :bordered="false">
<div class="doc-layout">
<!-- 左侧目录树面板 -->
<aside class="doc-tree-panel">
<aside class="doc-tree-panel" :style="{ width: treeWidth + 'px' }">
<div class="tree-panel-header">
<span class="tree-panel-title">📁 文档目录</span>
<t-button variant="text" size="small" @click="loadTreeData">刷新</t-button>
@ -17,29 +17,26 @@
hover
expand-all
empty="暂无分类与目录"
:height="'100%'"
v-model:actived="activedKeys"
@active="onTreeActive"
>
<template #label="{ node }">
<span class="tree-node">
<span class="tree-label-text">{{ node.data.label }}</span>
<!-- 目录节点 hover 时显示 /重命名/删除 -->
<span v-if="node.data.nodeType === 'folder'" class="tree-ops">
<t-button size="small" variant="text" @click.stop="openCreateFolder(node.data.categoryId, node.data.folderId)"></t-button>
<t-button size="small" variant="text" @click.stop="openRenameFolder(node.data)">重命名</t-button>
<t-button size="small" variant="text" theme="danger" @click.stop="removeFolder(node.data)">删除</t-button>
</span>
<!-- 分类节点显示新建目录 -->
<span v-else class="tree-ops">
<t-button size="small" variant="text" @click.stop="openCreateFolder(node.data.categoryId)">新建目录</t-button>
</span>
<span class="tree-label-text" :title="node.data.label">{{ node.data.label }}</span>
<t-dropdown class="tree-ops" trigger="click" :options="nodeActionOptions(node)" @click="(item: any) => onNodeAction(item, node)">
<t-button size="small" variant="text" shape="square" @click.stop>
<template #icon><MoreIcon /></template>
</t-button>
</t-dropdown>
</span>
</template>
</t-tree>
</div>
</aside>
<!-- 可拖拽分隔条 -->
<div class="tree-resizer" :class="{ dragging: isResizing }" @mousedown="startResize"></div>
<!-- 右侧工具栏 + 表格 -->
<div class="doc-content">
<!-- 工具栏搜索 + 筛选 + 批量操作 -->
@ -64,8 +61,10 @@
</template>
</div>
<!-- 表格 -->
<t-table
<!-- 表格外层容器撑满剩余高度表格在其中内部滚动表头固定 -->
<div class="doc-table-wrap">
<BaseTable
:max-height="'calc(100vh - 220px)'"
:data="documents" :columns="columns" row-key="id" :loading="loading"
:pagination="{ current: page, total: total, pageSize: pageSize, showJumper: true }"
:selected-row-keys="Array.from(selectedIds)"
@ -100,7 +99,8 @@
<t-button size="small" variant="text" theme="danger" @click="remove(row.id)">删除</t-button>
</t-space>
</template>
</t-table>
</BaseTable>
</div>
</div>
</div>
@ -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<any[]>([])
const activedKeys = ref<string[]>([])
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; }

Loading…
Cancel
Save