|
|
@ -2,7 +2,7 @@ |
|
|
<t-card class="doc-list-card" title="文档列表" :bordered="false"> |
|
|
<t-card class="doc-list-card" title="文档列表" :bordered="false"> |
|
|
<div class="doc-layout"> |
|
|
<div class="doc-layout"> |
|
|
<!-- 左侧:目录树面板 --> |
|
|
<!-- 左侧:目录树面板 --> |
|
|
<aside class="doc-tree-panel"> |
|
|
|
|
|
|
|
|
<aside class="doc-tree-panel" :style="{ width: treeWidth + 'px' }"> |
|
|
<div class="tree-panel-header"> |
|
|
<div class="tree-panel-header"> |
|
|
<span class="tree-panel-title">📁 文档目录</span> |
|
|
<span class="tree-panel-title">📁 文档目录</span> |
|
|
<t-button variant="text" size="small" @click="loadTreeData">刷新</t-button> |
|
|
<t-button variant="text" size="small" @click="loadTreeData">刷新</t-button> |
|
|
@ -17,29 +17,26 @@ |
|
|
hover |
|
|
hover |
|
|
expand-all |
|
|
expand-all |
|
|
empty="暂无分类与目录" |
|
|
empty="暂无分类与目录" |
|
|
:height="'100%'" |
|
|
|
|
|
v-model:actived="activedKeys" |
|
|
v-model:actived="activedKeys" |
|
|
@active="onTreeActive" |
|
|
@active="onTreeActive" |
|
|
> |
|
|
> |
|
|
<template #label="{ node }"> |
|
|
<template #label="{ node }"> |
|
|
<span class="tree-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> |
|
|
</span> |
|
|
</template> |
|
|
</template> |
|
|
</t-tree> |
|
|
</t-tree> |
|
|
</div> |
|
|
</div> |
|
|
</aside> |
|
|
</aside> |
|
|
|
|
|
|
|
|
|
|
|
<!-- 可拖拽分隔条 --> |
|
|
|
|
|
<div class="tree-resizer" :class="{ dragging: isResizing }" @mousedown="startResize"></div> |
|
|
|
|
|
|
|
|
<!-- 右侧:工具栏 + 表格 --> |
|
|
<!-- 右侧:工具栏 + 表格 --> |
|
|
<div class="doc-content"> |
|
|
<div class="doc-content"> |
|
|
<!-- 工具栏:搜索 + 筛选 + 批量操作 --> |
|
|
<!-- 工具栏:搜索 + 筛选 + 批量操作 --> |
|
|
@ -64,8 +61,10 @@ |
|
|
</template> |
|
|
</template> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<!-- 表格 --> |
|
|
|
|
|
<t-table |
|
|
|
|
|
|
|
|
<!-- 表格:外层容器撑满剩余高度,表格在其中内部滚动(表头固定) --> |
|
|
|
|
|
<div class="doc-table-wrap"> |
|
|
|
|
|
<BaseTable |
|
|
|
|
|
:max-height="'calc(100vh - 220px)'" |
|
|
:data="documents" :columns="columns" row-key="id" :loading="loading" |
|
|
:data="documents" :columns="columns" row-key="id" :loading="loading" |
|
|
:pagination="{ current: page, total: total, pageSize: pageSize, showJumper: true }" |
|
|
:pagination="{ current: page, total: total, pageSize: pageSize, showJumper: true }" |
|
|
:selected-row-keys="Array.from(selectedIds)" |
|
|
: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-button size="small" variant="text" theme="danger" @click="remove(row.id)">删除</t-button> |
|
|
</t-space> |
|
|
</t-space> |
|
|
</template> |
|
|
</template> |
|
|
</t-table> |
|
|
|
|
|
|
|
|
</BaseTable> |
|
|
|
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
@ -152,6 +152,7 @@ import { toast } from '@/utils/toast' |
|
|
import { formatBytes, formatDate } from '@/utils/format' |
|
|
import { formatBytes, formatDate } from '@/utils/format' |
|
|
import { useConfirm } from '@/composables/useConfirm' |
|
|
import { useConfirm } from '@/composables/useConfirm' |
|
|
import { useDebounce } from '@/composables/useDebounce' |
|
|
import { useDebounce } from '@/composables/useDebounce' |
|
|
|
|
|
import { MoreIcon } from 'tdesign-icons-vue-next' |
|
|
|
|
|
|
|
|
const { confirm } = useConfirm() |
|
|
const { confirm } = useConfirm() |
|
|
const { debounce } = useDebounce() |
|
|
const { debounce } = useDebounce() |
|
|
@ -182,6 +183,12 @@ const treeData = ref<any[]>([]) |
|
|
const activedKeys = ref<string[]>([]) |
|
|
const activedKeys = ref<string[]>([]) |
|
|
const filterFolder = ref('') |
|
|
const filterFolder = ref('') |
|
|
const folderTreeLoading = ref(false) |
|
|
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) |
|
|
const sortInfo = ref<{ sortBy: string; descending: boolean } | null>(null) |
|
|
|
|
|
|
|
|
@ -217,7 +224,7 @@ function stopPolling() { |
|
|
if (pollTimer) { clearInterval(pollTimer); pollTimer = null } |
|
|
if (pollTimer) { clearInterval(pollTimer); pollTimer = null } |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
onUnmounted(() => { stopPolling() }) |
|
|
|
|
|
|
|
|
onUnmounted(() => { stopPolling(); if (resizeCleanup) resizeCleanup() }) |
|
|
|
|
|
|
|
|
// ==================== 表格列 ==================== |
|
|
// ==================== 表格列 ==================== |
|
|
const columns = [ |
|
|
const columns = [ |
|
|
@ -373,6 +380,30 @@ function clearFolderFilter() { |
|
|
load(1) |
|
|
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) { |
|
|
function onSortChange(sort: any) { |
|
|
sortInfo.value = sort && sort.sortBy ? { sortBy: sort.sortBy, descending: !!sort.descending } : null |
|
|
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) { |
|
|
function openCreateFolder(categoryId: any, parentId?: any) { |
|
|
folderDialog.value = { |
|
|
folderDialog.value = { |
|
|
visible: true, |
|
|
visible: true, |
|
|
@ -573,13 +628,12 @@ loadData() |
|
|
.selected-count { font-size: 12px; font-weight: 600; color: var(--td-text-color-primary); } |
|
|
.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-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 { |
|
|
.doc-tree-panel { |
|
|
width: 240px; |
|
|
|
|
|
flex-shrink: 0; |
|
|
flex-shrink: 0; |
|
|
display: flex; |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
flex-direction: column; |
|
|
@ -587,18 +641,29 @@ loadData() |
|
|
padding-right: 12px; |
|
|
padding-right: 12px; |
|
|
border-right: 1px solid var(--color-border); |
|
|
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-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; } |
|
|
.tree-panel-title { font-size: 13px; font-weight: 600; } |
|
|
.tree-panel-title { font-size: 13px; font-weight: 600; } |
|
|
.tree-loading { padding: 16px; font-size: 13px; color: var(--td-text-color-placeholder); } |
|
|
.tree-loading { padding: 16px; font-size: 13px; color: var(--td-text-color-placeholder); } |
|
|
|
|
|
|
|
|
/* 树节点 hover 操作按钮 */ |
|
|
/* 树节点 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; } |
|
|
.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; } |
|
|
.move-tree-tip { font-size: 12px; color: var(--td-text-color-placeholder); margin-bottom: 8px; } |
|
|
|