|
|
@@ -1,18 +1,12 @@
|
|
|
<template>
|
|
|
<div class="app-container tree-sidebar-manage-wrap">
|
|
|
- <el-page-header @back="goBack" style="margin-bottom: 12px; flex-shrink: 0;">
|
|
|
- <template #content>
|
|
|
- <span class="page-header-title">{{ pageTitle }}</span>
|
|
|
- </template>
|
|
|
- </el-page-header>
|
|
|
-
|
|
|
<div class="portal-detail-wrap">
|
|
|
<div class="folder-sidebar">
|
|
|
<div class="sidebar-header">
|
|
|
<span>文件夹</span>
|
|
|
<el-button link type="primary" icon="Refresh" @click="getFolderTree" />
|
|
|
</div>
|
|
|
- <el-input v-model="folderSearch" placeholder="搜索文件夹" clearable size="small" style="margin-bottom: 8px; flex-shrink: 0;" />
|
|
|
+ <el-input v-model="folderSearch" placeholder="搜索文件夹" clearable style="margin-bottom: 8px; flex-shrink: 0;" />
|
|
|
<el-tree
|
|
|
ref="folderTreeRef"
|
|
|
:data="folderTree"
|
|
|
@@ -22,7 +16,17 @@
|
|
|
:default-expanded-keys="expandedKeys"
|
|
|
highlight-current
|
|
|
@node-click="handleNodeClick"
|
|
|
- />
|
|
|
+ >
|
|
|
+ <template #default="{ data }">
|
|
|
+ <div class="tree-node-row">
|
|
|
+ <span class="tree-node-label">{{ data.name }}</span>
|
|
|
+ <div class="tree-node-actions">
|
|
|
+ <el-button v-if="data.pid" link type="primary" icon="Plus" class="tree-node-add" title="在此新建子目录" @click.stop="handleAddChild(data)" />
|
|
|
+ <el-button v-if="data.pid" link type="danger" icon="Delete" class="tree-node-del" title="删除该目录" @click.stop="handleDeleteFolder(data)" />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-tree>
|
|
|
</div>
|
|
|
|
|
|
<div class="file-content">
|
|
|
@@ -152,7 +156,7 @@
|
|
|
</template>
|
|
|
|
|
|
<script setup name="PortalDetail">
|
|
|
-import { folderList, getFileList, downloadFile, moveFile, mkdir, deleteFile, getPlanValue, updatePlanValue } from "@/api/efinder/finder"
|
|
|
+import { folderList, getFileList, downloadFile, moveFile, mkdir, deleteFile, deleteFolderRecursive, getPlanValue, updatePlanValue } from "@/api/efinder/finder"
|
|
|
import { getToken } from "@/utils/auth"
|
|
|
import { parseTime } from "@/utils/goldenwater"
|
|
|
|
|
|
@@ -173,9 +177,9 @@ const fdCate = computed(() => {
|
|
|
const folderId = ref(Number(route.params.folderId))
|
|
|
|
|
|
const titleMap = { zggc: '直管工程', zdgc: '重点工程', gzzd: '规章制度', ddjc: '监督检查', wyh: '望亭闸', tpz: '图片展' }
|
|
|
-// 页面标题优先显示当前项目(文件夹)名称,实现点击项目跳转到对应模块的感知
|
|
|
-const currentFolderName = ref('')
|
|
|
-const pageTitle = computed(() => currentFolderName.value || titleMap[fdCate.value] || '详情')
|
|
|
+// 页面标题始终显示当前工程名称
|
|
|
+const projectName = ref('')
|
|
|
+const pageTitle = computed(() => projectName.value || titleMap[fdCate.value] || '详情')
|
|
|
const showPlanValue = computed(() => fdCate.value === 'zggc' || fdCate.value === 'zdgc')
|
|
|
|
|
|
const folderTree = ref([])
|
|
|
@@ -194,6 +198,8 @@ const dialogOpen = ref(false)
|
|
|
const dialogTitle = ref('')
|
|
|
const dialogForm = ref({})
|
|
|
const dialogRules = { name: [{ required: true, message: '名称不能为空', trigger: 'blur' }] }
|
|
|
+const mkdirParentId = ref(null)
|
|
|
+const currentProjectRoot = ref(null)
|
|
|
|
|
|
const uploadOpen = ref(false)
|
|
|
const uploadRef = ref(null)
|
|
|
@@ -221,13 +227,45 @@ function filterNode(value, data) {
|
|
|
return data.name?.includes(value)
|
|
|
}
|
|
|
|
|
|
-function goBack() {
|
|
|
- const backMap = { zggc: '/zggc/index', zdgc: '/zdgc/index', gzzd: '/gzzd/index', ddjc: '/ddjc/index', wyh: '/more/wyh', tpz: '/more/tpz' }
|
|
|
- router.push(backMap[fdCate.value] || '/index')
|
|
|
-}
|
|
|
-
|
|
|
// 树数据缓存:同一模块(fdCate)的文件夹树只拉取一次,切换项目时复用,减少重复请求
|
|
|
const treeCache = new Map()
|
|
|
+const folderFlatList = ref([])
|
|
|
+
|
|
|
+// 找到当前节点所属的顶层工程根节点
|
|
|
+function findProjectRoot(list, folderId) {
|
|
|
+ let current = list.find(f => f.id === folderId)
|
|
|
+ while (current && current.pid) {
|
|
|
+ current = list.find(f => f.id === current.pid)
|
|
|
+ }
|
|
|
+ return current ? current.id : folderId
|
|
|
+}
|
|
|
+
|
|
|
+// 过滤出根节点及其所有子孙
|
|
|
+function filterDescendants(list, rootId) {
|
|
|
+ const ids = new Set([rootId])
|
|
|
+ let changed = true
|
|
|
+ while (changed) {
|
|
|
+ changed = false
|
|
|
+ for (const f of list) {
|
|
|
+ if (ids.has(f.pid) && !ids.has(f.id)) {
|
|
|
+ ids.add(f.id)
|
|
|
+ changed = true
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return list.filter(f => ids.has(f.id))
|
|
|
+}
|
|
|
+
|
|
|
+// 展开指定节点及其祖先链,保证其下级目录立即可见
|
|
|
+function expandNodeChain(id) {
|
|
|
+ if (!folderTreeRef.value || !id) return
|
|
|
+ const expandIds = getAncestorKeys(folderFlatList.value, id)
|
|
|
+ expandIds.unshift(id)
|
|
|
+ expandIds.forEach(kid => {
|
|
|
+ const node = folderTreeRef.value.getNode(kid)
|
|
|
+ if (node) node.expanded = true
|
|
|
+ })
|
|
|
+}
|
|
|
|
|
|
function getFolderTree() {
|
|
|
const cached = treeCache.get(fdCate.value)
|
|
|
@@ -239,18 +277,29 @@ function getFolderTree() {
|
|
|
return list
|
|
|
})
|
|
|
return load.then(list => {
|
|
|
- // 根据当前选中文件夹更新标题,体现二级模块跳转
|
|
|
- const current = list.find(f => f.id === currentPid.value)
|
|
|
- if (current) {
|
|
|
- currentFolderName.value = current.name
|
|
|
+ // 找到当前工程的根节点,只显示该工程的子目录
|
|
|
+ const rootId = findProjectRoot(list, currentPid.value)
|
|
|
+ currentProjectRoot.value = rootId
|
|
|
+ // 工程名称为根节点名称
|
|
|
+ const root = list.find(f => f.id === rootId)
|
|
|
+ if (root) {
|
|
|
+ projectName.value = root.name
|
|
|
}
|
|
|
- const treeData = proxy.handleTree(list, 'id', 'pid')
|
|
|
+ const projectList = filterDescendants(list, rootId)
|
|
|
+ // 去掉根节点本身,树只显示其子目录
|
|
|
+ const treeList = projectList.filter(f => f.id !== rootId)
|
|
|
+ folderFlatList.value = projectList
|
|
|
+
|
|
|
+ // 只构建当前工程的树(不含根节点)
|
|
|
+ const treeData = proxy.handleTree(treeList, 'id', 'pid')
|
|
|
folderTree.value = treeData
|
|
|
- // 计算当前项目的祖先链,树只展开该链,聚焦到当前项目及其二级模块
|
|
|
- expandedKeys.value = getAncestorKeys(list, currentPid.value)
|
|
|
+
|
|
|
+ expandedKeys.value = getAncestorKeys(treeList, currentPid.value)
|
|
|
nextTick(() => {
|
|
|
if (folderTreeRef.value && currentPid.value) {
|
|
|
folderTreeRef.value.setCurrentKey(currentPid.value)
|
|
|
+ // 展开当前节点及其祖先链,保证其下级目录立即可见(如三级目录)
|
|
|
+ expandNodeChain(currentPid.value)
|
|
|
}
|
|
|
})
|
|
|
})
|
|
|
@@ -294,7 +343,6 @@ function loadPlanValue(id) {
|
|
|
}
|
|
|
|
|
|
function handleNodeClick(node) {
|
|
|
- currentFolderName.value = node.name
|
|
|
// 更新 URL,实现二级模块的真实跳转(可刷新/可回退),由 watcher 统一加载
|
|
|
if (Number(route.params.folderId) !== node.id) {
|
|
|
router.replace({ path: '/' + fdCate.value + '/detail/' + node.id })
|
|
|
@@ -306,9 +354,24 @@ function handleNodeClick(node) {
|
|
|
// 监听 URL 参数变化(浏览器前进/后退或外部跳转)
|
|
|
watch(() => route.params.folderId, (val) => {
|
|
|
if (val) {
|
|
|
- currentPid.value = Number(val)
|
|
|
+ const newPid = Number(val)
|
|
|
+ currentPid.value = newPid
|
|
|
queryParams.value.pageNum = 1
|
|
|
- Promise.all([getFolderTree(), fetchFileList()])
|
|
|
+
|
|
|
+ // 判断项目根节点是否变化,变化才重建树,否则只更新选中状态
|
|
|
+ const newRoot = findProjectRoot(folderFlatList.value, newPid)
|
|
|
+ if (newRoot !== currentProjectRoot.value) {
|
|
|
+ currentProjectRoot.value = newRoot
|
|
|
+ getFolderTree()
|
|
|
+ } else {
|
|
|
+ nextTick(() => {
|
|
|
+ if (folderTreeRef.value) {
|
|
|
+ folderTreeRef.value.setCurrentKey(newPid)
|
|
|
+ expandNodeChain(newPid)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+ fetchFileList()
|
|
|
}
|
|
|
})
|
|
|
|
|
|
@@ -367,25 +430,86 @@ function uploadError() {
|
|
|
proxy.$modal.msgError('上传失败')
|
|
|
}
|
|
|
|
|
|
+function openMkdirDialog(pid) {
|
|
|
+ mkdirParentId.value = pid
|
|
|
+ dialogForm.value = { name: '' }
|
|
|
+ dialogTitle.value = '新建文件夹'
|
|
|
+ dialogOpen.value = true
|
|
|
+}
|
|
|
+
|
|
|
+// 删除树节点目录(递归删除其下所有子目录和文件)
|
|
|
+async function handleDeleteFolder(data) {
|
|
|
+ // 检查当前目录下是否有子目录或文件
|
|
|
+ const hasChildren = folderFlatList.value.some(f => f.pid === data.id)
|
|
|
+ let hasFiles = false
|
|
|
+ try {
|
|
|
+ const res = await getFileList({ pid: data.id, pageSize: 1 })
|
|
|
+ hasFiles = (res.total || 0) > 0
|
|
|
+ } catch (e) {}
|
|
|
+
|
|
|
+ let msg = '是否确认删除目录"' + data.name + '"?'
|
|
|
+ if (hasChildren && hasFiles) {
|
|
|
+ msg += '当前目录下还有子目录以及文件,'
|
|
|
+ } else if (hasChildren) {
|
|
|
+ msg += '当前目录下还有子目录,'
|
|
|
+ } else if (hasFiles) {
|
|
|
+ msg += '当前目录下还有文件,'
|
|
|
+ }
|
|
|
+ msg += '删除后不可恢复。'
|
|
|
+
|
|
|
+ proxy.$modal.confirm(msg).then(() => {
|
|
|
+ return deleteFolderRecursive(data.id)
|
|
|
+ }).then(() => {
|
|
|
+ proxy.$modal.msgSuccess('删除成功')
|
|
|
+ treeCache.delete(fdCate.value)
|
|
|
+ getFolderTree()
|
|
|
+ // 若删除的是当前所在目录,回到工程根目录
|
|
|
+ if (currentPid.value === data.id) {
|
|
|
+ router.replace({ path: '/' + fdCate.value + '/detail/' + currentProjectRoot.value })
|
|
|
+ }
|
|
|
+ }).catch(() => {})
|
|
|
+}
|
|
|
+
|
|
|
function handleMkdir() {
|
|
|
if (!currentPid.value) {
|
|
|
proxy.$modal.msgWarning('请先选择文件夹')
|
|
|
return
|
|
|
}
|
|
|
- dialogForm.value = { name: '' }
|
|
|
- dialogTitle.value = '新建文件夹'
|
|
|
- dialogOpen.value = true
|
|
|
+ // 禁止在工程根目录下直接创建,只能在七个默认目录下创建
|
|
|
+ const current = folderFlatList.value.find(f => f.id === currentPid.value)
|
|
|
+ if (current && !current.pid) {
|
|
|
+ proxy.$modal.msgWarning('请在左侧目录树中选择一个分类后再创建')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ openMkdirDialog(currentPid.value)
|
|
|
+}
|
|
|
+
|
|
|
+// 树节点"+":在该节点下新建子目录
|
|
|
+function handleAddChild(data) {
|
|
|
+ openMkdirDialog(data.id)
|
|
|
+ nextTick(() => {
|
|
|
+ if (folderTreeRef.value) {
|
|
|
+ folderTreeRef.value.setCurrentKey(data.id)
|
|
|
+ expandNodeChain(data.id)
|
|
|
+ }
|
|
|
+ })
|
|
|
}
|
|
|
|
|
|
function submitDialog() {
|
|
|
proxy.$refs.formRef.validate(valid => {
|
|
|
if (valid) {
|
|
|
- mkdir({ name: dialogForm.value.name, pid: currentPid.value, fdCate: fdCate.value }).then(() => {
|
|
|
+ const pid = mkdirParentId.value
|
|
|
+ mkdir({ name: dialogForm.value.name, pid, fdCate: fdCate.value }).then(() => {
|
|
|
proxy.$modal.msgSuccess('新建成功')
|
|
|
dialogOpen.value = false
|
|
|
// 新建文件夹后清除树缓存,强制刷新
|
|
|
treeCache.delete(fdCate.value)
|
|
|
- getFolderTree()
|
|
|
+ getFolderTree().then(() => {
|
|
|
+ // 展开新建目录的父级,确保新目录立即可见
|
|
|
+ nextTick(() => {
|
|
|
+ if (pid) expandNodeChain(pid)
|
|
|
+ })
|
|
|
+ })
|
|
|
})
|
|
|
}
|
|
|
})
|
|
|
@@ -504,9 +628,11 @@ onMounted(() => {
|
|
|
flex-direction: column;
|
|
|
height: 100%;
|
|
|
}
|
|
|
-.page-header-title {
|
|
|
+.content-header h3 {
|
|
|
+ margin: 0 0 12px;
|
|
|
font-size: 18px;
|
|
|
font-weight: 600;
|
|
|
+ flex-shrink: 0;
|
|
|
color: #303133;
|
|
|
}
|
|
|
.portal-detail-wrap {
|
|
|
@@ -542,6 +668,42 @@ onMounted(() => {
|
|
|
padding-bottom: 8px;
|
|
|
border-bottom: 1px solid #f0f2f6;
|
|
|
}
|
|
|
+.tree-node-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ flex: 1;
|
|
|
+ min-width: 0;
|
|
|
+ padding-right: 4px;
|
|
|
+}
|
|
|
+.tree-node-label {
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+}
|
|
|
+.tree-node-actions {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ flex-shrink: 0;
|
|
|
+ margin-left: 6px;
|
|
|
+}
|
|
|
+.tree-node-add {
|
|
|
+ padding: 0;
|
|
|
+ font-size: 13px;
|
|
|
+ opacity: 0.7;
|
|
|
+}
|
|
|
+.tree-node-add:hover {
|
|
|
+ opacity: 1;
|
|
|
+}
|
|
|
+.tree-node-del {
|
|
|
+ padding: 0;
|
|
|
+ font-size: 13px;
|
|
|
+ margin-left: 4px;
|
|
|
+ opacity: 0.7;
|
|
|
+}
|
|
|
+.tree-node-del:hover {
|
|
|
+ opacity: 1;
|
|
|
+}
|
|
|
.file-content {
|
|
|
flex: 1;
|
|
|
min-width: 0;
|
|
|
@@ -554,13 +716,6 @@ onMounted(() => {
|
|
|
border-radius: 10px;
|
|
|
padding: 16px;
|
|
|
}
|
|
|
-.content-header h3 {
|
|
|
- margin: 0 0 12px;
|
|
|
- font-size: 18px;
|
|
|
- font-weight: 600;
|
|
|
- flex-shrink: 0;
|
|
|
- color: #303133;
|
|
|
-}
|
|
|
.search-form {
|
|
|
flex-shrink: 0;
|
|
|
}
|