77681 16 órája
szülő
commit
6240afcd94

+ 2 - 2
gw-ui/src/assets/styles/index.scss

@@ -122,9 +122,9 @@ aside {
 
 //main-container全局样式
 .app-container {
-  padding: 20px;
+  padding: 16px;
   height: 100%;
-  overflow: hidden;
+  overflow: auto;
 }
 
 .components-container {

+ 47 - 25
gw-ui/src/layout/components/TopNavbar/index.vue

@@ -164,53 +164,73 @@ export default {
 
 <style scoped>
 .top-navbar {
-  height: 60px;
-  background: #217ff4;
+  height: 56px;
+  background: linear-gradient(135deg, #1a6be0, #217ff4);
   display: flex;
   align-items: center;
   justify-content: space-between;
-  padding: 0 30px;
-  box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
+  padding: 0 24px;
+  box-shadow: 0 2px 8px rgba(33, 127, 244, 0.15);
+  user-select: none;
 }
 
 .logo-area {
-  width: 260px;
+  display: flex;
+  align-items: center;
   flex-shrink: 0;
 }
 
 .logo-text {
   color: #fff;
-  font-size: 20px;
-  font-weight: bold;
+  font-size: 18px;
+  font-weight: 600;
+  letter-spacing: 2px;
+  white-space: nowrap;
 }
 
 .menu-area {
   flex: 1;
   display: flex;
-  gap: 8px;
-  margin: 0 30px;
+  gap: 2px;
+  margin: 0 16px;
   overflow-x: auto;
+  justify-content: flex-start;
 }
 
 .menu-link {
-  padding: 0 24px;
-  height: 60px;
-  line-height: 60px;
-  color: #fff;
+  padding: 0 20px;
+  height: 56px;
+  line-height: 56px;
+  color: rgba(255, 255, 255, 0.85);
   cursor: pointer;
-  font-size: 18px;
+  font-size: 15px;
   font-weight: 500;
-  letter-spacing: 1px;
   white-space: nowrap;
   flex-shrink: 0;
+  transition: all 0.2s;
+  position: relative;
 }
 
 .menu-link:hover {
-  background: rgba(255, 255, 255, 0.15);
+  color: #fff;
+  background: rgba(255, 255, 255, 0.12);
 }
 
 .menu-link.active {
-  background: rgba(255, 255, 255, 0.25);
+  color: #fff;
+  background: rgba(255, 255, 255, 0.18);
+}
+
+.menu-link.active::after {
+  content: '';
+  position: absolute;
+  bottom: 0;
+  left: 50%;
+  transform: translateX(-50%);
+  width: 60%;
+  height: 3px;
+  background: #fff;
+  border-radius: 3px 3px 0 0;
 }
 
 .right-menu {
@@ -229,20 +249,22 @@ export default {
 .avatar-wrapper {
   display: flex;
   align-items: center;
-  gap: 8px;
+  gap: 6px;
   cursor: pointer;
-  padding: 0 12px;
-  height: 60px;
+  padding: 0 10px;
+  height: 56px;
   max-width: 200px;
+  transition: background 0.2s;
+  border-radius: 4px;
 }
 
 .avatar-wrapper:hover {
-  background: rgba(255, 255, 255, 0.15);
+  background: rgba(255, 255, 255, 0.12);
 }
 
 .user-icon {
-  width: 18px;
-  height: 18px;
+  width: 16px;
+  height: 16px;
   color: #fff;
   flex-shrink: 0;
 }
@@ -258,8 +280,8 @@ export default {
 }
 
 .dropdown-icon {
-  font-size: 14px;
-  color: #fff;
+  font-size: 12px;
+  color: rgba(255, 255, 255, 0.75);
   transition: transform 0.3s;
   flex-shrink: 0;
 }

+ 8 - 4
gw-ui/src/layout/index.vue

@@ -18,13 +18,13 @@
 
     <!-- 下方区域:左右结构 -->
     <div class="main-layout">
-      <!-- 左侧二三级菜单 -->
-      <div class="sidebar-wrapper" :class="{ collapsed: isCollapse }">
+      <!-- 左侧二三级菜单(通过路由 meta.sidebar 控制显隐) -->
+      <div v-if="showSidebar" class="sidebar-wrapper" :class="{ collapsed: isCollapse }">
         <sidebar v-if="!sidebar.hide" :top-menu-key="activeTopMenu" />
       </div>
 
       <!-- 右侧内容区域 -->
-      <div class="content-wrapper">
+      <div class="content-wrapper" :class="{ 'content-full': !showSidebar }">
         <app-main />
         <settings ref="settingRef" />
       </div>
@@ -55,6 +55,7 @@ const HOME_PATH = "/index";
 const theme = computed(() => settingsStore.theme);
 const sidebar = computed(() => appStore.sidebar);
 const isCollapse = computed(() => appStore.sidebar.isCollapse);
+const showSidebar = computed(() => route.meta?.sidebar !== false);
 
 // 当前选中的顶级菜单
 const activeTopMenu = ref("");
@@ -165,7 +166,10 @@ onMounted(() => {
 
 .content-wrapper {
   flex: 1;
-  overflow: auto;
+  overflow: hidden;
   background: #f0f2f6;
+  padding: 0 80px;
+  display: flex;
+  flex-direction: column;
 }
 </style>

+ 11 - 11
gw-ui/src/router/index.js

@@ -38,7 +38,7 @@ export const constantRoutes = [
         path: '',
         component: () => import('@/views/index'),
         name: 'Index',
-        meta: { title: '首页', icon: 'dashboard', affix: true }
+        meta: { title: '首页', icon: 'dashboard', affix: true, sidebar: false }
       }
     ]
   },
@@ -86,14 +86,14 @@ export const businessRoutes = [
         path: 'index',
         component: () => import('@/views/portal/zggc'),
         name: 'Zggc',
-        meta: { title: '直管工程' }
+        meta: { title: '直管工程', sidebar: false }
       },
       {
         path: 'detail/:folderId',
         component: () => import('@/views/portal/portalDetail'),
         name: 'ZggcDetail',
         hidden: true,
-        meta: { title: '直管工程详情', activeMenu: '/zggc/index' }
+        meta: { title: '直管工程详情', activeMenu: '/zggc/index', sidebar: false }
       }
     ]
   },
@@ -108,14 +108,14 @@ export const businessRoutes = [
         path: 'index',
         component: () => import('@/views/portal/zdgc'),
         name: 'Zdgc',
-        meta: { title: '重点工程' }
+        meta: { title: '重点工程', sidebar: false }
       },
       {
         path: 'detail/:folderId',
         component: () => import('@/views/portal/portalDetail'),
         name: 'ZdgcDetail',
         hidden: true,
-        meta: { title: '重点工程详情', activeMenu: '/zdgc/index' }
+        meta: { title: '重点工程详情', activeMenu: '/zdgc/index', sidebar: false }
       }
     ]
   },
@@ -130,7 +130,7 @@ export const businessRoutes = [
         path: 'index',
         component: () => import('@/views/portal/ddjc'),
         name: 'Ddjc',
-        meta: { title: '督导检查' }
+        meta: { title: '督导检查', sidebar: false }
       }
     ]
   },
@@ -145,14 +145,14 @@ export const businessRoutes = [
         path: 'index',
         component: () => import('@/views/portal/gzzd'),
         name: 'Gzzd',
-        meta: { title: '规章制度' }
+        meta: { title: '规章制度', sidebar: false }
       },
       {
         path: 'detail/:folderId',
         component: () => import('@/views/portal/portalDetail'),
         name: 'GzzdDetail',
         hidden: true,
-        meta: { title: '规章制度详情', activeMenu: '/gzzd/index' }
+        meta: { title: '规章制度详情', activeMenu: '/gzzd/index', sidebar: false }
       }
     ]
   },
@@ -170,13 +170,13 @@ export const businessRoutes = [
         path: 'tpz',
         component: () => import('@/views/portal/tpz'),
         name: 'MoreTpz',
-        meta: { title: '太浦闸(图片展)' }
+        meta: { title: '太浦闸(图片展)', sidebar: false }
       },
       {
         path: 'wyh',
         component: () => import('@/views/portal/wyh'),
         name: 'MoreWyh',
-        meta: { title: '望亭闸' }
+        meta: { title: '望亭闸', sidebar: false }
       }
     ]
   },
@@ -191,7 +191,7 @@ export const businessRoutes = [
         path: 'viewer',
         component: () => import('@/views/efinder/viewer'),
         name: 'EfinderViewer',
-        meta: { title: '文件预览' }
+        meta: { title: '文件预览', sidebar: false }
       }
     ]
   }

+ 28 - 23
gw-ui/src/views/index.vue

@@ -8,12 +8,12 @@
       <el-col :span="24">
         <el-card class="home-card">
           <el-row :gutter="10">
-            <el-col :span="3" v-for="(item, index) in tpzItems" :key="item.id">
+            <el-col :xs="12" :sm="8" :md="6" :lg="4" :xl="3" v-for="(item, index) in tpzItems" :key="item.id">
               <div class="card-item" @click="goToPage('/more/tpz?folderId=' + item.id)">
                 {{ index + 1 }}){{ item.name }}
               </div>
             </el-col>
-            <el-col :span="3" v-if="loadingTpz && tpzItems.length === 0">
+            <el-col :xs="12" :sm="8" :md="6" :lg="4" :xl="3" v-if="loadingTpz && tpzItems.length === 0">
               <div class="empty-text">暂无数据</div>
             </el-col>
           </el-row>
@@ -29,12 +29,12 @@
       <el-col :span="24">
         <el-card class="home-card">
           <el-row :gutter="10">
-            <el-col :span="3" v-for="(item, index) in wyhItems" :key="item.id">
+            <el-col :xs="12" :sm="8" :md="6" :lg="4" :xl="3" v-for="(item, index) in wyhItems" :key="item.id">
               <div class="card-item" @click="goToPage('/more/wyh?folderId=' + item.id)">
                 {{ index + 1 }}){{ item.name }}
               </div>
             </el-col>
-            <el-col :span="3" v-if="loadingWyh && wyhItems.length === 0">
+            <el-col :xs="12" :sm="8" :md="6" :lg="4" :xl="3" v-if="loadingWyh && wyhItems.length === 0">
               <div class="empty-text">暂无数据</div>
             </el-col>
           </el-row>
@@ -50,12 +50,12 @@
       <el-col :span="24">
         <el-card class="home-card">
           <el-row :gutter="10">
-            <el-col :span="3" v-for="(item, index) in zggcList" :key="item.id">
+            <el-col :xs="12" :sm="8" :md="6" :lg="4" :xl="3" v-for="(item, index) in zggcList" :key="item.id">
               <div class="project-item" @click="goToPage('/zggc/detail/' + item.id)">
                 <div class="project-name">{{ item.name }}</div>
               </div>
             </el-col>
-            <el-col :span="3" v-if="loadingZggc && zggcList.length === 0">
+            <el-col :xs="12" :sm="8" :md="6" :lg="4" :xl="3" v-if="loadingZggc && zggcList.length === 0">
               <div class="empty-text">暂无数据</div>
             </el-col>
           </el-row>
@@ -71,12 +71,12 @@
       <el-col :span="24">
         <el-card class="home-card">
           <el-row :gutter="10">
-            <el-col :span="3" v-for="(item, index) in zdgcList" :key="item.id">
+            <el-col :xs="12" :sm="8" :md="6" :lg="4" :xl="3" v-for="(item, index) in zdgcList" :key="item.id">
               <div class="project-item" @click="goToPage('/zdgc/detail/' + item.id)">
                 <div class="project-name">{{ item.name }}</div>
               </div>
             </el-col>
-            <el-col :span="3" v-if="loadingZdgc && zdgcList.length === 0">
+            <el-col :xs="12" :sm="8" :md="6" :lg="4" :xl="3" v-if="loadingZdgc && zdgcList.length === 0">
               <div class="empty-text">暂无数据</div>
             </el-col>
           </el-row>
@@ -113,12 +113,12 @@
       <el-col :span="24">
         <el-card class="home-card">
           <el-row :gutter="10">
-            <el-col :span="3" v-for="(item, index) in gzzdList" :key="item.id">
+            <el-col :xs="12" :sm="8" :md="6" :lg="4" :xl="3" v-for="(item, index) in gzzdList" :key="item.id">
               <div class="card-item" @click="goToPage('/gzzd/detail/' + item.id)">
                 {{ item.name }}
               </div>
             </el-col>
-            <el-col :span="3" v-if="loadingGzzd && gzzdList.length === 0">
+            <el-col :xs="12" :sm="8" :md="6" :lg="4" :xl="3" v-if="loadingGzzd && gzzdList.length === 0">
               <div class="empty-text">暂无数据</div>
             </el-col>
           </el-row>
@@ -175,17 +175,18 @@ onMounted(() => {
 
 <style scoped>
 .home {
-  padding: 20px;
+  padding: 16px;
 }
 
 .section-title {
   font-size: 18px;
-  font-weight: bold;
+  font-weight: 600;
   color: #303133;
 }
 
 .home-card {
   margin-bottom: 20px;
+  border-radius: 8px;
 }
 
 .card-item {
@@ -193,37 +194,40 @@ onMounted(() => {
   line-height: 40px;
   text-align: center;
   border: 1px solid #ebeef5;
-  border-radius: 4px;
+  border-radius: 6px;
   cursor: pointer;
-  transition: all 0.3s;
-  font-size: 14px;
+  transition: all 0.25s;
+  font-size: 13px;
   color: #606266;
   white-space: nowrap;
   overflow: hidden;
   text-overflow: ellipsis;
+  margin: 4px 0;
 }
 
 .card-item:hover {
-  background-color: #f5f7fa;
+  background-color: #ecf5ff;
   border-color: #409eff;
   color: #409eff;
 }
 
 .project-item {
   height: 60px;
-  background-color: #79c48c;
-  border-radius: 4px;
+  background: linear-gradient(135deg, #67c23a, #85ce61);
+  border-radius: 6px;
   cursor: pointer;
-  transition: all 0.3s;
+  transition: all 0.25s;
   display: flex;
   align-items: center;
   justify-content: center;
   padding: 5px;
+  margin: 4px 0;
 }
 
 .project-item:hover {
-  background-color: #67a87a;
+  background: linear-gradient(135deg, #5daf34, #74bd52);
   transform: translateY(-2px);
+  box-shadow: 0 4px 12px rgba(103, 194, 58, 0.3);
 }
 
 .project-name {
@@ -241,18 +245,19 @@ onMounted(() => {
   line-height: 40px;
   text-align: center;
   border: 1px solid #ebeef5;
-  border-radius: 4px;
+  border-radius: 6px;
   cursor: pointer;
-  transition: all 0.3s;
+  transition: all 0.25s;
   font-size: 12px;
   color: #606266;
   white-space: nowrap;
   overflow: hidden;
   text-overflow: ellipsis;
+  margin: 4px 0;
 }
 
 .ddjc-item:hover {
-  background-color: #f5f7fa;
+  background-color: #ecf5ff;
   border-color: #409eff;
   color: #409eff;
 }

+ 23 - 2
gw-ui/src/views/portal/ddjc.vue

@@ -64,7 +64,7 @@
         </el-table-column>
       </el-table>
 
-      <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getFileList" />
+      <pagination :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getFileList" />
     </div>
 
     <el-dialog title="添加分类" v-model="addDialogVisible" width="400px" append-to-body>
@@ -286,15 +286,26 @@ onMounted(() => {
 <style lang="scss" scoped>
 .ddjc-wrap {
   display: flex;
+  flex-direction: row;
   gap: 12px;
+  flex: 1;
+  min-height: 0;
+  height: 100%;
 }
 .category-sidebar {
   width: 240px;
   flex-shrink: 0;
   background: #fff;
   padding: 12px;
-  border-radius: 4px;
+  border-radius: 6px;
   border: 1px solid #ebeef5;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+  .el-tree {
+    flex: 1;
+    overflow: auto;
+  }
 }
 .sidebar-header {
   display: flex;
@@ -303,16 +314,26 @@ onMounted(() => {
   font-size: 14px;
   font-weight: 600;
   margin-bottom: 8px;
+  flex-shrink: 0;
 }
 .file-content {
   flex: 1;
   min-width: 0;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+  .el-table {
+    flex: 1;
+    overflow: auto;
+  }
 }
 .content-header h3 {
   margin: 0 0 12px;
   font-size: 18px;
+  flex-shrink: 0;
 }
 .mb8 {
   margin-bottom: 8px;
+  flex-shrink: 0;
 }
 </style>

+ 23 - 2
gw-ui/src/views/portal/gzzd.vue

@@ -66,7 +66,7 @@
         </el-table-column>
       </el-table>
 
-      <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getDocList" />
+      <pagination :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getDocList" />
     </div>
 
     <el-dialog title="添加分类" v-model="addDialogVisible" width="400px" append-to-body>
@@ -295,15 +295,26 @@ onMounted(() => {
 <style lang="scss" scoped>
 .gzzd-wrap {
   display: flex;
+  flex-direction: row;
   gap: 12px;
+  flex: 1;
+  min-height: 0;
+  height: 100%;
 }
 .category-sidebar {
   width: 240px;
   flex-shrink: 0;
   background: #fff;
   padding: 12px;
-  border-radius: 4px;
+  border-radius: 6px;
   border: 1px solid #ebeef5;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+  .el-tree {
+    flex: 1;
+    overflow: auto;
+  }
 }
 .sidebar-header {
   display: flex;
@@ -312,14 +323,23 @@ onMounted(() => {
   font-size: 14px;
   font-weight: 600;
   margin-bottom: 8px;
+  flex-shrink: 0;
 }
 .file-content {
   flex: 1;
   min-width: 0;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+  .el-table {
+    flex: 1;
+    overflow: auto;
+  }
 }
 .content-header h3 {
   margin: 0 0 12px;
   font-size: 18px;
+  flex-shrink: 0;
 }
 .file-icon {
   margin-right: 4px;
@@ -328,5 +348,6 @@ onMounted(() => {
 }
 .mb8 {
   margin-bottom: 8px;
+  flex-shrink: 0;
 }
 </style>

+ 48 - 16
gw-ui/src/views/portal/portalDetail.vue

@@ -1,6 +1,6 @@
 <template>
   <div class="app-container tree-sidebar-manage-wrap">
-    <el-page-header @back="goBack" style="margin-bottom: 12px;">
+    <el-page-header @back="goBack" style="margin-bottom: 12px; flex-shrink: 0;">
       <template #content>
         <span class="page-header-title">{{ pageTitle }}</span>
       </template>
@@ -12,7 +12,7 @@
           <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;" />
+        <el-input v-model="folderSearch" placeholder="搜索文件夹" clearable size="small" style="margin-bottom: 8px; flex-shrink: 0;" />
         <el-tree
           ref="folderTreeRef"
           :data="folderTree"
@@ -26,7 +26,10 @@
       </div>
 
       <div class="file-content">
-        <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch">
+        <div class="content-header">
+          <h3>{{ pageTitle }}</h3>
+        </div>
+        <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" class="search-form">
           <el-form-item label="文件名称" prop="name">
             <el-input v-model="queryParams.name" placeholder="请输入文件名称" clearable style="width: 200px" @keyup.enter="handleQuery" />
           </el-form-item>
@@ -52,7 +55,7 @@
           <right-toolbar v-model:showSearch="showSearch" @queryTable="fetchFileList" />
         </el-row>
 
-        <el-table v-loading="loading" :data="fileList" @selection-change="handleSelectionChange">
+        <el-table v-loading="loading" :data="fileList" @selection-change="handleSelectionChange" class="detail-table">
           <el-table-column type="selection" width="50" align="center" />
           <el-table-column label="文件名称" prop="name" min-width="200" :show-overflow-tooltip="true">
             <template #default="scope">
@@ -81,7 +84,7 @@
           </el-table-column>
         </el-table>
 
-        <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="fetchFileList" />
+        <pagination :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="fetchFileList" />
       </div>
     </div>
 
@@ -222,20 +225,17 @@ function goBack() {
 }
 
 function getFolderTree() {
-  folderList({ fdCate: fdCate.value, pageSize: 999 }).then(response => {
+  return folderList({ fdCate: fdCate.value, pageSize: 999 }).then(response => {
     const list = response.rows || response.data || []
     const treeData = proxy.handleTree(list, 'id', 'pid')
     folderTree.value = treeData
-    if (!currentPid.value && folderId.value) {
-      currentPid.value = folderId.value
-      fetchFileList()
-    }
   })
 }
 
 function fetchFileList() {
+  if (!currentPid.value && !folderId.value) return
   loading.value = true
-  queryParams.value.pid = currentPid.value
+  queryParams.value.pid = currentPid.value || folderId.value
   getFileList(queryParams.value).then(res => {
     fileList.value = res.rows || []
     total.value = res.total || 0
@@ -243,8 +243,8 @@ function fetchFileList() {
   }).catch(() => {
     loading.value = false
   })
-  if (showPlanValue.value && currentPid.value) {
-    loadPlanValue(currentPid.value)
+  if (showPlanValue.value && (currentPid.value || folderId.value)) {
+    loadPlanValue(currentPid.value || folderId.value)
   }
 }
 
@@ -419,12 +419,16 @@ function formatFileSize(size) {
 
 onMounted(() => {
   currentPid.value = folderId.value
-  getFolderTree()
-  fetchFileList()
+  Promise.all([getFolderTree(), fetchFileList()])
 })
 </script>
 
 <style lang="scss" scoped>
+.tree-sidebar-manage-wrap {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+}
 .page-header-title {
   font-size: 18px;
   font-weight: 600;
@@ -432,14 +436,24 @@ onMounted(() => {
 .portal-detail-wrap {
   display: flex;
   gap: 12px;
+  flex: 1;
+  min-height: 0;
 }
 .folder-sidebar {
   width: 240px;
   flex-shrink: 0;
   background: #fff;
   padding: 12px;
-  border-radius: 4px;
+  border-radius: 6px;
   border: 1px solid #ebeef5;
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+  overflow: hidden;
+  .el-tree {
+    flex: 1;
+    overflow: auto;
+  }
 }
 .sidebar-header {
   display: flex;
@@ -448,12 +462,30 @@ onMounted(() => {
   font-size: 14px;
   font-weight: 600;
   margin-bottom: 8px;
+  flex-shrink: 0;
 }
 .file-content {
   flex: 1;
   min-width: 0;
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+  overflow: hidden;
+}
+.content-header h3 {
+  margin: 0 0 12px;
+  font-size: 18px;
+  flex-shrink: 0;
+}
+.search-form {
+  flex-shrink: 0;
 }
 .mb8 {
   margin-bottom: 8px;
+  flex-shrink: 0;
+}
+.detail-table {
+  flex: 1;
+  overflow: auto;
 }
 </style>

+ 106 - 67
gw-ui/src/views/portal/zdgc.vue

@@ -1,54 +1,57 @@
 <template>
   <div class="app-container">
-    <div class="portal-header">
-      <h2 class="portal-title">重点工程</h2>
-      <div class="header-actions">
-        <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleBatchDelete">删除</el-button>
-        <el-button type="primary" icon="Plus" @click="handleAddProject">添加工程</el-button>
+    <div class="content-box">
+      <div class="portal-header">
+        <h2 class="portal-title">重点工程</h2>
+        <div class="header-actions">
+          <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleBatchDelete">删除</el-button>
+          <el-button type="primary" icon="Plus" @click="handleAddProject">添加工程</el-button>
+        </div>
       </div>
-    </div>
 
-    <el-tabs v-model="activeTab" @tab-click="handleTabClick" class="category-tabs">
-      <el-tab-pane v-for="tab in categoryTabs" :key="tab.id" :label="tab.name" :name="String(tab.id)" />
-    </el-tabs>
+      <el-tabs v-model="activeTab" @tab-click="handleTabClick" class="category-tabs">
+        <el-tab-pane v-for="tab in categoryTabs" :key="tab.id" :label="tab.name" :name="String(tab.id)" />
+      </el-tabs>
 
-    <div v-if="activeTab && activeTab !== '-1'" class="tab-content">
-      <el-form :model="queryParams" ref="queryRef" :inline="true" class="search-form">
-        <el-form-item label="工程名称" prop="name">
-          <el-input v-model="queryParams.name" placeholder="请输入工程名称" clearable style="width: 220px" @keyup.enter="handleQuery" />
-        </el-form-item>
-        <el-form-item>
-          <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
-          <el-button icon="Refresh" @click="resetQuery">重置</el-button>
-        </el-form-item>
-      </el-form>
+      <div v-if="activeTab && activeTab !== '-1'" class="tab-content">
+        <el-form :model="queryParams" ref="queryRef" :inline="true" class="search-form">
+          <el-form-item label="工程名称" prop="name">
+            <el-input v-model="queryParams.name" placeholder="请输入工程名称" clearable style="width: 220px" @keyup.enter="handleQuery" />
+          </el-form-item>
+          <el-form-item>
+            <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+            <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+          </el-form-item>
+        </el-form>
 
-      <el-row :gutter="16">
-        <el-col v-for="item in projectList" :key="item.id" :xs="24" :sm="12" :md="8" :lg="6" :xl="6" class="mb16">
-          <el-card shadow="hover" class="project-card" :style="{ borderTop: '3px solid ' + getPlanColor(item.planValue) }">
-            <div class="card-header-row">
-              <el-checkbox v-model="item._checked" @change="handleCheckChange" @click.stop />
-              <el-button link type="danger" icon="Delete" class="card-delete-btn" @click.stop="handleDelete(item)" />
-            </div>
-            <div class="card-body" @click="handleProjectClick(item)">
-              <div class="card-info">
-                <div class="card-title">{{ item.name }}</div>
-                <div class="card-progress">
-                  <el-progress
-                    :percentage="item.planValue || 0"
-                    :color="getPlanColor(item.planValue)"
-                    :stroke-width="10"
-                    :format="(pct) => pct + '%'"
-                  />
+        <div class="card-grid">
+          <el-row :gutter="16">
+            <el-col v-for="item in projectList" :key="item.id" :xs="24" :sm="12" :md="8" :lg="6" :xl="4" class="mb16">
+            <el-card shadow="hover" class="project-card" :style="{ borderTop: '3px solid ' + getPlanColor(item.planValue) }" @click="handleProjectClick(item)">
+              <div class="card-header-row" @click.stop>
+                <el-checkbox v-model="item._checked" @change="handleCheckChange" />
+                <el-button link type="danger" icon="Delete" class="card-delete-btn" @click.stop="handleDelete(item)" />
+              </div>
+              <div class="card-body">
+                <div class="card-info">
+                  <div class="card-title">{{ item.name }}</div>
+                  <div class="card-progress">
+                    <el-progress
+                      :percentage="item.planValue || 0"
+                      :color="getPlanColor(item.planValue)"
+                      :stroke-width="10"
+                      :format="(pct) => pct + '%'"
+                    />
+                  </div>
                 </div>
               </div>
-            </div>
-          </el-card>
-        </el-col>
-      </el-row>
-
-      <el-empty v-if="!loading && projectList.length === 0" description="暂无工程数据" />
-      <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="fetchProjects" />
+            </el-card>
+            </el-col>
+          </el-row>
+          <el-empty v-if="!loading && projectList.length === 0" description="暂无工程数据" />
+        </div>
+        <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="showTabProjects" />
+      </div>
     </div>
 
     <el-dialog title="添加工程" v-model="addDialogVisible" width="450px" append-to-body>
@@ -103,40 +106,46 @@ function getPlanColor(val) {
 function loadTabs() {
   folderList({ fdCate: 'zdgc', pageSize: 999 }).then(res => {
     const list = res.rows || res.data || []
-    const tree = proxy.handleTree(list, 'id', 'pid')
+    const tree = proxy.handleTree(JSON.parse(JSON.stringify(list)), 'id', 'pid')
+    // 把children直接存为tab的projects,方便切换时直接取
+    tree.forEach(t => { t.projects = t.children || []; delete t.children })
     categoryTabs.value = tree
     if (tree.length > 0) {
       activeTab.value = String(tree[0].id)
-      queryParams.pid = tree[0].id
-      fetchProjects()
+      showTabProjects()
     }
   })
 }
 
-function handleTabClick() {
-  queryParams.pid = Number(activeTab.value)
-  queryParams.pageNum = 1
-  fetchProjects()
+function handleTabClick(pane) {
+  activeTab.value = String(pane.paneName ?? pane.props?.name ?? pane.name)
 }
 
-function fetchProjects() {
-  if (!queryParams.pid) return
+watch(activeTab, (val) => {
+  if (val && val !== '-1') showTabProjects()
+})
+
+function showTabProjects() {
   loading.value = true
-  folderList({ ...queryParams, fdCate: 'zdgc' }).then(res => {
-    projectList.value = (res.rows || res.data || []).map(item => ({ ...item, _checked: false }))
-    total.value = res.total || projectList.value.length
-    loading.value = false
-  }).catch(() => { loading.value = false })
+  const tab = categoryTabs.value.find(t => String(t.id) === activeTab.value)
+  let filtered = tab?.projects || []
+  if (queryParams.name) {
+    filtered = filtered.filter(f => f.name?.includes(queryParams.name))
+  }
+  const ps = queryParams.pageSize || 20
+  const pn = queryParams.pageNum || 1
+  total.value = filtered.length
+  projectList.value = filtered.slice((pn - 1) * ps, pn * ps).map(item => ({ ...item, _checked: false }))
+  loading.value = false
 }
 
 function handleQuery() {
   queryParams.pageNum = 1
-  fetchProjects()
+  showTabProjects()
 }
 
 function resetQuery() {
   proxy.resetForm('queryRef')
-  queryParams.pid = Number(activeTab.value)
   handleQuery()
 }
 
@@ -160,6 +169,16 @@ function resetAddForm() {
   addForm.planValue = 0
 }
 
+function reloadAll() {
+  folderList({ fdCate: 'zdgc', pageSize: 999 }).then(res => {
+    const list = res.rows || res.data || []
+    const tree = proxy.handleTree(JSON.parse(JSON.stringify(list)), 'id', 'pid')
+    tree.forEach(t => { t.projects = t.children || []; delete t.children })
+    categoryTabs.value = tree
+    showTabProjects()
+  })
+}
+
 function submitAdd() {
   addFormRef.value.validate(valid => {
     if (!valid) return
@@ -167,7 +186,7 @@ function submitAdd() {
       if (res.code === 200) {
         proxy.$modal.msgSuccess('添加成功')
         addDialogVisible.value = false
-        fetchProjects()
+        reloadAll()
       }
     })
   })
@@ -178,7 +197,7 @@ function handleDelete(item) {
     return deleteFolderRecursive(item.id)
   }).then(() => {
     proxy.$modal.msgSuccess('删除成功')
-    fetchProjects()
+    reloadAll()
   }).catch(() => {})
 }
 
@@ -193,7 +212,7 @@ async function handleBatchDelete() {
       await deleteFolderRecursive(id)
     }
     proxy.$modal.msgSuccess('删除成功')
-    fetchProjects()
+    reloadAll()
   } catch (_) {}
 }
 
@@ -201,17 +220,37 @@ onMounted(() => { loadTabs() })
 </script>
 
 <style lang="scss" scoped>
+.content-box {
+  background: #fff;
+  border-radius: 8px;
+  padding: 20px;
+  border: 1px solid #ebeef5;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+}
+.tab-content {
+  display: flex;
+  flex-direction: column;
+  flex: 1;
+  min-height: 0;
+}
+.card-grid {
+  flex: 1;
+  overflow: auto;
+  min-height: 0;
+}
 .portal-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
 .portal-title { font-size: 20px; font-weight: 600; margin: 0; }
 .header-actions { display: flex; gap: 8px; }
-.category-tabs { margin-bottom: 12px; }
-.search-form { margin-bottom: 12px; }
-.project-card { cursor: pointer; transition: transform 0.2s; &:hover { transform: translateY(-2px); } }
-.card-header-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
+.category-tabs { margin-bottom: 16px; }
+.search-form { margin-bottom: 16px; }
+.project-card { cursor: pointer; transition: all 0.25s; border-radius: 8px; &:hover { transform: translateY(-3px); box-shadow: 0 6px 20px rgba(0,0,0,0.08); } }
+.card-header-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; }
 .card-delete-btn { font-size: 14px; }
 .card-body { cursor: pointer; }
 .card-info { min-width: 0; }
-.card-title { font-size: 15px; font-weight: 500; margin-bottom: 8px; }
+.card-title { font-size: 15px; font-weight: 500; margin-bottom: 6px; }
 .card-progress { margin-top: 4px; }
 .mb16 { margin-bottom: 16px; }
 </style>

+ 70 - 54
gw-ui/src/views/portal/zggc.vue

@@ -1,50 +1,53 @@
 <template>
   <div class="app-container">
-    <div class="portal-header">
-      <h2 class="portal-title">直管工程</h2>
-      <div class="header-actions">
-        <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleBatchDelete">删除</el-button>
-        <el-button type="primary" icon="Plus" @click="handleAddProject">添加项目</el-button>
+    <div class="content-box">
+      <div class="portal-header">
+        <h2 class="portal-title">直管工程</h2>
+        <div class="header-actions">
+          <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleBatchDelete">删除</el-button>
+          <el-button type="primary" icon="Plus" @click="handleAddProject">添加项目</el-button>
+        </div>
       </div>
-    </div>
 
-    <el-form :model="queryParams" ref="queryRef" :inline="true" class="search-form">
-      <el-form-item label="项目名称" prop="name">
-        <el-input v-model="queryParams.name" placeholder="请输入项目名称" clearable style="width: 220px" @keyup.enter="handleQuery" />
-      </el-form-item>
-      <el-form-item>
-        <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
-        <el-button icon="Refresh" @click="resetQuery">重置</el-button>
-      </el-form-item>
-    </el-form>
-
-    <el-row :gutter="16">
-      <el-col v-for="item in folderListData" :key="item.id" :xs="24" :sm="12" :md="8" :lg="6" :xl="6" class="mb16">
-        <el-card shadow="hover" class="project-card" :style="{ borderTop: '3px solid ' + getPlanColor(item.planValue) }">
-          <div class="card-header-row">
-            <el-checkbox v-model="item._checked" @change="handleCheckChange" @click.stop />
-            <el-button link type="danger" icon="Delete" class="card-delete-btn" @click.stop="handleDelete(item)" />
-          </div>
-          <div class="card-body" @click="handleProjectClick(item)">
-            <div class="card-info">
-              <div class="card-title">{{ item.name }}</div>
-              <div class="card-progress">
-                <el-progress
-                  :percentage="item.planValue || 0"
-                  :color="getPlanColor(item.planValue)"
-                  :stroke-width="10"
-                  :format="(pct) => pct + '%'"
-                />
-              </div>
-            </div>
-          </div>
-        </el-card>
-      </el-col>
-    </el-row>
+      <el-form :model="queryParams" ref="queryRef" :inline="true" class="search-form">
+        <el-form-item label="项目名称" prop="name">
+          <el-input v-model="queryParams.name" placeholder="请输入项目名称" clearable style="width: 220px" @keyup.enter="handleQuery" />
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+          <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+        </el-form-item>
+      </el-form>
 
-    <el-empty v-if="!loading && folderListData.length === 0" description="暂无工程数据" />
+      <div class="card-grid">
+        <el-row :gutter="16">
+          <el-col v-for="item in folderListData" :key="item.id" :xs="24" :sm="12" :md="8" :lg="6" :xl="4" class="mb16">
+            <el-card shadow="hover" class="project-card" :style="{ borderTop: '3px solid ' + getPlanColor(item.planValue) }" @click="handleProjectClick(item)">
+              <div class="card-header-row" @click.stop>
+                <el-checkbox v-model="item._checked" @change="handleCheckChange" />
+                <el-button link type="danger" icon="Delete" class="card-delete-btn" @click.stop="handleDelete(item)" />
+              </div>
+              <div class="card-body">
+                <div class="card-info">
+                  <div class="card-title">{{ item.name }}</div>
+                  <div class="card-progress">
+                    <el-progress
+                      :percentage="item.planValue || 0"
+                      :color="getPlanColor(item.planValue)"
+                      :stroke-width="10"
+                      :format="(pct) => pct + '%'"
+                    />
+                  </div>
+                </div>
+              </div>
+            </el-card>
+          </el-col>
+        </el-row>
+        <el-empty v-if="!loading && folderListData.length === 0" description="暂无工程数据" />
+      </div>
 
-    <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getFolderList" />
+      <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getFolderList" />
+    </div>
 
     <el-dialog title="添加项目" v-model="addDialogVisible" width="450px" append-to-body>
       <el-form ref="addFormRef" :model="addForm" :rules="addRules" label-width="100px">
@@ -178,36 +181,49 @@ onMounted(() => { getFolderList() })
 </script>
 
 <style lang="scss" scoped>
+.content-box {
+  background: #fff;
+  border-radius: 8px;
+  padding: 20px;
+  border: 1px solid #ebeef5;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+}
+.card-grid {
+  flex: 1;
+  overflow: auto;
+  min-height: 0;
+}
+
 .portal-header {
   display: flex;
   align-items: center;
   justify-content: space-between;
-  margin-bottom: 12px;
+  margin-bottom: 16px;
 }
 .portal-title { font-size: 20px; font-weight: 600; margin: 0; }
 .header-actions { display: flex; gap: 8px; }
-.search-form { margin-bottom: 12px; }
+.search-form { margin-bottom: 16px; }
 
 .project-card {
   cursor: pointer;
-  transition: transform 0.2s;
-  &:hover { transform: translateY(-2px); }
+  transition: all 0.25s;
+  border-radius: 8px;
+  &:hover {
+    transform: translateY(-3px);
+    box-shadow: 0 6px 20px rgba(0, 0, 0, 0.08);
+  }
 }
-
 .card-header-row {
   display: flex;
   align-items: center;
   justify-content: space-between;
-  margin-bottom: 8px;
-}
-
-.card-delete-btn {
-  font-size: 14px;
+  margin-bottom: 6px;
 }
-
-.card-body { cursor: pointer; }
+.card-delete-btn { font-size: 14px; }
 .card-info { min-width: 0; }
-.card-title { font-size: 15px; font-weight: 500; margin-bottom: 8px; }
+.card-title { font-size: 15px; font-weight: 500; margin-bottom: 6px; }
 .card-progress { margin-top: 4px; }
 .mb16 { margin-bottom: 16px; }
 </style>