Переглянути джерело

feat(frontend): 列表页增加行序号 + keep-alive页面缓存与滚动位置恢复

【行序号】
- ResourceList/ProjectList/OperationLog:分页表格增加序号列,自动按页码偏移续编
- ProjectDetail/UserManage/DictManage:非分页表格增加简单递增序号列
- DictManage:资源类型、服务商两个Tab表格均独立添加序号

【页面缓存与滚动恢复】
- Layout.vue:router-view 外层包裹 <keep-alive>,所有子路由组件保持存活
  返回列表页时数据、筛选条件、分页状态均保留,无需重新请求
- 新建 composables/useScrollRestore.js:
  利用 onActivated/onDeactivated 生命周期,
  离开列表页时保存 .layout__content 的 scrollTop,
  返回时精确恢复滚动位置,使用标准 useRoute() 获取路由引用
- 6个列表页(ResourceList/ProjectList/UserManage/DictManage/OperationLog/ProjectDetail)
  均已集成 useScrollRestore()

【Bug修复】
- ProjectDetail.vue:route.params.id watcher 增加 if(newVal) 防御检查,
  防止 keep-alive 下组件失活时 watcher 仍以 undefined id 触发 API 调用导致500错误
C 2 місяців тому
батько
коміт
b097a9284d

+ 30 - 0
frontend/src/composables/useScrollRestore.js

@@ -0,0 +1,30 @@
+import { onActivated, onDeactivated, nextTick } from 'vue'
+import { useRoute } from 'vue-router'
+
+/**
+ * 列表页滚动位置缓存
+ * 配合 <keep-alive> 使用,离开时保存滚动位置,返回时恢复
+ * 用法:在列表页 setup 中调用 useScrollRestore() 即可
+ */
+const scrollMap = new Map()
+
+export function useScrollRestore() {
+  const route = useRoute()
+
+  function getContainer() {
+    return document.querySelector('.layout__content')
+  }
+
+  onDeactivated(() => {
+    const el = getContainer()
+    if (el && route.name) scrollMap.set(route.name, el.scrollTop)
+  })
+
+  onActivated(() => {
+    const pos = scrollMap.get(route.name) ?? 0
+    nextTick(() => {
+      const el = getContainer()
+      if (el) el.scrollTop = pos
+    })
+  })
+}

+ 5 - 1
frontend/src/layout/Layout.vue

@@ -43,7 +43,11 @@
         </div>
       </header>
       <main class="layout__content">
-        <router-view />
+        <router-view v-slot="{ Component }">
+          <keep-alive>
+            <component :is="Component" />
+          </keep-alive>
+        </router-view>
       </main>
     </div>
 

+ 9 - 0
frontend/src/views/dict/DictManage.vue

@@ -8,6 +8,9 @@
             <el-button type="primary" @click="openTypeDialog()">+ 新增类型</el-button>
           </div>
           <el-table :data="typeList" v-loading="typeLoading">
+            <el-table-column label="序号" width="60" header-align="right" align="right">
+              <template #default="{ $index }">{{ $index + 1 }}</template>
+            </el-table-column>
             <el-table-column prop="name" label="名称" min-width="160" />
             <el-table-column prop="sort" label="排序" width="80" header-align="right" align="right" />
             <el-table-column prop="remark" label="备注" min-width="200" show-overflow-tooltip />
@@ -28,6 +31,9 @@
             <el-button type="primary" @click="openProviderDialog()">+ 新增服务商</el-button>
           </div>
           <el-table :data="providerList" v-loading="providerLoading">
+            <el-table-column label="序号" width="60" header-align="right" align="right">
+              <template #default="{ $index }">{{ $index + 1 }}</template>
+            </el-table-column>
             <el-table-column prop="name" label="名称" min-width="160" />
             <el-table-column prop="remark" label="备注" min-width="200" show-overflow-tooltip />
             <el-table-column label="操作" width="140">
@@ -75,6 +81,9 @@ import {
   resourceTypeListApi, resourceTypeSaveApi, resourceTypeDeleteApi,
   providerListApi, providerSaveApi, providerDeleteApi
 } from '@/api/dict'
+import { useScrollRestore } from '@/composables/useScrollRestore'
+
+useScrollRestore()
 
 const activeTab = ref('type')
 

+ 8 - 2
frontend/src/views/project/ProjectDetail.vue

@@ -34,6 +34,9 @@
       </div>
 
       <el-table :data="detail.resources" empty-text="暂未关联任何资源">
+        <el-table-column label="序号" width="60" header-align="right" align="right">
+          <template #default="{ $index }">{{ $index + 1 }}</template>
+        </el-table-column>
         <el-table-column prop="name" label="资源名称" min-width="140" />
         <el-table-column prop="typeName" label="类型" width="110" />
         <el-table-column prop="providerName" label="服务商" width="100" />
@@ -78,6 +81,9 @@ import { ElMessage } from 'element-plus'
 import { projectDetailApi } from '@/api/project'
 import { resourcePageApi, resourceRelateApi, resourceUnrelateApi } from '@/api/resource'
 import { resourceStatusClass } from '@/utils/status'
+import { useScrollRestore } from '@/composables/useScrollRestore'
+
+useScrollRestore()
 
 const route = useRoute()
 const router = useRouter()
@@ -134,8 +140,8 @@ onMounted(() => {
   loadAllResources()
 })
 
-watch(() => route.params.id, () => {
-  loadDetail()
+watch(() => route.params.id, (newVal) => {
+  if (newVal) loadDetail()
 })
 </script>
 

+ 6 - 0
frontend/src/views/project/ProjectList.vue

@@ -36,6 +36,9 @@
       </el-form>
 
       <el-table :data="list" v-loading="loading">
+        <el-table-column label="序号" width="60" header-align="right" align="right">
+          <template #default="{ $index }">{{ (query.pageNum - 1) * query.pageSize + $index + 1 }}</template>
+        </el-table-column>
         <el-table-column label="项目名称" min-width="160">
           <template #default="{ row }">
             <el-button link type="primary" @click="goDetail(row.id)">{{ row.name }}</el-button>
@@ -107,6 +110,9 @@ import { useRouter } from 'vue-router'
 import { ElMessage } from 'element-plus'
 import { projectPageApi, projectSaveApi, projectDeleteApi } from '@/api/project'
 import { userEnabledListApi } from '@/api/user'
+import { useScrollRestore } from '@/composables/useScrollRestore'
+
+useScrollRestore()
 
 const router = useRouter()
 const query = reactive({ pageNum: 1, pageSize: 10, name: '', status: null })

+ 6 - 0
frontend/src/views/resource/ResourceList.vue

@@ -49,6 +49,9 @@
       </el-form>
 
       <el-table :data="list" v-loading="loading">
+        <el-table-column label="序号" width="60" header-align="right" align="right">
+          <template #default="{ $index }">{{ (query.pageNum - 1) * query.pageSize + $index + 1 }}</template>
+        </el-table-column>
         <el-table-column prop="name" label="资源名称" min-width="150" />
         <el-table-column prop="typeName" label="类型" width="110" />
         <el-table-column prop="providerName" label="服务商" width="100" />
@@ -106,6 +109,9 @@ import { resourcePageApi, resourceDeleteApi, resourceExportApi } from '@/api/res
 import { resourceTypeListApi } from '@/api/dict'
 import { userEnabledListApi } from '@/api/user'
 import { resourceStatusClass } from '@/utils/status'
+import { useScrollRestore } from '@/composables/useScrollRestore'
+
+useScrollRestore()
 
 const query = reactive({ pageNum: 1, pageSize: 10, name: '', typeId: null, ownerId: null, status: null })
 const list = ref([])

+ 6 - 0
frontend/src/views/system/OperationLog.vue

@@ -25,6 +25,9 @@
       </el-form>
 
       <el-table :data="list" v-loading="loading">
+        <el-table-column label="序号" width="60" header-align="right" align="right">
+          <template #default="{ $index }">{{ (query.pageNum - 1) * query.pageSize + $index + 1 }}</template>
+        </el-table-column>
         <el-table-column prop="createTime" label="时间" width="180" class-name="mono-num" header-align="right" align="right" />
         <el-table-column prop="module" label="模块" width="100" />
         <el-table-column prop="action" label="操作" width="100" />
@@ -46,6 +49,9 @@
 <script setup>
 import { ref, reactive, onMounted } from 'vue'
 import { operationLogPageApi } from '@/api/log'
+import { useScrollRestore } from '@/composables/useScrollRestore'
+
+useScrollRestore()
 
 const query = reactive({ pageNum: 1, pageSize: 15, module: '', action: '' })
 const dateRange = ref(null)

+ 6 - 0
frontend/src/views/system/UserManage.vue

@@ -7,6 +7,9 @@
       </div>
 
       <el-table :data="list" v-loading="loading">
+        <el-table-column label="序号" width="60" header-align="right" align="right">
+          <template #default="{ $index }">{{ $index + 1 }}</template>
+        </el-table-column>
         <el-table-column prop="username" label="账号" width="140" />
         <el-table-column prop="realName" label="姓名" width="120" />
         <el-table-column prop="email" label="邮箱" min-width="180" />
@@ -70,6 +73,9 @@
 import { ref, reactive, onMounted } from 'vue'
 import { ElMessage } from 'element-plus'
 import { userListApi, userSaveApi, userDeleteApi } from '@/api/user'
+import { useScrollRestore } from '@/composables/useScrollRestore'
+
+useScrollRestore()
 
 const list = ref([])
 const loading = ref(false)