77681 5 ساعت پیش
والد
کامیت
4b290c656e

+ 63 - 16
gw-slgc/src/main/java/com/goldenwater/web/controller/pms/PmsAddProjectController.java

@@ -62,28 +62,17 @@ public class PmsAddProjectController extends BaseController {
 
 
             // 根据 zggcPrarms 创建子文件夹
             // 根据 zggcPrarms 创建子文件夹
             if (StringUtils.isNotBlank(zggcPrarms)) {
             if (StringUtils.isNotBlank(zggcPrarms)) {
-                String[] subFolderNames;
+                String fdCate = rootFolder.getFdCate();
                 switch (zggcPrarms) {
                 switch (zggcPrarms) {
-                    case "1": // 直管工程
-                        subFolderNames = new String[]{"工程概况", "程序管理", "质量管理", "安全管理", "验收管理", "监督检查", "多媒体资料管理"};
+                    case "1": // 直管工程 — 多级目录结构
+                        createNestedStructure(rootFolder.getId(), fdCate, "1");
                         break;
                         break;
                     case "2": // 重点工程
                     case "2": // 重点工程
-                        subFolderNames = new String[]{"工程概况", "监督检查", "工程验收"};
+                        createFlatStructure(rootFolder.getId(), fdCate, "工程概况", "监督检查", "工程验收");
                         break;
                         break;
                     case "3": // 规章制度
                     case "3": // 规章制度
-                        subFolderNames = new String[]{"制度概况"};
+                        createFlatStructure(rootFolder.getId(), fdCate, "制度概况");
                         break;
                         break;
-                    default:
-                        subFolderNames = new String[0];
-                }
-                for (String subName : subFolderNames) {
-                    DocFolder subFolder = new DocFolder();
-                    subFolder.setPid(rootFolder.getId());
-                    subFolder.setName(subName);
-                    subFolder.setDate(new Date());
-                    // 子目录继承根目录分类,保证详情页文件夹树能按 fdCate 查询到
-                    subFolder.setFdCate(rootFolder.getFdCate());
-                    docFolderService.insert(subFolder);
                 }
                 }
             }
             }
 
 
@@ -108,4 +97,62 @@ public class PmsAddProjectController extends BaseController {
         List<Map<String, Object>> list = userProjectService.getUserProject();
         List<Map<String, Object>> list = userProjectService.getUserProject();
         return success(list);
         return success(list);
     }
     }
+
+    /** 批量创建扁平目录(单级) */
+    private void createFlatStructure(Long rootPid, String fdCate, String... names) {
+        for (String name : names) {
+            DocFolder folder = new DocFolder();
+            folder.setPid(rootPid);
+            folder.setName(name);
+            folder.setDate(new Date());
+            folder.setFdCate(fdCate);
+            docFolderService.insert(folder);
+        }
+    }
+
+    /** 创建多级目录结构 */
+    private void createNestedStructure(Long rootPid, String fdCate, String type) {
+        if ("1".equals(type)) {
+            // 一、工程概况
+            DocFolder f1 = insertFolder(rootPid, "一、工程概况", fdCate);
+            insertFolder(f1.getId(), "项目简介", fdCate);
+            insertFolder(f1.getId(), "初设及批复文件", fdCate);
+
+            // 二、建设管理
+            DocFolder f2 = insertFolder(rootPid, "二、建设管理", fdCate);
+            insertFolder(f2.getId(), "机构组建", fdCate);
+            insertFolder(f2.getId(), "招标投标", fdCate);
+            insertFolder(f2.getId(), "开工备案", fdCate);
+            insertFolder(f2.getId(), "设计变更", fdCate);
+            insertFolder(f2.getId(), "预备费动用", fdCate);
+
+            // 三、质量管理
+            DocFolder f3 = insertFolder(rootPid, "三、质量管理", fdCate);
+            insertFolder(f3.getId(), "质量监督", fdCate);
+            insertFolder(f3.getId(), "质量验收资料", fdCate);
+
+            // 四、监督检查
+            insertFolder(rootPid, "四、监督检查", fdCate);
+
+            // 五、验收管理
+            insertFolder(rootPid, "五、验收管理", fdCate);
+
+            // 六、安全度汛
+            insertFolder(rootPid, "六、安全度汛", fdCate);
+
+            // 七、多媒体资料
+            insertFolder(rootPid, "七、多媒体资料", fdCate);
+        }
+    }
+
+    /** 插入单个文件夹并返回实体(含回填的id) */
+    private DocFolder insertFolder(Long pid, String name, String fdCate) {
+        DocFolder folder = new DocFolder();
+        folder.setPid(pid);
+        folder.setName(name);
+        folder.setDate(new Date());
+        folder.setFdCate(fdCate);
+        docFolderService.insert(folder);
+        return folder;
+    }
 }
 }

+ 1 - 1
gw-ui/src/layout/components/AppMain.vue

@@ -6,7 +6,7 @@
           <component
           <component
             v-if="!route.meta.link"
             v-if="!route.meta.link"
             :is="Component"
             :is="Component"
-            :key="route.path"
+            :key="route.name"
           />
           />
         </keep-alive>
         </keep-alive>
       </transition>
       </transition>

+ 195 - 40
gw-ui/src/views/portal/portalDetail.vue

@@ -1,18 +1,12 @@
 <template>
 <template>
   <div class="app-container tree-sidebar-manage-wrap">
   <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="portal-detail-wrap">
       <div class="folder-sidebar">
       <div class="folder-sidebar">
         <div class="sidebar-header">
         <div class="sidebar-header">
           <span>文件夹</span>
           <span>文件夹</span>
           <el-button link type="primary" icon="Refresh" @click="getFolderTree" />
           <el-button link type="primary" icon="Refresh" @click="getFolderTree" />
         </div>
         </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
         <el-tree
           ref="folderTreeRef"
           ref="folderTreeRef"
           :data="folderTree"
           :data="folderTree"
@@ -22,7 +16,17 @@
           :default-expanded-keys="expandedKeys"
           :default-expanded-keys="expandedKeys"
           highlight-current
           highlight-current
           @node-click="handleNodeClick"
           @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>
 
 
       <div class="file-content">
       <div class="file-content">
@@ -152,7 +156,7 @@
 </template>
 </template>
 
 
 <script setup name="PortalDetail">
 <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 { getToken } from "@/utils/auth"
 import { parseTime } from "@/utils/goldenwater"
 import { parseTime } from "@/utils/goldenwater"
 
 
@@ -173,9 +177,9 @@ const fdCate = computed(() => {
 const folderId = ref(Number(route.params.folderId))
 const folderId = ref(Number(route.params.folderId))
 
 
 const titleMap = { zggc: '直管工程', zdgc: '重点工程', gzzd: '规章制度', ddjc: '监督检查', wyh: '望亭闸', tpz: '图片展' }
 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 showPlanValue = computed(() => fdCate.value === 'zggc' || fdCate.value === 'zdgc')
 
 
 const folderTree = ref([])
 const folderTree = ref([])
@@ -194,6 +198,8 @@ const dialogOpen = ref(false)
 const dialogTitle = ref('')
 const dialogTitle = ref('')
 const dialogForm = ref({})
 const dialogForm = ref({})
 const dialogRules = { name: [{ required: true, message: '名称不能为空', trigger: 'blur' }] }
 const dialogRules = { name: [{ required: true, message: '名称不能为空', trigger: 'blur' }] }
+const mkdirParentId = ref(null)
+const currentProjectRoot = ref(null)
 
 
 const uploadOpen = ref(false)
 const uploadOpen = ref(false)
 const uploadRef = ref(null)
 const uploadRef = ref(null)
@@ -221,13 +227,45 @@ function filterNode(value, data) {
   return data.name?.includes(value)
   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)的文件夹树只拉取一次,切换项目时复用,减少重复请求
 // 树数据缓存:同一模块(fdCate)的文件夹树只拉取一次,切换项目时复用,减少重复请求
 const treeCache = new Map()
 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() {
 function getFolderTree() {
   const cached = treeCache.get(fdCate.value)
   const cached = treeCache.get(fdCate.value)
@@ -239,18 +277,29 @@ function getFolderTree() {
         return list
         return list
       })
       })
   return load.then(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
     folderTree.value = treeData
-    // 计算当前项目的祖先链,树只展开该链,聚焦到当前项目及其二级模块
-    expandedKeys.value = getAncestorKeys(list, currentPid.value)
+
+    expandedKeys.value = getAncestorKeys(treeList, currentPid.value)
     nextTick(() => {
     nextTick(() => {
       if (folderTreeRef.value && currentPid.value) {
       if (folderTreeRef.value && currentPid.value) {
         folderTreeRef.value.setCurrentKey(currentPid.value)
         folderTreeRef.value.setCurrentKey(currentPid.value)
+        // 展开当前节点及其祖先链,保证其下级目录立即可见(如三级目录)
+        expandNodeChain(currentPid.value)
       }
       }
     })
     })
   })
   })
@@ -294,7 +343,6 @@ function loadPlanValue(id) {
 }
 }
 
 
 function handleNodeClick(node) {
 function handleNodeClick(node) {
-  currentFolderName.value = node.name
   // 更新 URL,实现二级模块的真实跳转(可刷新/可回退),由 watcher 统一加载
   // 更新 URL,实现二级模块的真实跳转(可刷新/可回退),由 watcher 统一加载
   if (Number(route.params.folderId) !== node.id) {
   if (Number(route.params.folderId) !== node.id) {
     router.replace({ path: '/' + fdCate.value + '/detail/' + node.id })
     router.replace({ path: '/' + fdCate.value + '/detail/' + node.id })
@@ -306,9 +354,24 @@ function handleNodeClick(node) {
 // 监听 URL 参数变化(浏览器前进/后退或外部跳转)
 // 监听 URL 参数变化(浏览器前进/后退或外部跳转)
 watch(() => route.params.folderId, (val) => {
 watch(() => route.params.folderId, (val) => {
   if (val) {
   if (val) {
-    currentPid.value = Number(val)
+    const newPid = Number(val)
+    currentPid.value = newPid
     queryParams.value.pageNum = 1
     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('上传失败')
   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() {
 function handleMkdir() {
   if (!currentPid.value) {
   if (!currentPid.value) {
     proxy.$modal.msgWarning('请先选择文件夹')
     proxy.$modal.msgWarning('请先选择文件夹')
     return
     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() {
 function submitDialog() {
   proxy.$refs.formRef.validate(valid => {
   proxy.$refs.formRef.validate(valid => {
     if (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('新建成功')
         proxy.$modal.msgSuccess('新建成功')
         dialogOpen.value = false
         dialogOpen.value = false
         // 新建文件夹后清除树缓存,强制刷新
         // 新建文件夹后清除树缓存,强制刷新
         treeCache.delete(fdCate.value)
         treeCache.delete(fdCate.value)
-        getFolderTree()
+        getFolderTree().then(() => {
+          // 展开新建目录的父级,确保新目录立即可见
+          nextTick(() => {
+            if (pid) expandNodeChain(pid)
+          })
+        })
       })
       })
     }
     }
   })
   })
@@ -504,9 +628,11 @@ onMounted(() => {
   flex-direction: column;
   flex-direction: column;
   height: 100%;
   height: 100%;
 }
 }
-.page-header-title {
+.content-header h3 {
+  margin: 0 0 12px;
   font-size: 18px;
   font-size: 18px;
   font-weight: 600;
   font-weight: 600;
+  flex-shrink: 0;
   color: #303133;
   color: #303133;
 }
 }
 .portal-detail-wrap {
 .portal-detail-wrap {
@@ -542,6 +668,42 @@ onMounted(() => {
   padding-bottom: 8px;
   padding-bottom: 8px;
   border-bottom: 1px solid #f0f2f6;
   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 {
 .file-content {
   flex: 1;
   flex: 1;
   min-width: 0;
   min-width: 0;
@@ -554,13 +716,6 @@ onMounted(() => {
   border-radius: 10px;
   border-radius: 10px;
   padding: 16px;
   padding: 16px;
 }
 }
-.content-header h3 {
-  margin: 0 0 12px;
-  font-size: 18px;
-  font-weight: 600;
-  flex-shrink: 0;
-  color: #303133;
-}
 .search-form {
 .search-form {
   flex-shrink: 0;
   flex-shrink: 0;
 }
 }