feat(layout): 菜单支持国际化
All checks were successful
CI/CD Pipeline / playwright (push) Successful in 3m42s
CI/CD Pipeline / build-and-deploy (push) Successful in 4m34s

This commit is contained in:
严浩
2025-10-23 18:15:45 +08:00
parent cec32dceb2
commit 93ecda7617
13 changed files with 76 additions and 23 deletions

View File

@@ -3,7 +3,25 @@ import { createGetRoutes, router } from '@/plugins/router-plugin';
import type { MenuOption } from 'naive-ui'; import type { MenuOption } from 'naive-ui';
import { RouterLink, type RouteRecordRaw } from 'vue-router'; import { RouterLink, type RouteRecordRaw } from 'vue-router';
import IconMenuRounded from '~icons/material-symbols/menu-rounded'; import IconMenuRounded from '~icons/material-symbols/menu-rounded';
import { useAppStore } from '../../stores/app-store'; import { useAppStore } from '@/stores/app-store';
import zhCN from '@/pages/_page-title-locales/zh-CN';
import enUS from '@/pages/_page-title-locales/en-US';
const { t, te } = useI18n({
inheritLocale: true,
useScope: 'local',
missing: (locale, key) => {
consola.warn(`菜单翻译缺失: locale=${locale}, key=${key}`);
return key;
},
fallbackRoot: true,
messages: {
'zh-CN': zhCN,
'en-US': enUS,
},
});
// 路由转换为菜单树的辅助函数 // 路由转换为菜单树的辅助函数
function convertRoutesToMenuOptions(routes: Readonly<RouteRecordRaw[]>): MenuOption[] { function convertRoutesToMenuOptions(routes: Readonly<RouteRecordRaw[]>): MenuOption[] {
const menuMap = new Map<string, MenuOption>(); const menuMap = new Map<string, MenuOption>();
@@ -31,7 +49,9 @@ function convertRoutesToMenuOptions(routes: Readonly<RouteRecordRaw[]>): MenuOpt
// 构建菜单树 // 构建菜单树
for (const route of validRoutes) { for (const route of validRoutes) {
const pathSegments = route.path.split('/').filter(Boolean); const pathSegments = route.path.split('/').filter(Boolean);
const text = route.meta?.title || route.name; const routeName = route.name as string;
const text = te(routeName) ? t(routeName) : route.meta?.title || routeName;
const menuOption: MenuOption = { const menuOption: MenuOption = {
label: () => (route.meta?.link === false ? text : <RouterLink to={route}>{text}</RouterLink>), label: () => (route.meta?.link === false ? text : <RouterLink to={route}>{text}</RouterLink>),
key: route.path, key: route.path,
@@ -72,8 +92,8 @@ function convertRoutesToMenuOptions(routes: Readonly<RouteRecordRaw[]>): MenuOpt
const routes = createGetRoutes(router)(); const routes = createGetRoutes(router)();
const menuOptions = computed(() => convertRoutesToMenuOptions(routes)); const menuOptions = computed(() => convertRoutesToMenuOptions(routes));
console.debug('原始路由:', JSON.stringify(routes, null, 0)); // console.debug('原始路由:', JSON.stringify(routes, null, 0));
console.debug('转换后的菜单:', JSON.stringify(menuOptions.value, null, 0)); // console.debug('转换后的菜单:', JSON.stringify(menuOptions.value, null, 0));
const menuInstRef = useTemplateRef('menuInstRef'); const menuInstRef = useTemplateRef('menuInstRef');
const selectedKey = ref(''); const selectedKey = ref('');

View File

@@ -7,7 +7,12 @@ const appStore = useAppStore();
</script> </script>
<template> <template>
<AdminLayout :is-mobile="appStore.isMobile" v-model:sider-collapse="appStore.sidebarCollapsed"> <AdminLayout
:footer-visible="!false"
:tab-visible="false"
:is-mobile="appStore.isMobile"
v-model:sider-collapse="appStore.sidebarCollapsed"
>
<template #header> <template #header>
<BaseLayoutHeader /> <BaseLayoutHeader />
</template> </template>
@@ -19,9 +24,6 @@ const appStore = useAppStore();
<template #sider> <template #sider>
<BaseLayoutSider /> <BaseLayoutSider />
</template> </template>
<div class="bg-yellow-100/28 dark:bg-yellow-900/28 text-yellow-900 dark:text-yellow-100 p-4">
4#GlobalMenu
</div>
<!-- <div>GlobalContent</div> --> <!-- <div>GlobalContent</div> -->
<router-view v-slot="{ Component }"> <router-view v-slot="{ Component }">
<transition name="fade" mode="out-in"> <transition name="fade" mode="out-in">
@@ -30,8 +32,10 @@ const appStore = useAppStore();
</router-view> </router-view>
<!-- <div>ThemeDrawer</div> --> <!-- <div>ThemeDrawer</div> -->
<template #footer> <template #footer>
<div class="bg-red-100/28 dark:bg-red-900/28 text-red-900 dark:text-red-100 h-full"> <div
5#GlobalFooter class="bg-red-100/28 dark:bg-red-900/28 text-red-900 dark:text-red-100 h-full flex items-center justify-center"
>
GlobalFooter
</div> </div>
</template> </template>
</AdminLayout> </AdminLayout>

View File

@@ -0,0 +1,12 @@
import type { RouteLocalizationFlags } from '@/plugins/router-plugin';
export default {
Root: 'Root',
$Path: '$Path',
Demos: 'Demos',
DemosApiDemo: 'API Demo',
DemosCounterDemo: 'Counter Demo',
DemosI18nDemo: 'i18n Demo',
DemosWebsocketDemo: 'WebSocket Demo',
Home: 'Home',
} satisfies RouteLocalizationFlags;

View File

@@ -0,0 +1,12 @@
import type { RouteLocalizationFlags } from '@/plugins/router-plugin';
export default {
Root: '根 (Gēn)',
$Path: '$Path',
Demos: '示例演示',
DemosApiDemo: 'API 调用示例',
DemosCounterDemo: '点击计数器',
DemosI18nDemo: '国际化示例',
DemosWebsocketDemo: 'WebSocket 示例',
Home: '首页',
} satisfies RouteLocalizationFlags;

View File

@@ -2,9 +2,7 @@
import { ref } from 'vue'; import { ref } from 'vue';
definePage({ definePage({
meta: { meta: {},
title: 'API 调用示例',
},
}); });
// ========== API 模块 ========== // ========== API 模块 ==========

View File

@@ -3,9 +3,7 @@ import { ref } from 'vue';
import { NButton } from 'naive-ui'; import { NButton } from 'naive-ui';
definePage({ definePage({
meta: { meta: {},
title: '点击计数器',
},
}); });
// ========== 计数器模块 ========== // ========== 计数器模块 ==========

View File

@@ -1,8 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
definePage({ definePage({
meta: { meta: {},
title: '国际化示例',
},
}); });
const { t, locale } = useI18n(); const { t, locale } = useI18n();

View File

@@ -1,4 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
definePage({ meta: { title: '示例演示', link: false } }); definePage({ meta: { link: false } });
</script> </script>
<template><div>此页面文件仅用于在侧边栏菜单中显示示例演示分组标题</div></template> <template><div>此页面文件仅用于在侧边栏菜单中显示示例演示分组标题</div></template>

View File

@@ -2,9 +2,7 @@
import { ref, onUnmounted, computed, nextTick } from 'vue'; import { ref, onUnmounted, computed, nextTick } from 'vue';
definePage({ definePage({
meta: { meta: {},
title: 'WebSocket 示例',
},
}); });
// ========== WebSocket 模块 ========== // ========== WebSocket 模块 ==========

View File

@@ -2,6 +2,7 @@ import { DataLoaderPlugin } from 'unplugin-vue-router/data-loaders';
import { setupLayouts } from 'virtual:meta-layouts'; import { setupLayouts } from 'virtual:meta-layouts';
// import { createGetRoutes, setupLayouts } from 'virtual:generated-layouts'; // import { createGetRoutes, setupLayouts } from 'virtual:generated-layouts';
import { createRouter, createWebHistory } from 'vue-router'; import { createRouter, createWebHistory } from 'vue-router';
import type { RouteNamedMap } from 'vue-router/auto-routes';
import { routes, handleHotUpdate } from 'vue-router/auto-routes'; import { routes, handleHotUpdate } from 'vue-router/auto-routes';
const setupLayoutsResult = setupLayouts(routes); const setupLayoutsResult = setupLayouts(routes);
@@ -63,6 +64,7 @@ declare module 'vue-router' {
export { router, setupLayoutsResult }; export { router, setupLayoutsResult };
export { createGetRoutes } from 'virtual:meta-layouts'; export { createGetRoutes } from 'virtual:meta-layouts';
export type RouteLocalizationFlags = Record<keyof RouteNamedMap, string>;
if (__DEV__) Object.assign(globalThis, { router }); if (__DEV__) Object.assign(globalThis, { router });
// This will update routes at runtime without reloading the page // This will update routes at runtime without reloading the page

View File

@@ -11,6 +11,14 @@ export function install({ app }: { app: import('vue').App<Element> }) {
createI18n({ createI18n({
legacy: false, // you must set `false`, to use Composition API legacy: false, // you must set `false`, to use Composition API
locale: navigator.language, locale: navigator.language,
fallbackRoot: false,
// flatJson: true,
missing: (locale, key /* , instance, type */) => {
consola.warn(`缺少国际化内容: locale='${locale}', key='${key}'`);
return `[${key}]`;
},
missingWarn: !true,
fallbackWarn: !true,
messages, messages,
}), }),
); );

View File

@@ -138,6 +138,9 @@ export default defineConfig(async (configEnv) => {
if (packageName) { if (packageName) {
// 根据包名分组 // 根据包名分组
if (packageName.includes('consola')) {
return 'consola';
}
if (packageName.includes('naive-ui')) { if (packageName.includes('naive-ui')) {
return 'naive-ui'; return 'naive-ui';
} }