菜单多语言配置
This commit is contained in:
@@ -17,7 +17,7 @@
|
|||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="block max-w-46 overflow-hidden text-ellipsis whitespace-nowrap px-1.5 text-sm text-g-600 dark:text-g-800"
|
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>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
@click="searchGoPage(item)"
|
@click="searchGoPage(item)"
|
||||||
@mouseenter="highlightOnHover(index)"
|
@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" />
|
<ArtSvgIcon v-show="isHighlighted(index)" icon="fluent:arrow-enter-left-20-filled" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -60,7 +60,7 @@
|
|||||||
@click="searchGoPage(item)"
|
@click="searchGoPage(item)"
|
||||||
@mouseenter="highlightOnHoverHistory(index)"
|
@mouseenter="highlightOnHoverHistory(index)"
|
||||||
>
|
>
|
||||||
{{ formatMenuTitle(item.meta.title) }}
|
{{ formatMenuTitle(item.meta.title, item.path) }}
|
||||||
<div
|
<div
|
||||||
class="size-5 selected-icon select-none rounded-full text-g-500 flex-cc c-p"
|
class="size-5 selected-icon select-none rounded-full text-g-500 flex-cc c-p"
|
||||||
@click.stop="deleteHistory(index)"
|
@click.stop="deleteHistory(index)"
|
||||||
@@ -182,7 +182,7 @@
|
|||||||
const flattenAndMatch = (item: AppRouteRecord) => {
|
const flattenAndMatch = (item: AppRouteRecord) => {
|
||||||
if (item.meta?.isHide) return
|
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) {
|
if (item.children && item.children.length > 0) {
|
||||||
item.children.forEach(flattenAndMatch)
|
item.children.forEach(flattenAndMatch)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<ElSubMenu v-if="hasChildren" :index="item.path || item.meta.title" class="!p-0">
|
<ElSubMenu v-if="hasChildren" :index="item.path || item.meta.title" class="!p-0">
|
||||||
<template #title>
|
<template #title>
|
||||||
<ArtSvgIcon :icon="item.meta.icon" :color="theme?.iconColor" class="mr-1 text-lg" />
|
<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.showBadge" class="art-badge art-badge-horizontal" />
|
||||||
<div v-if="item.meta.showTextBadge" class="art-text-badge">
|
<div v-if="item.meta.showTextBadge" class="art-text-badge">
|
||||||
{{ item.meta.showTextBadge }}
|
{{ item.meta.showTextBadge }}
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
class="mr-1 text-lg"
|
class="mr-1 text-lg"
|
||||||
:style="{ color: theme.iconColor }"
|
: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
|
<div
|
||||||
v-if="item.meta.showBadge"
|
v-if="item.meta.showBadge"
|
||||||
class="art-badge"
|
class="art-badge"
|
||||||
|
|||||||
@@ -135,7 +135,7 @@
|
|||||||
return props.list.map((item) => ({
|
return props.list.map((item) => ({
|
||||||
...item,
|
...item,
|
||||||
isActive: isMenuItemActive(item),
|
isActive: isMenuItemActive(item),
|
||||||
formattedTitle: formatMenuTitle(item.meta.title)
|
formattedTitle: formatMenuTitle(item.meta.title, item.path)
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
<ElTooltip
|
<ElTooltip
|
||||||
class="box-item"
|
class="box-item"
|
||||||
effect="dark"
|
effect="dark"
|
||||||
:content="$t(menu.meta.title)"
|
:content="formatMenuTitle(menu.meta.title, menu.path)"
|
||||||
placement="right"
|
placement="right"
|
||||||
:offset="15"
|
:offset="15"
|
||||||
:hide-after="0"
|
:hide-after="0"
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
<span v-if="dualMenuShowText" class="text-md text-g-700">
|
<span v-if="dualMenuShowText" class="text-md text-g-700">
|
||||||
{{ $t(menu.meta.title) }}
|
{{ formatMenuTitle(menu.meta.title, menu.path) }}
|
||||||
</span>
|
</span>
|
||||||
<div v-if="menu.meta.showBadge" class="art-badge art-badge-dual" />
|
<div v-if="menu.meta.showBadge" class="art-badge art-badge-dual" />
|
||||||
</div>
|
</div>
|
||||||
@@ -136,6 +136,7 @@
|
|||||||
import { useMenuStore } from '@/store/modules/menu'
|
import { useMenuStore } from '@/store/modules/menu'
|
||||||
import { isIframe } from '@/utils/navigation'
|
import { isIframe } from '@/utils/navigation'
|
||||||
import { handleMenuJump } from '@/utils/navigation'
|
import { handleMenuJump } from '@/utils/navigation'
|
||||||
|
import { formatMenuTitle } from '@/utils/router'
|
||||||
import SidebarSubmenu from './widget/SidebarSubmenu.vue'
|
import SidebarSubmenu from './widget/SidebarSubmenu.vue'
|
||||||
import { useCommon } from '@/hooks/core/useCommon'
|
import { useCommon } from '@/hooks/core/useCommon'
|
||||||
import { useWindowSize, useTimeoutFn } from '@vueuse/core'
|
import { useWindowSize, useTimeoutFn } from '@vueuse/core'
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span class="menu-name">
|
<span class="menu-name">
|
||||||
{{ formatMenuTitle(item.meta.title) }}
|
{{ formatMenuTitle(item.meta.title, item.path) }}
|
||||||
</span>
|
</span>
|
||||||
<div v-if="item.meta.showBadge" class="art-badge" style="right: 10px" />
|
<div v-if="item.meta.showBadge" class="art-badge" style="right: 10px" />
|
||||||
</template>
|
</template>
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
|
|
||||||
<template #title>
|
<template #title>
|
||||||
<span class="menu-name">
|
<span class="menu-name">
|
||||||
{{ formatMenuTitle(item.meta.title) }}
|
{{ formatMenuTitle(item.meta.title, item.path) }}
|
||||||
</span>
|
</span>
|
||||||
<div v-if="item.meta.showBadge" class="art-badge" />
|
<div v-if="item.meta.showBadge" class="art-badge" />
|
||||||
<div v-if="item.meta.showTextBadge && (level > 0 || menuOpen)" class="art-text-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="text-base mr-1 group-hover:text-theme"
|
||||||
:class="item.path === activeTab ? 'text-theme' : 'text-g-600'"
|
:class="item.path === activeTab ? 'text-theme' : 'text-g-600'"
|
||||||
/>
|
/>
|
||||||
{{ item.customTitle || formatMenuTitle(item.title) }}
|
{{ item.customTitle || formatMenuTitle(item.title, item.path) }}
|
||||||
<span
|
<span
|
||||||
v-if="list.length > 1 && !item.fixedTab"
|
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"
|
class="inline-flex flex-cc relative ml-0.5 p-1 rounded-full tad-200 hover:bg-g-200"
|
||||||
|
|||||||
@@ -245,7 +245,11 @@
|
|||||||
},
|
},
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
"title": "Dashboard",
|
"title": "Dashboard",
|
||||||
"console": "Console"
|
"console": "Console",
|
||||||
|
"userCenter": "User Center"
|
||||||
|
},
|
||||||
|
"userCenter": {
|
||||||
|
"title": "User Center"
|
||||||
},
|
},
|
||||||
"result": {
|
"result": {
|
||||||
"title": "Result Page",
|
"title": "Result Page",
|
||||||
@@ -259,11 +263,43 @@
|
|||||||
"serverError": "500"
|
"serverError": "500"
|
||||||
},
|
},
|
||||||
"system": {
|
"system": {
|
||||||
"title": "System Settings",
|
"title": "System Management",
|
||||||
"user": "User Manage",
|
"user": "User Management",
|
||||||
"role": "Role Manage",
|
"role": "Role Management",
|
||||||
"userCenter": "User Center",
|
"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": {
|
"table": {
|
||||||
|
|||||||
@@ -11,6 +11,62 @@ import NProgress from 'nprogress'
|
|||||||
import 'nprogress/nprogress.css'
|
import 'nprogress/nprogress.css'
|
||||||
import i18n, { $t } from '@/locales'
|
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 & {
|
export type AppRouteRecordRaw = RouteRecordRaw & {
|
||||||
hidden?: boolean
|
hidden?: boolean
|
||||||
@@ -34,28 +90,42 @@ export const setPageTitle = (to: RouteLocationNormalized): void => {
|
|||||||
const { title } = to.meta
|
const { title } = to.meta
|
||||||
if (title) {
|
if (title) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
document.title = `${formatMenuTitle(String(title))} - ${AppConfig.systemInfo.name}`
|
document.title = `${formatMenuTitle(String(title), to.path)} - ${AppConfig.systemInfo.name}`
|
||||||
}, 150)
|
}, 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 格式化后的菜单标题
|
* @returns 格式化后的菜单标题
|
||||||
*/
|
*/
|
||||||
export const formatMenuTitle = (title: string): string => {
|
export const formatMenuTitle = (title: string, path?: string): string => {
|
||||||
if (title) {
|
if (!title) return ''
|
||||||
|
|
||||||
if (title.startsWith('menus.')) {
|
if (title.startsWith('menus.')) {
|
||||||
// 使用 te() 方法检查翻译键值是否存在,避免控制台警告
|
|
||||||
if (i18n.global.te(title)) {
|
if (i18n.global.te(title)) {
|
||||||
return $t(title)
|
return $t(title)
|
||||||
} else {
|
}
|
||||||
// 如果翻译不存在,返回键值的最后部分作为fallback
|
|
||||||
return title.split('.').pop() || title
|
return title.split('.').pop() || title
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (path) {
|
||||||
|
const i18nKey = getMenuI18nKeyByPath(path)
|
||||||
|
if (i18nKey && i18n.global.te(i18nKey)) {
|
||||||
|
return $t(i18nKey)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return title
|
return title
|
||||||
}
|
|
||||||
return ''
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user