添加国际化支持:引入 vue-i18n,更新多个组件以支持语言切换和文本翻译

This commit is contained in:
jxxghp
2025-04-27 17:44:09 +08:00
parent 80ae853582
commit d0b3bc8137
27 changed files with 973 additions and 374 deletions

View File

@@ -3,18 +3,20 @@ import VerticalNavSectionTitle from '@/@layouts/components/VerticalNavSectionTit
import VerticalNavLayout from '@layouts/components/VerticalNavLayout.vue'
import VerticalNavLink from '@layouts/components/VerticalNavLink.vue'
import Footer from '@/layouts/components/Footer.vue'
import NavbarThemeSwitcher from '@/layouts/components/NavbarThemeSwitcher.vue'
import NavbarActions from '@/layouts/components/NavbarActions.vue'
import UserNofification from '@/layouts/components/UserNotification.vue'
import SearchBar from '@/layouts/components/SearchBar.vue'
import ShortcutBar from '@/layouts/components/ShortcutBar.vue'
import UserProfile from '@/layouts/components/UserProfile.vue'
import { useUserStore } from '@/stores'
import { SystemNavMenus } from '@/router/menu'
import { getNavMenus } from '@/router/i18n-menu'
import { NavMenu } from '@/@layouts/types'
import { useDisplay } from 'vuetify'
import { useI18n } from 'vue-i18n'
const display = useDisplay()
const appMode = inject('pwaMode')
const { t } = useI18n()
// 用户 Store
const userStore = useUserStore()
@@ -39,7 +41,9 @@ const systemMenus = ref<NavMenu[]>([])
// 根据分类获取菜单列表
const getMenuList = (header: string) => {
return SystemNavMenus.filter((item: NavMenu) => item.header === header && (superUser || !item.admin))
// 使用国际化菜单
const menus = getNavMenus()
return menus.filter((item: NavMenu) => item.header === header && (superUser || !item.admin))
}
// 返回上一页
@@ -49,11 +53,11 @@ function goBack() {
onMounted(() => {
// 获取菜单列表
startMenus.value = getMenuList('开始')
discoveryMenus.value = getMenuList('发现')
subscribeMenus.value = getMenuList('订阅')
organizeMenus.value = getMenuList('整理')
systemMenus.value = getMenuList('系统')
startMenus.value = getMenuList(t('menu.start'))
discoveryMenus.value = getMenuList(t('menu.discovery'))
subscribeMenus.value = getMenuList(t('menu.subscribe'))
organizeMenus.value = getMenuList(t('menu.organize'))
systemMenus.value = getMenuList(t('menu.system'))
})
</script>
@@ -74,10 +78,10 @@ onMounted(() => {
<SearchBar />
<!-- 👉 Spacer -->
<VSpacer />
<!-- 👉 Theme & Language -->
<NavbarActions />
<!-- 👉 Shortcuts -->
<ShortcutBar v-if="superUser" />
<!-- 👉 Theme -->
<NavbarThemeSwitcher />
<!-- 👉 Notification -->
<UserNofification />
<!-- 👉 UserProfile -->
@@ -91,7 +95,7 @@ onMounted(() => {
<VerticalNavSectionTitle
v-if="discoveryMenus.length > 0"
:item="{
heading: '发现',
heading: t('menu.discovery'),
}"
/>
<VerticalNavLink v-for="item in discoveryMenus" :item="item" />
@@ -99,7 +103,7 @@ onMounted(() => {
<VerticalNavSectionTitle
v-if="subscribeMenus.length > 0"
:item="{
heading: '订阅',
heading: t('menu.subscribe'),
}"
/>
<VerticalNavLink v-for="item in subscribeMenus" :item="item" />
@@ -107,7 +111,7 @@ onMounted(() => {
<VerticalNavSectionTitle
v-if="organizeMenus.length > 0"
:item="{
heading: '整理',
heading: t('menu.organize'),
}"
/>
<VerticalNavLink v-for="item in organizeMenus" :item="item" />
@@ -115,7 +119,7 @@ onMounted(() => {
<VerticalNavSectionTitle
v-if="systemMenus.length > 0"
:item="{
heading: '系统',
heading: t('menu.system'),
}"
/>
<VerticalNavLink v-for="item in systemMenus" :item="item" />

View File

@@ -1,16 +1,20 @@
<script setup lang="ts">
import { SystemNavMenus } from '@/router/menu'
import { getNavMenus } from '@/router/i18n-menu'
import { useDisplay } from 'vuetify'
import { NavMenu } from '@/@layouts/types'
const display = useDisplay()
const appMode = inject('pwaMode') && display.mdAndDown.value
const route = useRoute()
// 获取导航菜单
const navMenus = computed(() => getNavMenus())
// 根据当前路径获取匹配的菜单路径
function getMenuPathFromRoute(path: string): string {
const matchedMenu = SystemNavMenus.find(menu => menu.footer === true && path.startsWith(menu.to))
return matchedMenu ? matchedMenu.to : '/apps'
const matchedMenu = navMenus.value.find((menu: NavMenu) => menu.footer === true && path.startsWith(menu.to as string))
return matchedMenu ? (matchedMenu.to as string) : '/apps'
}
// 当前选中的菜单,初始值基于当前路由
@@ -18,7 +22,7 @@ const currentMenu = ref<string>(getMenuPathFromRoute(route.path))
// 过滤出底部菜单项
const footerMenus = computed(() => {
return SystemNavMenus.filter(menu => menu.footer === true)
return navMenus.value.filter((menu: NavMenu) => menu.footer === true)
})
// 监听路由变化来更新currentMenu

View File

@@ -2,10 +2,10 @@
const props = defineProps({
modelValue: {
type: String,
default: '* * * * *',
default: '',
},
items: {
type: Array as PropType<{ title: string; icon: string }[]>,
type: Array as PropType<{ title: string; icon: string; tab: string }[]>,
default: () => [],
},
})
@@ -76,8 +76,8 @@ onUnmounted(() => {
v-for="(item, index) in items"
:key="index"
class="header-tab"
:class="{ 'active': currentValue === item.title }"
@click="currentValue = item.title"
:class="{ 'active': currentValue === item.tab }"
@click="currentValue = item.tab"
>
<VIcon v-if="item.icon" :icon="item.icon" size="small" class="header-tab-icon" />
<span>{{ item.title }}</span>

View File

@@ -0,0 +1,45 @@
<script setup lang="ts">
import type { ThemeSwitcherTheme } from '@layouts/types'
import { useI18n } from 'vue-i18n'
import LocaleSwitcher from '@/@core/components/LocaleSwitcher.vue'
const { t } = useI18n()
const themes: ThemeSwitcherTheme[] = [
{
name: 'auto',
title: t('theme.auto'),
icon: 'mdi-laptop',
},
{
name: 'light',
title: t('theme.light'),
icon: 'mdi-weather-sunny',
},
{
name: 'dark',
title: t('theme.dark'),
icon: 'mdi-weather-night',
},
{
name: 'purple',
title: t('theme.purple'),
icon: 'mdi-brightness-4',
},
{
name: 'transparent',
title: t('theme.transparent'),
icon: 'mdi-gradient-horizontal',
},
]
</script>
<template>
<div class="d-flex align-center">
<!-- 语言切换 -->
<LocaleSwitcher class="me-2" />
<!-- 主题切换 -->
<ThemeSwitcher :themes="themes" />
</div>
</template>

View File

@@ -7,11 +7,14 @@ import api from '@/api'
import ProgressDialog from '@/components/dialog/ProgressDialog.vue'
import UserAuthDialog from '@/components/dialog/UserAuthDialog.vue'
import { useAuthStore, useUserStore } from '@/stores'
import { useI18n } from 'vue-i18n'
// 认证 Store
const authStore = useAuthStore()
// 用户 Store
const userStore = useUserStore()
// 国际化
const { t } = useI18n()
// 确认框
const createConfirm = useConfirm()
@@ -153,7 +156,7 @@ const userLevel = computed(() => userStore.level)
<template #prepend>
<VIcon icon="mdi-logout" />
</template>
退出登录
{{ t('app.logout') }}
</VBtn>
</div>
</VList>
@@ -161,7 +164,7 @@ const userLevel = computed(() => userStore.level)
<!-- !SECTION -->
</VAvatar>
<!-- 重启进度框 -->
<ProgressDialog v-if="progressDialog" v-model="progressDialog" text="正在重启 ..." />
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="t('app.restarting')" />
<!-- 用户认证对话框 -->
<UserAuthDialog v-if="siteAuthDialog" v-model="siteAuthDialog" @done="siteAuthDone" @close="siteAuthDialog = false" />
<!-- 重启确认对话框 -->
@@ -173,14 +176,18 @@ const userLevel = computed(() => userStore.level)
<VIcon size="x-large" icon="mdi-alert" />
</VAvatar>
<div class="ms-3">
<p class="font-weight-bold text-xl text-high-emphasis">确认重启系统吗</p>
<p>重启后您将被注销并需要重新登录</p>
<p class="font-weight-bold text-xl text-high-emphasis">{{ t('app.confirmRestart') }}</p>
<p>{{ t('app.restartTip') }}</p>
</div>
</div>
</VCardItem>
<VCardActions class="mx-auto">
<VBtn variant="tonal" color="secondary" class="px-5" @click="restartDialog = false">取消</VBtn>
<VBtn variant="elevated" color="error" @click="restart" prepend-icon="mdi-restart" class="px-5"> 确定 </VBtn>
<VBtn variant="tonal" color="secondary" class="px-5" @click="restartDialog = false">{{
t('common.cancel')
}}</VBtn>
<VBtn variant="elevated" color="error" @click="restart" prepend-icon="mdi-restart" class="px-5">{{
t('common.confirm')
}}</VBtn>
</VCardActions>
<VDialogCloseBtn @click="restartDialog = false" />
</VCard>