feat(layout): 多页签 keep-alive(默认缓存,meta.keepAlive:false 可关)

- 11 个菜单页面补 defineOptions name(缓存标识)
- tabs store 加 keepAlive 字段 + pathToName 辅助
- layout 用 keep-alive 包裹 router-view,按打开且未关闭缓存的页签缓存

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
liuyun 2026-10-11 22:13:18 +08:00
parent 5de298a956
commit ff0e1dcd27
14 changed files with 42 additions and 4 deletions

2
components.d.ts vendored
View File

@ -38,6 +38,7 @@ declare module 'vue' {
ElPagination: typeof import('element-plus/es')['ElPagination'] ElPagination: typeof import('element-plus/es')['ElPagination']
ElRadioButton: typeof import('element-plus/es')['ElRadioButton'] ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
ElResult: typeof import('element-plus/es')['ElResult']
ElScrollbar: typeof import('element-plus/es')['ElScrollbar'] ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
ElSelect: typeof import('element-plus/es')['ElSelect'] ElSelect: typeof import('element-plus/es')['ElSelect']
ElSlider: typeof import('element-plus/es')['ElSlider'] ElSlider: typeof import('element-plus/es')['ElSlider']
@ -98,6 +99,7 @@ declare global {
const ElPagination: typeof import('element-plus/es')['ElPagination'] const ElPagination: typeof import('element-plus/es')['ElPagination']
const ElRadioButton: typeof import('element-plus/es')['ElRadioButton'] const ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
const ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] const ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
const ElResult: typeof import('element-plus/es')['ElResult']
const ElScrollbar: typeof import('element-plus/es')['ElScrollbar'] const ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
const ElSelect: typeof import('element-plus/es')['ElSelect'] const ElSelect: typeof import('element-plus/es')['ElSelect']
const ElSlider: typeof import('element-plus/es')['ElSlider'] const ElSlider: typeof import('element-plus/es')['ElSlider']

View File

@ -57,7 +57,11 @@
@close-all="handleCloseAll" @close-all="handleCloseAll"
/> />
<div class="app-main"> <div class="app-main">
<router-view /> <router-view v-slot="{ Component, route }">
<keep-alive :include="cachedNames">
<component :is="Component" :key="route.path" />
</keep-alive>
</router-view>
</div> </div>
<ty-footer :fixed="app.footerFixed" /> <ty-footer :fixed="app.footerFixed" />
</main> </main>
@ -72,7 +76,7 @@ import { computed, onMounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { useAppStore } from '@/stores/modules/app' import { useAppStore } from '@/stores/modules/app'
import { HOME_PATH, useTabsStore } from '@/stores/modules/tabs' import { HOME_PATH, pathToName, useTabsStore } from '@/stores/modules/tabs'
import { useAuthStore } from '@/stores/modules/auth' import { useAuthStore } from '@/stores/modules/auth'
import { useMenuStore } from '@/stores/modules/menu' import { useMenuStore } from '@/stores/modules/menu'
import { useDictStore } from '@/stores/modules/dict' import { useDictStore } from '@/stores/modules/dict'
@ -110,6 +114,9 @@ onMounted(() => {
const activePath = computed(() => route.path) const activePath = computed(() => route.path)
/** keep-alive 缓存的组件名集合:打开且未关闭缓存的页签(keepAlive !== false 以兼容旧持久化数据)。 */
const cachedNames = computed(() => tabsStore.tabs.filter((t) => t.keepAlive !== false).map((t) => pathToName(t.path)))
/** 当前路由对应的顶级菜单(及其叶子)。 */ /** 当前路由对应的顶级菜单(及其叶子)。 */
const activeTop = computed<ResourceNode | null>(() => { const activeTop = computed<ResourceNode | null>(() => {
for (const m of menus.value) { for (const m of menus.value) {
@ -297,7 +304,7 @@ watch(
const top = activeTop.value const top = activeTop.value
const leaf = top?.children?.find((c) => (c.path ?? c.code) === p) const leaf = top?.children?.find((c) => (c.path ?? c.code) === p)
const title = leaf?.name ?? top?.name ?? p const title = leaf?.name ?? top?.name ?? p
tabsStore.addTab({ path: p, title, closable: p !== '/' }) tabsStore.addTab({ path: p, title, closable: p !== '/', keepAlive: route.meta.keepAlive !== false })
}, },
{ immediate: true }, { immediate: true },
) )

View File

@ -35,6 +35,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
defineOptions({ name: 'dashboard' })
import { computed, onBeforeUnmount, ref } from 'vue' import { computed, onBeforeUnmount, ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/modules/auth' import { useAuthStore } from '@/stores/modules/auth'

View File

@ -20,6 +20,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
defineOptions({ name: 'audit' })
import { computed, onMounted, reactive, ref, watch } from 'vue' import { computed, onMounted, reactive, ref, watch } from 'vue'
import type { JsonSchema, TableColumn } from '@/types/crud' import type { JsonSchema, TableColumn } from '@/types/crud'
import { useRequest } from '@/composables/useRequest' import { useRequest } from '@/composables/useRequest'

View File

@ -20,6 +20,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
defineOptions({ name: 'login-log' })
import { computed, onMounted, reactive, ref, watch } from 'vue' import { computed, onMounted, reactive, ref, watch } from 'vue'
import type { JsonSchema, TableColumn } from '@/types/crud' import type { JsonSchema, TableColumn } from '@/types/crud'
import { useRequest } from '@/composables/useRequest' import { useRequest } from '@/composables/useRequest'

View File

@ -26,6 +26,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
defineOptions({ name: 'session' })
import { computed, onMounted, reactive, ref, watch } from 'vue' import { computed, onMounted, reactive, ref, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import type { JsonSchema, TableColumn } from '@/types/crud' import type { JsonSchema, TableColumn } from '@/types/crud'

View File

@ -15,6 +15,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
defineOptions({ name: 'config' })
import type { JsonSchema, PageQuery, TableColumn } from '@/types/crud' import type { JsonSchema, PageQuery, TableColumn } from '@/types/crud'
import { createConfig, deleteConfig, listConfigs, updateConfig, type ConfigListQuery } from '@/api/config' import { createConfig, deleteConfig, listConfigs, updateConfig, type ConfigListQuery } from '@/api/config'

View File

@ -63,6 +63,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
defineOptions({ name: 'dict' })
import { computed, ref, watch } from 'vue' import { computed, ref, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import type { CrudAction, JsonSchema, PageQuery, TableColumn } from '@/types/crud' import type { CrudAction, JsonSchema, PageQuery, TableColumn } from '@/types/crud'

View File

@ -39,6 +39,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
defineOptions({ name: 'org' })
import { computed, reactive, ref } from 'vue' import { computed, reactive, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import type { JsonSchema, TableColumn } from '@/types/crud' import type { JsonSchema, TableColumn } from '@/types/crud'

View File

@ -39,6 +39,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
defineOptions({ name: 'resource' })
import { computed, reactive, ref } from 'vue' import { computed, reactive, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import type { JsonSchema, TableColumn } from '@/types/crud' import type { JsonSchema, TableColumn } from '@/types/crud'

View File

@ -16,6 +16,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
defineOptions({ name: 'role' })
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import type { CrudAction, JsonSchema, PageQuery, TableColumn } from '@/types/crud' import type { CrudAction, JsonSchema, PageQuery, TableColumn } from '@/types/crud'
import { showDialog } from '@/utils/dialog' import { showDialog } from '@/utils/dialog'

View File

@ -15,6 +15,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
defineOptions({ name: 'tenant' })
import type { JsonSchema, PageQuery, TableColumn } from '@/types/crud' import type { JsonSchema, PageQuery, TableColumn } from '@/types/crud'
import { createTenant, deleteTenant, listTenants, updateTenant, type TenantListQuery } from '@/api/tenant' import { createTenant, deleteTenant, listTenants, updateTenant, type TenantListQuery } from '@/api/tenant'

View File

@ -32,6 +32,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
defineOptions({ name: 'user' })
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import type { CrudAction, JsonSchema, PageQuery, TableColumn } from '@/types/crud' import type { CrudAction, JsonSchema, PageQuery, TableColumn } from '@/types/crud'

View File

@ -5,15 +5,22 @@ export interface TabItem {
path: string path: string
title: string title: string
closable: boolean closable: boolean
keepAlive: boolean
} }
export const HOME_PATH = '/' export const HOME_PATH = '/'
/** 由路径推导 keep-alive 组件名:'/'→dashboard,其余取路径末段 kebab。 */
export function pathToName(path: string): string {
if (path === '/') return 'dashboard'
return path.split('/').filter(Boolean).pop() ?? ''
}
/** 多页签状态(持久化,刷新保留)。 */ /** 多页签状态(持久化,刷新保留)。 */
export const useTabsStore = defineStore( export const useTabsStore = defineStore(
'tabsStore', 'tabsStore',
() => { () => {
const tabs = ref<TabItem[]>([{ path: HOME_PATH, title: '首页', closable: false }]) const tabs = ref<TabItem[]>([{ path: HOME_PATH, title: '首页', closable: false, keepAlive: true }])
function addTab(tab: TabItem): void { function addTab(tab: TabItem): void {
if (!tabs.value.some((t) => t.path === tab.path)) { if (!tabs.value.some((t) => t.path === tab.path)) {