菜单多语言配置
This commit is contained in:
@@ -17,7 +17,7 @@
|
||||
>
|
||||
<span
|
||||
class="block max-w-46 overflow-hidden text-ellipsis whitespace-nowrap px-1.5 text-sm text-g-600 dark:text-g-800"
|
||||
>{{ formatMenuTitle(item.meta?.title as string) }}</span
|
||||
>{{ formatMenuTitle(item.meta?.title as string, item.path) }}</span
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
@click="searchGoPage(item)"
|
||||
@mouseenter="highlightOnHover(index)"
|
||||
>
|
||||
{{ formatMenuTitle(item.meta.title) }}
|
||||
{{ formatMenuTitle(item.meta.title, item.path) }}
|
||||
<ArtSvgIcon v-show="isHighlighted(index)" icon="fluent:arrow-enter-left-20-filled" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -60,7 +60,7 @@
|
||||
@click="searchGoPage(item)"
|
||||
@mouseenter="highlightOnHoverHistory(index)"
|
||||
>
|
||||
{{ formatMenuTitle(item.meta.title) }}
|
||||
{{ formatMenuTitle(item.meta.title, item.path) }}
|
||||
<div
|
||||
class="size-5 selected-icon select-none rounded-full text-g-500 flex-cc c-p"
|
||||
@click.stop="deleteHistory(index)"
|
||||
@@ -182,7 +182,7 @@
|
||||
const flattenAndMatch = (item: AppRouteRecord) => {
|
||||
if (item.meta?.isHide) return
|
||||
|
||||
const lowerItemTitle = formatMenuTitle(item.meta.title).toLowerCase()
|
||||
const lowerItemTitle = formatMenuTitle(item.meta.title, item.path).toLowerCase()
|
||||
|
||||
if (item.children && item.children.length > 0) {
|
||||
item.children.forEach(flattenAndMatch)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<ElSubMenu v-if="hasChildren" :index="item.path || item.meta.title" class="!p-0">
|
||||
<template #title>
|
||||
<ArtSvgIcon :icon="item.meta.icon" :color="theme?.iconColor" class="mr-1 text-lg" />
|
||||
<span class="text-md">{{ formatMenuTitle(item.meta.title) }}</span>
|
||||
<span class="text-md">{{ formatMenuTitle(item.meta.title, item.path) }}</span>
|
||||
<div v-if="item.meta.showBadge" class="art-badge art-badge-horizontal" />
|
||||
<div v-if="item.meta.showTextBadge" class="art-text-badge">
|
||||
{{ item.meta.showTextBadge }}
|
||||
@@ -32,7 +32,7 @@
|
||||
class="mr-1 text-lg"
|
||||
:style="{ color: theme.iconColor }"
|
||||
/>
|
||||
<span class="text-md">{{ formatMenuTitle(item.meta.title) }}</span>
|
||||
<span class="text-md">{{ formatMenuTitle(item.meta.title, item.path) }}</span>
|
||||
<div
|
||||
v-if="item.meta.showBadge"
|
||||
class="art-badge"
|
||||
|
||||
@@ -135,7 +135,7 @@
|
||||
return props.list.map((item) => ({
|
||||
...item,
|
||||
isActive: isMenuItemActive(item),
|
||||
formattedTitle: formatMenuTitle(item.meta.title)
|
||||
formattedTitle: formatMenuTitle(item.meta.title, item.path)
|
||||
}))
|
||||
})
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<ElTooltip
|
||||
class="box-item"
|
||||
effect="dark"
|
||||
:content="$t(menu.meta.title)"
|
||||
:content="formatMenuTitle(menu.meta.title, menu.path)"
|
||||
placement="right"
|
||||
:offset="15"
|
||||
:hide-after="0"
|
||||
@@ -43,7 +43,7 @@
|
||||
}"
|
||||
/>
|
||||
<span v-if="dualMenuShowText" class="text-md text-g-700">
|
||||
{{ $t(menu.meta.title) }}
|
||||
{{ formatMenuTitle(menu.meta.title, menu.path) }}
|
||||
</span>
|
||||
<div v-if="menu.meta.showBadge" class="art-badge art-badge-dual" />
|
||||
</div>
|
||||
@@ -136,6 +136,7 @@
|
||||
import { useMenuStore } from '@/store/modules/menu'
|
||||
import { isIframe } from '@/utils/navigation'
|
||||
import { handleMenuJump } from '@/utils/navigation'
|
||||
import { formatMenuTitle } from '@/utils/router'
|
||||
import SidebarSubmenu from './widget/SidebarSubmenu.vue'
|
||||
import { useCommon } from '@/hooks/core/useCommon'
|
||||
import { useWindowSize, useTimeoutFn } from '@vueuse/core'
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
/>
|
||||
</div>
|
||||
<span class="menu-name">
|
||||
{{ formatMenuTitle(item.meta.title) }}
|
||||
{{ formatMenuTitle(item.meta.title, item.path) }}
|
||||
</span>
|
||||
<div v-if="item.meta.showBadge" class="art-badge" style="right: 10px" />
|
||||
</template>
|
||||
@@ -45,7 +45,7 @@
|
||||
|
||||
<template #title>
|
||||
<span class="menu-name">
|
||||
{{ formatMenuTitle(item.meta.title) }}
|
||||
{{ formatMenuTitle(item.meta.title, item.path) }}
|
||||
</span>
|
||||
<div v-if="item.meta.showBadge" class="art-badge" />
|
||||
<div v-if="item.meta.showTextBadge && (level > 0 || menuOpen)" class="art-text-badge">
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
class="text-base mr-1 group-hover:text-theme"
|
||||
:class="item.path === activeTab ? 'text-theme' : 'text-g-600'"
|
||||
/>
|
||||
{{ item.customTitle || formatMenuTitle(item.title) }}
|
||||
{{ item.customTitle || formatMenuTitle(item.title, item.path) }}
|
||||
<span
|
||||
v-if="list.length > 1 && !item.fixedTab"
|
||||
class="inline-flex flex-cc relative ml-0.5 p-1 rounded-full tad-200 hover:bg-g-200"
|
||||
|
||||
@@ -245,7 +245,11 @@
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Dashboard",
|
||||
"console": "Console"
|
||||
"console": "Console",
|
||||
"userCenter": "User Center"
|
||||
},
|
||||
"userCenter": {
|
||||
"title": "User Center"
|
||||
},
|
||||
"result": {
|
||||
"title": "Result Page",
|
||||
@@ -259,11 +263,43 @@
|
||||
"serverError": "500"
|
||||
},
|
||||
"system": {
|
||||
"title": "System Settings",
|
||||
"user": "User Manage",
|
||||
"role": "Role Manage",
|
||||
"title": "System Management",
|
||||
"user": "User Management",
|
||||
"role": "Role Management",
|
||||
"userCenter": "User Center",
|
||||
"menu": "Menu Manage"
|
||||
"menu": "Menu Management",
|
||||
"dept": "Department Management",
|
||||
"post": "Post Management",
|
||||
"config": "System Config"
|
||||
},
|
||||
"safeguard": {
|
||||
"title": "Operations Management",
|
||||
"dict": "Data Dictionary",
|
||||
"server": "Server Monitor",
|
||||
"operLog": "Operation Log",
|
||||
"loginLog": "Login Log",
|
||||
"emailLog": "Email Log",
|
||||
"database": "Database",
|
||||
"cache": "Cache Management",
|
||||
"attachment": "Attachment"
|
||||
},
|
||||
"tool": {
|
||||
"title": "Development Tools",
|
||||
"crontab": "Crontab",
|
||||
"code": "Code Generator"
|
||||
},
|
||||
"dice": {
|
||||
"title": "Dice Game",
|
||||
"lotteryPoolConfig": "Lottery Tier Weight Config",
|
||||
"player": "Player Management",
|
||||
"playerWalletRecord": "Player Wallet Records",
|
||||
"playRecord": "Player Draw Records",
|
||||
"playerTicketRecord": "Player Ticket Records",
|
||||
"rewardConfig": "Reward Config",
|
||||
"reward": "Dice Point Weight Config",
|
||||
"rewardConfigRecord": "Dice Weight Test Records",
|
||||
"playRecordTest": "Draw Records (Test Weight)",
|
||||
"config": "Game Config"
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
|
||||
@@ -11,6 +11,62 @@ import NProgress from 'nprogress'
|
||||
import 'nprogress/nprogress.css'
|
||||
import i18n, { $t } from '@/locales'
|
||||
|
||||
/**
|
||||
* 路径到菜单 i18n key 的映射
|
||||
* 当后端返回的菜单名为中文或非 i18n key 时,根据 path 仍可显示多语言
|
||||
*/
|
||||
export const MAP_PATH_TO_MENU_I18N_KEY: Record<string, string> = {
|
||||
'/dashboard': 'menus.dashboard.title',
|
||||
'/dashboard/console': 'menus.dashboard.console',
|
||||
'/dashboard/user-center': 'menus.userCenter.title',
|
||||
'/system': 'menus.system.title',
|
||||
'/system/user': 'menus.system.user',
|
||||
'/system/role': 'menus.system.role',
|
||||
'/system/user-center': 'menus.system.userCenter',
|
||||
'/system/menu': 'menus.system.menu',
|
||||
'/system/dept': 'menus.system.dept',
|
||||
'/system/post': 'menus.system.post',
|
||||
'/system/config': 'menus.system.config',
|
||||
'/safeguard': 'menus.safeguard.title',
|
||||
'/safeguard/dict': 'menus.safeguard.dict',
|
||||
'/safeguard/server': 'menus.safeguard.server',
|
||||
'/safeguard/oper-log': 'menus.safeguard.operLog',
|
||||
'/safeguard/login-log': 'menus.safeguard.loginLog',
|
||||
'/safeguard/email-log': 'menus.safeguard.emailLog',
|
||||
'/safeguard/database': 'menus.safeguard.database',
|
||||
'/safeguard/cache': 'menus.safeguard.cache',
|
||||
'/safeguard/attachment': 'menus.safeguard.attachment',
|
||||
'/tool': 'menus.tool.title',
|
||||
'/tool/crontab': 'menus.tool.crontab',
|
||||
'/tool/code': 'menus.tool.code',
|
||||
'/dice': 'menus.dice.title',
|
||||
'/dice/lottery_pool_config': 'menus.dice.lotteryPoolConfig',
|
||||
'/dice/lottery_pool_config/index': 'menus.dice.lotteryPoolConfig',
|
||||
'/dice/player': 'menus.dice.player',
|
||||
'/dice/player/index': 'menus.dice.player',
|
||||
'/dice/player_wallet_record': 'menus.dice.playerWalletRecord',
|
||||
'/dice/player_wallet_record/index': 'menus.dice.playerWalletRecord',
|
||||
'/dice/play_record': 'menus.dice.playRecord',
|
||||
'/dice/play_record/index': 'menus.dice.playRecord',
|
||||
'/dice/player_ticket_record': 'menus.dice.playerTicketRecord',
|
||||
'/dice/player_ticket_record/index': 'menus.dice.playerTicketRecord',
|
||||
'/dice/reward_config': 'menus.dice.rewardConfig',
|
||||
'/dice/reward_config/index': 'menus.dice.rewardConfig',
|
||||
'/dice/reward': 'menus.dice.reward',
|
||||
'/dice/reward/index': 'menus.dice.reward',
|
||||
'/dice/reward_config_record': 'menus.dice.rewardConfigRecord',
|
||||
'/dice/reward_config_record/index': 'menus.dice.rewardConfigRecord',
|
||||
'/dice/play_record_test': 'menus.dice.playRecordTest',
|
||||
'/dice/play_record_test/index': 'menus.dice.playRecordTest',
|
||||
'/dice/config': 'menus.dice.config',
|
||||
'/dice/config/index': 'menus.dice.config',
|
||||
'/result/success': 'menus.result.success',
|
||||
'/result/fail': 'menus.result.fail',
|
||||
'/exception/403': 'menus.exception.forbidden',
|
||||
'/exception/404': 'menus.exception.notFound',
|
||||
'/exception/500': 'menus.exception.serverError'
|
||||
}
|
||||
|
||||
/** 扩展的路由配置类型 */
|
||||
export type AppRouteRecordRaw = RouteRecordRaw & {
|
||||
hidden?: boolean
|
||||
@@ -34,28 +90,42 @@ export const setPageTitle = (to: RouteLocationNormalized): void => {
|
||||
const { title } = to.meta
|
||||
if (title) {
|
||||
setTimeout(() => {
|
||||
document.title = `${formatMenuTitle(String(title))} - ${AppConfig.systemInfo.name}`
|
||||
document.title = `${formatMenuTitle(String(title), to.path)} - ${AppConfig.systemInfo.name}`
|
||||
}, 150)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据路径获取对应的菜单 i18n key(若有)
|
||||
*/
|
||||
export const getMenuI18nKeyByPath = (path: string): string | undefined => {
|
||||
if (!path) return undefined
|
||||
const normalized = path.replace(/\?.*$/, '').replace(/#.*$/, '').replace(/\/$/, '') || '/'
|
||||
return MAP_PATH_TO_MENU_I18N_KEY[normalized]
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化菜单标题
|
||||
* @param title 菜单标题,可以是 i18n 的 key,也可以是字符串
|
||||
* @param title 菜单标题,可以是 i18n 的 key(如 menus.dashboard.title),也可以是中文等纯文本
|
||||
* @param path 可选,当前菜单路由 path;当 title 非 i18n key 时,用 path 查表得到 key 再翻译,以实现多语言切换
|
||||
* @returns 格式化后的菜单标题
|
||||
*/
|
||||
export const formatMenuTitle = (title: string): string => {
|
||||
if (title) {
|
||||
export const formatMenuTitle = (title: string, path?: string): string => {
|
||||
if (!title) return ''
|
||||
|
||||
if (title.startsWith('menus.')) {
|
||||
// 使用 te() 方法检查翻译键值是否存在,避免控制台警告
|
||||
if (i18n.global.te(title)) {
|
||||
return $t(title)
|
||||
} else {
|
||||
// 如果翻译不存在,返回键值的最后部分作为fallback
|
||||
}
|
||||
return title.split('.').pop() || title
|
||||
}
|
||||
|
||||
if (path) {
|
||||
const i18nKey = getMenuI18nKeyByPath(path)
|
||||
if (i18nKey && i18n.global.te(i18nKey)) {
|
||||
return $t(i18nKey)
|
||||
}
|
||||
}
|
||||
|
||||
return title
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user