From d47868f6343b4dc24150d6040e47ca66b920c1b3 Mon Sep 17 00:00:00 2001
From: zzw <228544128@qq.com>
Date: 星期五, 02 十月 2026 18:35:42 +0800
Subject: [PATCH] feat: add MDI tabbed workspace

---
 emergency-repair-web/src/views/completion/CompletionList.vue     |    2 
 emergency-repair-web/src/views/fault/XcbReview.vue               |    2 
 emergency-repair-web/src/views/fault/DispatchPage.vue            |    2 
 emergency-repair-web/src/views/system/RoleManage.vue             |    2 
 emergency-repair-web/src/views/fault/FaultReport.vue             |    2 
 emergency-repair-web/src/router/index.js                         |   48 ++++----
 emergency-repair-web/src/views/system/MenuPermission.vue         |    2 
 emergency-repair-web/src/views/acceptance/AcceptanceList.vue     |    8 
 emergency-repair-web/src/layout/AppLayout.vue                    |  211 ++++++++++++++++++++++++++++++++++-
 emergency-repair-web/src/views/system/ContractorManage.vue       |    2 
 emergency-repair-web/src/views/fault/SimpleApproval.vue          |    6 
 emergency-repair-web/src/views/report/ReportPage.vue             |    6 
 emergency-repair-web/src/views/system/TeamMajorManage.vue        |    2 
 emergency-repair-web/src/views/completion/CompletionReport.vue   |    5 
 emergency-repair-web/src/views/system/TreeManage.vue             |    8 
 emergency-repair-web/src/views/system/EngineeringMajorManage.vue |    2 
 16 files changed, 259 insertions(+), 51 deletions(-)

diff --git a/emergency-repair-web/src/layout/AppLayout.vue b/emergency-repair-web/src/layout/AppLayout.vue
index 3a2878f..14ee88c 100644
--- a/emergency-repair-web/src/layout/AppLayout.vue
+++ b/emergency-repair-web/src/layout/AppLayout.vue
@@ -9,9 +9,43 @@
         </el-menu>
       </el-aside>
       <el-main class="layout-main">
-        <transition name="fade" mode="out-in">
-          <router-view></router-view>
-        </transition>
+        <div class="mdi-tabbar">
+          <div class="mdi-caption"><i class="el-icon-document-copy"></i><span>宸ヤ綔鍖�</span></div>
+          <div ref="tabScroll" class="mdi-tabs-scroll">
+            <div class="mdi-tabs">
+              <div
+                v-for="doc in openDocs"
+                :key="doc.fullPath"
+                class="mdi-tab"
+                :class="{ 'is-active': isActiveDoc(doc) }"
+                :title="doc.title"
+                @click="activateDoc(doc)"
+              >
+                <i :class="doc.icon"></i>
+                <span class="mdi-tab-title">{{ doc.title }}</span>
+                <i v-if="doc.pinned" class="el-icon-lock mdi-tab-pin"></i>
+                <i v-else class="el-icon-close mdi-tab-close" title="鍏抽棴" @click.stop="closeDoc(doc)"></i>
+              </div>
+            </div>
+          </div>
+          <div class="mdi-actions">
+            <el-dropdown trigger="click" @command="handleCommand">
+              <button class="mdi-actions-button" type="button" title="鏂囨。鎿嶄綔"><i class="el-icon-more"></i></button>
+              <el-dropdown-menu slot="dropdown">
+                <el-dropdown-item command="closeCurrent" :disabled="!activeDoc || activeDoc.pinned">鍏抽棴褰撳墠</el-dropdown-item>
+                <el-dropdown-item command="closeOthers" :disabled="!canCloseOthers">鍏抽棴鍏朵粬</el-dropdown-item>
+                <el-dropdown-item command="closeAll" :disabled="openDocs.length <= 1">鍏抽棴鍏ㄩ儴</el-dropdown-item>
+              </el-dropdown-menu>
+            </el-dropdown>
+          </div>
+        </div>
+        <div ref="workspace" class="mdi-workspace" @scroll="rememberScroll">
+          <router-view v-slot="{ Component, route }">
+            <keep-alive :include="cachedNames" :max="20">
+              <component :is="Component" :key="viewKey(route)" />
+            </keep-alive>
+          </router-view>
+        </div>
       </el-main>
     </el-container>
   </el-container>
@@ -22,11 +56,52 @@
 import AppHeader from '@/components/AppHeader.vue'
 import MenuNode from '@/components/MenuNode.vue'
 
+const HOME_DOC = {
+  fullPath: '/dashboard',
+  title: '棣栭〉闂ㄦ埛',
+  icon: 'el-icon-s-home',
+  cacheName: 'Dashboard',
+  pinned: true,
+  version: 1
+}
+
+const REPORT_TITLES = {
+  overview: '瑁呭鏁呴殰鎬讳綋鍒嗘瀽',
+  'top-equipment': '鏁呴殰楂樺彂璁惧Top',
+  'by-team': '鎸塗缁熻',
+  contractors: '鍘傚缁翠慨鎯呭喌',
+  trend: '骞村害瓒嬪娍'
+}
+
 export default {
   name: 'AppLayout',
   components: { AppHeader, MenuNode },
   data() {
-    return { menus: [] }
+    return {
+      menus: [],
+      openDocs: [],
+      currentFullPath: '',
+      cacheVersions: {},
+      scrollTops: {}
+    }
+  },
+  computed: {
+    activeDoc() {
+      return this.openDocs.find(doc => doc.fullPath === this.currentFullPath) || null
+    },
+    cachedNames() {
+      return [...new Set(this.openDocs.map(doc => doc.cacheName).filter(Boolean))]
+    },
+    canCloseOthers() {
+      const keepCount = this.activeDoc && this.activeDoc.pinned ? 1 : 2
+      return this.openDocs.length > keepCount
+    }
+  },
+  watch: {
+    '$route': {
+      immediate: true,
+      handler(route) { this.syncRoute(route) }
+    }
   },
   created() {
     this.loadMenus()
@@ -36,6 +111,112 @@
       api.get('/system/menu/nav').then(res => {
         this.menus = (res.data || []).filter(item => item.visible !== 0)
       }).catch(() => {})
+    },
+    syncRoute(route) {
+      if (!route || route.path === '/login') return
+      if (this.currentFullPath && this.$refs.workspace) {
+        this.scrollTops[this.currentFullPath] = this.$refs.workspace.scrollTop
+      }
+      this.ensureHomeDoc()
+      const fullPath = route.fullPath
+      let doc = this.openDocs.find(item => item.fullPath === fullPath)
+      if (!doc) {
+        doc = {
+          fullPath,
+          title: this.resolveTitle(route),
+          icon: this.resolveIcon(route.path),
+          cacheName: route.meta.cacheName || route.path,
+          pinned: false,
+          version: (this.cacheVersions[fullPath] || 0) + 1
+        }
+        this.openDocs.push(doc)
+      } else {
+        doc.title = this.resolveTitle(route)
+        doc.icon = this.resolveIcon(route.path)
+        doc.cacheName = route.meta.cacheName || doc.cacheName
+      }
+      this.currentFullPath = fullPath
+      this.$nextTick(() => {
+        this.restoreScroll()
+        this.scrollActiveTab()
+      })
+    },
+    ensureHomeDoc() {
+      if (!this.openDocs.some(doc => doc.pinned)) {
+        this.openDocs.unshift({ ...HOME_DOC })
+      }
+    },
+    resolveTitle(route) {
+      if (route.path.startsWith('/reports/')) {
+        return REPORT_TITLES[route.params.type] || route.meta.title || '缁熻鎶ヨ〃'
+      }
+      return route.meta.title || route.name || route.path
+    },
+    resolveIcon(path) {
+      if (path === '/dashboard') return 'el-icon-s-home'
+      if (path === '/fault/report') return 'el-icon-edit-outline'
+      if (path.includes('/approval/') || path === '/fault/review') return 'el-icon-s-check'
+      if (path === '/fault/dispatch') return 'el-icon-s-promotion'
+      if (path.startsWith('/fault/completion') || path === '/completion/report') return 'el-icon-finished'
+      if (path.startsWith('/acceptance/')) return 'el-icon-circle-check'
+      if (path.startsWith('/reports/')) return 'el-icon-data-analysis'
+      if (path.startsWith('/system/')) return 'el-icon-setting'
+      return 'el-icon-document'
+    },
+    isActiveDoc(doc) { return doc.fullPath === this.currentFullPath },
+    viewKey(route) {
+      const doc = this.openDocs.find(item => item.fullPath === route.fullPath)
+      return route.fullPath + '#' + (doc ? doc.version : 0)
+    },
+    activateDoc(doc) {
+      if (!this.isActiveDoc(doc)) this.$router.push(doc.fullPath).catch(() => {})
+    },
+    closeDoc(doc) {
+      if (!doc || doc.pinned) return
+      const index = this.openDocs.findIndex(item => item.fullPath === doc.fullPath)
+      if (index < 0) return
+      const wasActive = this.isActiveDoc(doc)
+      this.cacheVersions[doc.fullPath] = doc.version + 1
+      this.openDocs.splice(index, 1)
+      if (wasActive) {
+        const target = this.openDocs[index - 1] || this.openDocs[index] || this.openDocs[0]
+        this.$router.push(target.fullPath).catch(() => {})
+      } else {
+        this.$nextTick(this.scrollActiveTab)
+      }
+    },
+    handleCommand(command) {
+      if (command === 'closeCurrent') this.closeDoc(this.activeDoc)
+      if (command === 'closeOthers') this.closeOtherDocs()
+      if (command === 'closeAll') this.closeAllDocs()
+    },
+    closeOtherDocs() {
+      if (!this.activeDoc) return
+      const keepPath = this.activeDoc.fullPath
+      this.openDocs.filter(doc => !doc.pinned && doc.fullPath !== keepPath).forEach(doc => {
+        this.cacheVersions[doc.fullPath] = doc.version + 1
+      })
+      this.openDocs = this.openDocs.filter(doc => doc.pinned || doc.fullPath === keepPath)
+      this.$nextTick(this.scrollActiveTab)
+    },
+    closeAllDocs() {
+      this.openDocs.filter(doc => !doc.pinned).forEach(doc => {
+        this.cacheVersions[doc.fullPath] = doc.version + 1
+      })
+      this.openDocs = this.openDocs.filter(doc => doc.pinned)
+      this.$router.push('/dashboard').catch(() => {})
+    },
+    rememberScroll() {
+      if (this.currentFullPath && this.$refs.workspace) {
+        this.scrollTops[this.currentFullPath] = this.$refs.workspace.scrollTop
+      }
+    },
+    restoreScroll() {
+      if (this.$refs.workspace) this.$refs.workspace.scrollTop = this.scrollTops[this.currentFullPath] || 0
+    },
+    scrollActiveTab() {
+      const active = this.$refs.tabScroll && this.$refs.tabScroll.querySelector('.mdi-tab.is-active')
+      if (active) active.scrollIntoView({ block: 'nearest', inline: 'nearest' })
     }
   }
 }
@@ -46,7 +227,7 @@
 .layout-body { flex: 1; min-height: 0; }
 .layout-aside { border-right: 1px solid #d9e2eb; background: #f5f8fb; overflow-x: hidden; overflow-y: auto; }
 .aside-caption { padding: 14px 18px 8px; color: #8090a2; font-size: 12px; letter-spacing: .08em; }
-.layout-main { min-width: 0; padding: 0; overflow: auto; background: #eef2f6; }
+.layout-main { display: flex; flex-direction: column; min-width: 0; min-height: 0; padding: 0; overflow: hidden; background: #eef2f6; }
 .el-menu { border-right: 0; }
 .layout-aside ::v-deep .el-menu-item,
 .layout-aside ::v-deep .el-submenu__title {
@@ -65,6 +246,22 @@
   overflow-wrap: anywhere;
   word-break: break-word;
 }
-.fade-enter-active, .fade-leave-active { transition: opacity .12s ease; }
-.fade-enter, .fade-leave-to { opacity: 0; }
+.mdi-tabbar { display: flex; flex: 0 0 40px; align-items: stretch; height: 40px; padding-top: 5px; border-bottom: 1px solid #cfdae5; background: #e3eaf1; }
+.mdi-caption { display: flex; flex: 0 0 auto; align-items: center; gap: 6px; padding: 0 12px; border-right: 1px solid rgba(125, 147, 169, .22); color: #64788d; font-size: 12px; }
+.mdi-caption i { color: #2c7fb8; font-size: 15px; }
+.mdi-tabs-scroll { flex: 1; min-width: 0; overflow-x: auto; overflow-y: hidden; }
+.mdi-tabs { display: inline-flex; align-items: flex-end; min-width: 100%; height: 100%; padding: 0 4px; }
+.mdi-tab { display: inline-flex; flex: 0 0 auto; align-items: center; gap: 6px; max-width: 220px; height: 34px; margin-right: 2px; padding: 0 9px; border: 1px solid transparent; border-bottom: 0; border-radius: 4px 4px 0 0; color: #5c6f82; background: rgba(255, 255, 255, .42); cursor: pointer; font-size: 13px; user-select: none; }
+.mdi-tab:hover { color: #245275; background: rgba(255, 255, 255, .72); }
+.mdi-tab.is-active { height: 35px; color: #1f3348; background: #eef2f6; border-color: #cfdae5; box-shadow: inset 0 2px 0 #2c7fb8; }
+.mdi-tab > i { flex: 0 0 auto; }
+.mdi-tab-title { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.mdi-tab-close, .mdi-tab-pin { width: 16px; color: #8191a2; text-align: center; }
+.mdi-tab-close { border-radius: 50%; font-size: 12px; }
+.mdi-tab-close:hover { color: #fff; background: #d9534f; }
+.mdi-tab-pin { font-size: 12px; }
+.mdi-actions { display: flex; flex: 0 0 auto; align-items: center; padding: 0 6px; border-left: 1px solid rgba(125, 147, 169, .22); }
+.mdi-actions-button { width: 30px; height: 28px; padding: 0; border: 0; border-radius: 3px; color: #587087; background: transparent; cursor: pointer; font-size: 17px; }
+.mdi-actions-button:hover { color: #1f5f91; background: rgba(255, 255, 255, .68); }
+.mdi-workspace { position: relative; flex: 1; min-height: 0; overflow: auto; background: #eef2f6; }
 </style>
diff --git a/emergency-repair-web/src/router/index.js b/emergency-repair-web/src/router/index.js
index bc9b4f8..35442f5 100644
--- a/emergency-repair-web/src/router/index.js
+++ b/emergency-repair-web/src/router/index.js
@@ -11,30 +11,30 @@
     path: '/',
     component: AppLayout,
     children: [
-      { path: 'dashboard', component: () => import('@/views/Dashboard.vue') },
-      { path: 'fault/report', component: () => import('@/views/fault/FaultReport.vue') },
-      { path: 'fault/review', component: () => import('@/views/fault/XcbReview.vue') },
-      { path: 'fault/approval/cmb', component: () => import('@/views/fault/SimpleApproval.vue'), meta: { stage: 'CMB' } },
-      { path: 'fault/approval/jcc-assistant', component: () => import('@/views/fault/SimpleApproval.vue'), meta: { stage: 'JCC_ASSISTANT' } },
-      { path: 'fault/approval/jcc-director', component: () => import('@/views/fault/SimpleApproval.vue'), meta: { stage: 'JCC_DIRECTOR' } },
-      { path: 'fault/dispatch', component: () => import('@/views/fault/DispatchPage.vue') },
-      { path: 'fault/completion', component: () => import('@/views/completion/CompletionList.vue') },
-      { path: 'completion/report', component: () => import('@/views/completion/CompletionReport.vue') },
-      { path: 'acceptance/application', component: () => import('@/views/acceptance/AcceptanceList.vue'), meta: { stage: 'APPLICATION' } },
-      { path: 'acceptance/inspector', component: () => import('@/views/acceptance/AcceptanceList.vue'), meta: { stage: 'INSPECTOR' } },
-      { path: 'acceptance/leader', component: () => import('@/views/acceptance/AcceptanceList.vue'), meta: { stage: 'LEADER' } },
-      { path: 'acceptance/print', component: () => import('@/views/acceptance/AcceptanceList.vue'), meta: { stage: 'PRINT' } },
-      { path: 'acceptance/rating', component: () => import('@/views/acceptance/AcceptanceList.vue'), meta: { stage: 'RATING' } },
-      { path: 'reports/:type', component: () => import('@/views/report/ReportPage.vue') },
-      { path: 'system/contractors', component: () => import('@/views/system/ContractorManage.vue') },
-      { path: 'system/product-tree', component: () => import('@/views/system/TreeManage.vue'), meta: { treeType: 'product' } },
-      { path: 'system/base-equipment', component: () => import('@/views/system/TreeManage.vue'), meta: { treeType: 'base' } },
-      { path: 'system/engineering-majors', component: () => import('@/views/system/EngineeringMajorManage.vue') },
-      { path: 'system/team-majors', component: () => import('@/views/system/TeamMajorManage.vue') },
-      { path: 'system/roles', component: () => import('@/views/system/RoleManage.vue') },
-      { path: 'system/menus', component: () => import('@/views/system/MenuManage.vue') },
-      { path: 'system/menu-permissions', component: () => import('@/views/system/MenuPermission.vue') },
-      { path: 'system/three-member', component: () => import('@/views/system/ThreeMember.vue') }
+      { path: 'dashboard', component: () => import('@/views/Dashboard.vue'), meta: { title: '棣栭〉闂ㄦ埛', cacheName: 'Dashboard' } },
+      { path: 'fault/report', component: () => import('@/views/fault/FaultReport.vue'), meta: { title: '鏁呴殰涓婃姤', cacheName: 'FaultReport' } },
+      { path: 'fault/review', component: () => import('@/views/fault/XcbReview.vue'), meta: { title: 'XCB鍔╃悊瀹℃牳', cacheName: 'XcbReview' } },
+      { path: 'fault/approval/cmb', component: () => import('@/views/fault/SimpleApproval.vue'), props: { stage: 'CMB' }, meta: { title: 'CMB棰嗗瀹℃壒', stage: 'CMB', cacheName: 'SimpleApproval' } },
+      { path: 'fault/approval/jcc-assistant', component: () => import('@/views/fault/SimpleApproval.vue'), props: { stage: 'JCC_ASSISTANT' }, meta: { title: 'JCC鍔╃悊瀹℃壒', stage: 'JCC_ASSISTANT', cacheName: 'SimpleApproval' } },
+      { path: 'fault/approval/jcc-director', component: () => import('@/views/fault/SimpleApproval.vue'), props: { stage: 'JCC_DIRECTOR' }, meta: { title: 'JCC澶勯暱瀹℃壒', stage: 'JCC_DIRECTOR', cacheName: 'SimpleApproval' } },
+      { path: 'fault/dispatch', component: () => import('@/views/fault/DispatchPage.vue'), meta: { title: 'XCB鍔╃悊娲惧崟', cacheName: 'DispatchPage' } },
+      { path: 'fault/completion', component: () => import('@/views/completion/CompletionList.vue'), meta: { title: '瀹屽伐鎯呭喌澶勭悊', cacheName: 'CompletionList' } },
+      { path: 'completion/report', component: () => import('@/views/completion/CompletionReport.vue'), props: route => ({ workOrderId: route.query.workOrderId || null }), meta: { title: '瀹屽伐淇℃伅涓婃姤', cacheName: 'CompletionReport' } },
+      { path: 'acceptance/application', component: () => import('@/views/acceptance/AcceptanceList.vue'), props: { stage: 'APPLICATION' }, meta: { title: '楠屾敹鐢宠', stage: 'APPLICATION', cacheName: 'AcceptanceList' } },
+      { path: 'acceptance/inspector', component: () => import('@/views/acceptance/AcceptanceList.vue'), props: { stage: 'INSPECTOR' }, meta: { title: '璐ㄦ鍛橀獙鏀�', stage: 'INSPECTOR', cacheName: 'AcceptanceList' } },
+      { path: 'acceptance/leader', component: () => import('@/views/acceptance/AcceptanceList.vue'), props: { stage: 'LEADER' }, meta: { title: '璐ㄦ璐熻矗浜洪獙鏀�', stage: 'LEADER', cacheName: 'AcceptanceList' } },
+      { path: 'acceptance/print', component: () => import('@/views/acceptance/AcceptanceList.vue'), props: { stage: 'PRINT' }, meta: { title: '瀹屽伐楠屾敹鎵撳嵃杈撳嚭', stage: 'PRINT', cacheName: 'AcceptanceList' } },
+      { path: 'acceptance/rating', component: () => import('@/views/acceptance/AcceptanceList.vue'), props: { stage: 'RATING' }, meta: { title: '淇悗璐ㄩ噺璇勭骇', stage: 'RATING', cacheName: 'AcceptanceList' } },
+      { path: 'reports/:type', component: () => import('@/views/report/ReportPage.vue'), props: route => ({ type: route.params.type }), meta: { title: '缁熻鎶ヨ〃', cacheName: 'ReportPage' } },
+      { path: 'system/contractors', component: () => import('@/views/system/ContractorManage.vue'), meta: { title: '鎵夸慨鍗曚綅绠$悊', cacheName: 'ContractorManage' } },
+      { path: 'system/product-tree', component: () => import('@/views/system/TreeManage.vue'), props: { treeType: 'product' }, meta: { title: '浜у搧缁撴瀯鏍戠鐞�', treeType: 'product', cacheName: 'TreeManage' } },
+      { path: 'system/base-equipment', component: () => import('@/views/system/TreeManage.vue'), props: { treeType: 'base' }, meta: { title: '璁惧鍩虹搴撶鐞�', treeType: 'base', cacheName: 'TreeManage' } },
+      { path: 'system/engineering-majors', component: () => import('@/views/system/EngineeringMajorManage.vue'), meta: { title: '宸ョ▼涓撲笟閰嶇疆', cacheName: 'EngineeringMajorManage' } },
+      { path: 'system/team-majors', component: () => import('@/views/system/TeamMajorManage.vue'), meta: { title: 'T闃熶笓涓氶厤缃�', cacheName: 'TeamMajorManage' } },
+      { path: 'system/roles', component: () => import('@/views/system/RoleManage.vue'), meta: { title: '瑙掕壊绠$悊', cacheName: 'RoleManage' } },
+      { path: 'system/menus', component: () => import('@/views/system/MenuManage.vue'), meta: { title: '鑿滃崟绠$悊', cacheName: 'MenuManage' } },
+      { path: 'system/menu-permissions', component: () => import('@/views/system/MenuPermission.vue'), meta: { title: '鑿滃崟鏉冮檺', cacheName: 'MenuPermission' } },
+      { path: 'system/three-member', component: () => import('@/views/system/ThreeMember.vue'), meta: { title: '涓夊憳绠$悊', cacheName: 'ThreeMember' } }
     ]
   }
 ]
diff --git a/emergency-repair-web/src/views/acceptance/AcceptanceList.vue b/emergency-repair-web/src/views/acceptance/AcceptanceList.vue
index d801622..0ecce6b 100644
--- a/emergency-repair-web/src/views/acceptance/AcceptanceList.vue
+++ b/emergency-repair-web/src/views/acceptance/AcceptanceList.vue
@@ -16,7 +16,7 @@
     </div>
 
     <div class="table-panel">
-      <el-table :data="filteredRows" border stripe height="calc(100vh - 230px)">
+      <el-table :data="filteredRows" border stripe height="calc(100vh - 270px)">
         <el-table-column prop="workOrderNo" label="娲惧伐鍗曠紪鍙�" width="155" fixed></el-table-column>
         <el-table-column prop="status" label="楠屾敹鐘舵��" width="155" fixed>
           <template slot-scope="{ row }"><el-tag size="mini" :type="acceptanceMeta(row.status).type">{{ acceptanceMeta(row.status).text }}</el-tag></template>
@@ -83,6 +83,9 @@
 
 export default {
   name: 'AcceptanceList',
+  props: {
+    stage: { type: String, required: true }
+  },
   data() {
     return {
       rows: [],
@@ -95,7 +98,6 @@
     }
   },
   computed: {
-    stage() { return this.$route.meta.stage },
     pageTitle() {
       return { APPLICATION: '楠屾敹鐢宠', INSPECTOR: '璐ㄦ鍛橀獙鏀�', LEADER: '璐ㄦ璐熻矗浜洪獙鏀�', PRINT: '瀹屽伐楠屾敹鎵撳嵃杈撳嚭', RATING: '淇悗璐ㄩ噺璇勭骇' }[this.stage]
     },
@@ -125,7 +127,7 @@
     }
   },
   watch: {
-    '$route.path': 'onRouteChange'
+    stage: 'onRouteChange'
   },
   created() { this.load() },
   methods: {
diff --git a/emergency-repair-web/src/views/completion/CompletionList.vue b/emergency-repair-web/src/views/completion/CompletionList.vue
index befba8e..483b4e5 100644
--- a/emergency-repair-web/src/views/completion/CompletionList.vue
+++ b/emergency-repair-web/src/views/completion/CompletionList.vue
@@ -27,7 +27,7 @@
     </div>
 
     <div class="table-panel">
-      <el-table :data="rows" border stripe height="calc(100vh - 285px)" highlight-current-row @current-change="selectedRow = $event">
+      <el-table :data="rows" border stripe height="calc(100vh - 325px)" highlight-current-row @current-change="selectedRow = $event">
         <el-table-column width="46" align="center">
           <template slot-scope="{ row }"><el-radio v-model="selectedId" :label="row.id" @change="selectRow(row)"><span></span></el-radio></template>
         </el-table-column>
diff --git a/emergency-repair-web/src/views/completion/CompletionReport.vue b/emergency-repair-web/src/views/completion/CompletionReport.vue
index 35c5883..e7f9336 100644
--- a/emergency-repair-web/src/views/completion/CompletionReport.vue
+++ b/emergency-repair-web/src/views/completion/CompletionReport.vue
@@ -90,6 +90,9 @@
 import api from '@/api'
 export default {
   name: 'CompletionReport',
+  props: {
+    workOrderId: { type: [String, Number], default: null }
+  },
   data() {
     return {
       workOrder: null,
@@ -102,7 +105,7 @@
   },
   computed: {
     routeWorkOrderId() {
-      const value = this.$route.query.workOrderId
+      const value = this.workOrderId
       return value ? Number(value) : null
     }
   },
diff --git a/emergency-repair-web/src/views/fault/DispatchPage.vue b/emergency-repair-web/src/views/fault/DispatchPage.vue
index b534763..4592b29 100644
--- a/emergency-repair-web/src/views/fault/DispatchPage.vue
+++ b/emergency-repair-web/src/views/fault/DispatchPage.vue
@@ -19,7 +19,7 @@
     <el-alert :closable="false" type="info" show-icon title="涓�涓晠闅滄湁澶氫釜鎵夸慨鍗曚綅鏃讹紝灏嗗垎鍒敓鎴愭淳宸ュ崟锛涚紪鍙蜂粠鈥滄晠闅滅紪鍙�/01鈥濆紑濮嬩緷娆$紪鍙枫��" style="margin-bottom:12px"></el-alert>
 
     <div class="table-panel">
-      <el-table :data="rows" border stripe height="calc(100vh - 285px)" @selection-change="selection = $event">
+      <el-table :data="rows" border stripe height="calc(100vh - 325px)" @selection-change="selection = $event">
         <el-table-column type="selection" width="46"></el-table-column>
         <el-table-column prop="faultCode" label="鏁呴殰缂栧彿" width="180" fixed></el-table-column>
         <el-table-column prop="xNo" label="X鍙�" width="64"></el-table-column>
diff --git a/emergency-repair-web/src/views/fault/FaultReport.vue b/emergency-repair-web/src/views/fault/FaultReport.vue
index 7740c41..4c13110 100644
--- a/emergency-repair-web/src/views/fault/FaultReport.vue
+++ b/emergency-repair-web/src/views/fault/FaultReport.vue
@@ -56,7 +56,7 @@
     </div>
 
     <div class="table-panel">
-      <el-table :data="rows" border stripe height="calc(100vh - 355px)" :header-cell-style="{ textAlign: 'center' }" @selection-change="selection = $event">
+      <el-table :data="rows" border stripe height="calc(100vh - 395px)" :header-cell-style="{ textAlign: 'center' }" @selection-change="selection = $event">
         <el-table-column type="selection" width="46" :selectable="row => canEdit(row)"></el-table-column>
         <el-table-column prop="taskStatus" label="浠诲姟鐘舵��" width="145" fixed>
           <template slot-scope="{ row }"><el-tag size="mini" :type="taskMeta(row.taskStatus).type">{{ taskMeta(row.taskStatus).text }}</el-tag></template>
diff --git a/emergency-repair-web/src/views/fault/SimpleApproval.vue b/emergency-repair-web/src/views/fault/SimpleApproval.vue
index 0f398ab..aa303c8 100644
--- a/emergency-repair-web/src/views/fault/SimpleApproval.vue
+++ b/emergency-repair-web/src/views/fault/SimpleApproval.vue
@@ -17,7 +17,7 @@
     </div>
 
     <div class="table-panel">
-      <el-table :data="rows" border stripe height="calc(100vh - 230px)" :header-cell-style="{ textAlign: 'center' }" @selection-change="selection = $event">
+      <el-table :data="rows" border stripe height="calc(100vh - 270px)" :header-cell-style="{ textAlign: 'center' }" @selection-change="selection = $event">
         <el-table-column type="selection" width="46"></el-table-column>
         <el-table-column prop="taskStatus" label="浠诲姟鐘舵��" width="145" fixed>
           <template slot-scope="{ row }"><el-tag size="mini" :type="taskMeta(row.taskStatus).type">{{ taskMeta(row.taskStatus).text }}</el-tag></template>
@@ -81,11 +81,13 @@
 
 export default {
   name: 'SimpleApproval',
+  props: {
+    stage: { type: String, required: true }
+  },
   data() {
     return { rows: [], selection: [], keyword: '', dialogVisible: false, saving: false, form: { result: 'PASS', opinion: '' } }
   },
   computed: {
-    stage() { return this.$route.meta.stage },
     pageTitle() {
       return { CMB: 'CMB棰嗗瀹℃壒', JCC_ASSISTANT: 'JCC鍔╃悊瀹℃壒', JCC_DIRECTOR: 'JCC澶勯暱瀹℃壒' }[this.stage]
     },
diff --git a/emergency-repair-web/src/views/fault/XcbReview.vue b/emergency-repair-web/src/views/fault/XcbReview.vue
index 7a6384c..7fd8603 100644
--- a/emergency-repair-web/src/views/fault/XcbReview.vue
+++ b/emergency-repair-web/src/views/fault/XcbReview.vue
@@ -61,7 +61,7 @@
     </div>
 
     <div class="table-panel">
-      <el-table :data="rows" border stripe height="calc(100vh - 355px)" :header-cell-style="{ textAlign: 'center' }" @selection-change="selection = $event">
+      <el-table :data="rows" border stripe height="calc(100vh - 395px)" :header-cell-style="{ textAlign: 'center' }" @selection-change="selection = $event">
         <el-table-column type="selection" width="46" :selectable="canProcess"></el-table-column>
         <el-table-column prop="taskStatus" label="浠诲姟鐘舵��" width="145" fixed>
           <template slot-scope="{ row }"><el-tag size="mini" :type="taskMeta(row.taskStatus).type">{{ taskMeta(row.taskStatus).text }}</el-tag></template>
diff --git a/emergency-repair-web/src/views/report/ReportPage.vue b/emergency-repair-web/src/views/report/ReportPage.vue
index 2ae5bae..1baad25 100644
--- a/emergency-repair-web/src/views/report/ReportPage.vue
+++ b/emergency-repair-web/src/views/report/ReportPage.vue
@@ -38,11 +38,13 @@
 
 export default {
   name: 'ReportPage',
+  props: {
+    type: { type: String, required: true }
+  },
   data() {
     return { stats: {}, rows: [], chart: null }
   },
   computed: {
-    type() { return this.$route.params.type },
     title() {
       return {
         overview: '瑁呭鏁呴殰鎬讳綋鍒嗘瀽',
@@ -93,7 +95,7 @@
     }
   },
   watch: {
-    '$route.params.type': 'load'
+    type: 'load'
   },
   mounted() {
     this.load()
diff --git a/emergency-repair-web/src/views/system/ContractorManage.vue b/emergency-repair-web/src/views/system/ContractorManage.vue
index de3d7e3..1fa140f 100644
--- a/emergency-repair-web/src/views/system/ContractorManage.vue
+++ b/emergency-repair-web/src/views/system/ContractorManage.vue
@@ -15,7 +15,7 @@
     </div>
 
     <div class="table-panel">
-      <el-table :data="filteredRows" border stripe height="calc(100vh - 205px)">
+      <el-table :data="filteredRows" border stripe height="calc(100vh - 245px)">
         <el-table-column prop="sortNo" label="鎺掑簭" width="80" align="center"></el-table-column>
         <el-table-column label="甯哥敤" width="80" align="center">
           <template slot-scope="{ $index }"><el-tag v-if="$index < 7" size="mini" type="success">鍓�7</el-tag><span v-else>-</span></template>
diff --git a/emergency-repair-web/src/views/system/EngineeringMajorManage.vue b/emergency-repair-web/src/views/system/EngineeringMajorManage.vue
index 7b4309c..af96588 100644
--- a/emergency-repair-web/src/views/system/EngineeringMajorManage.vue
+++ b/emergency-repair-web/src/views/system/EngineeringMajorManage.vue
@@ -15,7 +15,7 @@
     </div>
 
     <div class="table-panel">
-      <el-table :data="filteredRows" border stripe height="calc(100vh - 205px)">
+      <el-table :data="filteredRows" border stripe height="calc(100vh - 245px)">
         <el-table-column type="index" label="搴忓彿" width="70" align="center"></el-table-column>
         <el-table-column prop="name" label="鍚嶇О" min-width="240"></el-table-column>
         <el-table-column label="鎿嶄綔" width="140" fixed="right">
diff --git a/emergency-repair-web/src/views/system/MenuPermission.vue b/emergency-repair-web/src/views/system/MenuPermission.vue
index ad5033f..f712cad 100644
--- a/emergency-repair-web/src/views/system/MenuPermission.vue
+++ b/emergency-repair-web/src/views/system/MenuPermission.vue
@@ -77,7 +77,7 @@
 </script>
 <style scoped>
 .permission-grid { display: grid; grid-template-columns: 1fr 1fr 320px; gap: 12px; }
-.permission-panel { min-height: calc(100vh - 180px); padding: 14px; border: 1px solid #dce4ec; border-radius: 4px; background: #fff; overflow: auto; }
+.permission-panel { min-height: calc(100vh - 220px); padding: 14px; border: 1px solid #dce4ec; border-radius: 4px; background: #fff; overflow: auto; }
 .panel-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
 .panel-head h3 { margin: 0; color: #31465d; font-size: 15px; }
 .intersection-list { display: flex; flex-wrap: wrap; gap: 6px; }
diff --git a/emergency-repair-web/src/views/system/RoleManage.vue b/emergency-repair-web/src/views/system/RoleManage.vue
index 37a437d..fbaec3e 100644
--- a/emergency-repair-web/src/views/system/RoleManage.vue
+++ b/emergency-repair-web/src/views/system/RoleManage.vue
@@ -5,7 +5,7 @@
       <el-button type="primary" icon="el-icon-plus" @click="openCreate">鏂板瑙掕壊</el-button>
     </div>
     <div class="table-panel">
-      <el-table :data="rows" border stripe height="calc(100vh - 150px)">
+      <el-table :data="rows" border stripe height="calc(100vh - 190px)">
         <el-table-column prop="sortNo" label="鎺掑簭" width="80" align="center"></el-table-column>
         <el-table-column prop="name" label="瑙掕壊鍚嶇О" min-width="180"></el-table-column>
         <el-table-column prop="code" label="瑙掕壊缂栫爜" width="170"></el-table-column>
diff --git a/emergency-repair-web/src/views/system/TeamMajorManage.vue b/emergency-repair-web/src/views/system/TeamMajorManage.vue
index 5dd73cc..e56d9c1 100644
--- a/emergency-repair-web/src/views/system/TeamMajorManage.vue
+++ b/emergency-repair-web/src/views/system/TeamMajorManage.vue
@@ -15,7 +15,7 @@
     </div>
 
     <div class="table-panel">
-      <el-table :data="filteredRows" border stripe height="calc(100vh - 205px)">
+      <el-table :data="filteredRows" border stripe height="calc(100vh - 245px)">
         <el-table-column type="index" label="搴忓彿" width="70" align="center"></el-table-column>
         <el-table-column prop="name" label="鍚嶇О" min-width="240"></el-table-column>
         <el-table-column prop="category" label="鍒嗙被" width="120" align="center">
diff --git a/emergency-repair-web/src/views/system/TreeManage.vue b/emergency-repair-web/src/views/system/TreeManage.vue
index 9431022..efba783 100644
--- a/emergency-repair-web/src/views/system/TreeManage.vue
+++ b/emergency-repair-web/src/views/system/TreeManage.vue
@@ -73,18 +73,20 @@
 import api from '@/api'
 export default {
   name: 'TreeManage',
+  props: {
+    treeType: { type: String, required: true }
+  },
   data() {
     return { tree: [], keyword: '', form: {}, saving: false, baseDevices: [] }
   },
   computed: {
-    treeType() { return this.$route.meta.treeType },
     isProduct() { return this.treeType === 'product' },
     maxLevel() { return this.isProduct ? 5 : 3 },
     levelNames() { return this.isProduct ? ['鍨嬪彿', 'X鍙�', '涓�绾х郴缁�', '浜岀骇绯荤粺', '璁惧'] : ['涓�绾х郴缁�', '浜岀骇绯荤粺', '璁惧'] }
   },
   watch: {
     keyword(value) { this.$refs.tree && this.$refs.tree.filter(value) },
-    '$route.meta.treeType'() { this.form = {}; this.load() }
+    treeType() { this.form = {}; this.load() }
   },
   created() { this.load() },
   methods: {
@@ -147,7 +149,7 @@
 </script>
 <style scoped>
 .tree-layout { display: grid; grid-template-columns: 420px minmax(0, 1fr); gap: 12px; }
-.tree-panel, .detail-panel { min-height: calc(100vh - 140px); padding: 14px; border: 1px solid #dce4ec; border-radius: 4px; background: #fff; }
+.tree-panel, .detail-panel { min-height: calc(100vh - 180px); padding: 14px; border: 1px solid #dce4ec; border-radius: 4px; background: #fff; }
 .tree-node { display: flex; align-items: center; gap: 7px; }
 .tree-node small { color: #95a2b0; }
 .level-badge { display: inline-flex; align-items: center; justify-content: center; min-width: 50px; height: 20px; padding: 0 5px; border-radius: 3px; color: #2f658f; background: #e9f3fb; font-size: 11px; }

--
Gitblit v1.9.1