多语言

This commit is contained in:
zhang zhuo 2025-12-05 17:53:47 +08:00
parent 2441594f1c
commit 935d764bb5
13 changed files with 153 additions and 49 deletions

View File

@ -7,7 +7,7 @@
<script setup>
import {useI18n} from 'vue-i18n'
import {computed, ref} from 'vue'
import tools from "@/utils/tools.js";
import tools from "@/utils/tools"
import colorTool from '@/utils/color'
const {locale, messages} = useI18n()

View File

@ -231,5 +231,8 @@ export default {
del: async function (data = {}) {
return await http.delete("translation/del", data);
},
load: async function (data = {}) {
return await http.get("translations", data);
}
},
}

View File

@ -1,16 +1,27 @@
<template>
<el-form ref="form" label-width="120px" label-position="left" style="padding:0 20px;">
<el-divider>{{t('user.thememode')}}</el-divider>
<el-divider>{{ t('user.thememode') }}</el-divider>
<el-row class="pi-theme" :gutter="20">
<el-col :span="8"><img src="/images/light.png" class="pi-pic" :class="{'active': dark == 'light'}" @click="themeClick('light')"/><el-text type="info" class="pi-text">浅色</el-text></el-col>
<el-col :span="8"><img src="/images/dark.png" class="pi-pic" :class="{'active': dark == 'dark'}" @click="themeClick('dark')"/><el-text type="info" class="pi-text">深色</el-text></el-col>
<el-col :span="8"><img src="/images/follow.png" class="pi-pic" :class="{'active': dark == 'follow'}" @click="themeClick('follow')"/><el-text type="info" class="pi-text">跟随系统</el-text></el-col>
<el-col :span="8"><img src="/images/light.png" class="pi-pic" :class="{'active': dark == 'light'}"
@click="themeClick('light')"/>
<el-text type="info" class="pi-text">浅色</el-text>
</el-col>
<el-col :span="8"><img src="/images/dark.png" class="pi-pic" :class="{'active': dark == 'dark'}"
@click="themeClick('dark')"/>
<el-text type="info" class="pi-text">深色</el-text>
</el-col>
<el-col :span="8"><img src="/images/follow.png" class="pi-pic" :class="{'active': dark == 'follow'}"
@click="themeClick('follow')"/>
<el-text type="info" class="pi-text">跟随系统</el-text>
</el-col>
</el-row>
<el-divider></el-divider>
<el-form-item :label="t('user.language')">
<el-select v-model="lang">
<el-option label="简体中文" value="zh-cn"></el-option>
<el-option label="English" value="en"></el-option>
<el-option label="日本語" value="ja"></el-option>
<el-option label="Tiếng Việt" value="vi"></el-option>
</el-select>
</el-form-item>
<el-divider></el-divider>
@ -46,9 +57,10 @@ import tools from "@/utils/tools";
import config from "@/config";
import globalStore from "@/store/global.js";
import {useI18n} from "vue-i18n";
import {setupI18n} from "@/locales/setup.js";
const global = globalStore()
const {t, locale} = useI18n()
const {t} = useI18n()
let layout = ref(tools.data.get('APP_LAYOUT') || global.layout);
let menuIsCollapse = ref(global.menuIsCollapse);
@ -83,8 +95,8 @@ watch(weakMode, (val) => {
})
watch(lang, (val) => {
locale.value = val
tools.data.set("APP_LANG", val);
setupI18n(val)
tools.data.set("APP_LANG", val)
})
watch(colorPrimary, (val) => {
@ -106,14 +118,14 @@ function themeClick(val) {
if (val == 'dark') {
document.documentElement.classList.add("dark")
localStorage.setItem("APP_DARK", val)
} else if(val == 'light') {
} else if (val == 'light') {
document.documentElement.classList.remove("dark")
localStorage.setItem("APP_DARK", val)
} else {
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)')
if (systemTheme.matches) {
document.documentElement.classList.add("dark")
}else {
} else {
document.documentElement.classList.remove("dark")
}
localStorage.setItem("APP_DARK", val)
@ -123,8 +135,25 @@ function themeClick(val) {
</script>
<style lang="scss" scoped>
.pi-theme {text-align: center;}
.pi-theme .pi-pic {cursor: pointer;margin-bottom: 6px;border-radius: 8px; box-shadow: 0 2px 8px #0003;width: 100px;}
.pi-theme .pi-pic.active {border: 2px solid var(--el-color-primary); cursor: pointer;}
.pi-theme .pi-text {margin-top: 6px; cursor: pointer;}
.pi-theme {
text-align: center;
}
.pi-theme .pi-pic {
cursor: pointer;
margin-bottom: 6px;
border-radius: 8px;
box-shadow: 0 2px 8px #0003;
width: 100px;
}
.pi-theme .pi-pic.active {
border: 2px solid var(--el-color-primary);
cursor: pointer;
}
.pi-theme .pi-text {
margin-top: 6px;
cursor: pointer;
}
</style>

View File

@ -1,29 +1,12 @@
import { createI18n } from 'vue-i18n'
import el_zh_cn from 'element-plus/dist/locale/zh-cn'
import el_en from 'element-plus/dist/locale/en'
import {createI18n} from 'vue-i18n'
import config from "@/config"
import tools from '@/utils/tools'
import zh_cn from '@/locales/lang/zh-cn'
import en from '@/locales/lang/en'
const messages = {
'zh-cn': {
...el_zh_cn,
...zh_cn
},
'en': {
...el_en,
...en
}
}
const i18n = createI18n({
legacy: false,
fallbackLocale: 'zh-cn',
locale: tools.data.get("APP_LANG") || config.LANG,
globalInjection: true,
messages,
legacy: false,
fallbackLocale: config.LANG,
locale: config.LANG,
globalInjection: true,
messages: {}
})
export default i18n;

1
src/locales/lang/ja.ts Normal file
View File

@ -0,0 +1 @@
export default {}

1
src/locales/lang/ms.ts Normal file
View File

@ -0,0 +1 @@
export default {}

1
src/locales/lang/vi.ts Normal file
View File

@ -0,0 +1 @@
export default {}

22
src/locales/languages.ts Normal file
View File

@ -0,0 +1,22 @@
import el_zh_cn from "element-plus/dist/locale/zh-cn";
import el_en from "element-plus/dist/locale/en";
import el_vi from "element-plus/dist/locale/vi";
import el_ms from "element-plus/dist/locale/ms";
import el_ja from "element-plus/dist/locale/ja";
import lang_zh_cn from "@/locales/lang/zh-cn";
import lang_en from "@/locales/lang/en";
import lang_ja from "@/locales/lang/ja";
import lang_vi from "@/locales/lang/vi";
import lang_ms from "@/locales/lang/ms";
export const LANGUAGE_MAP: Record<
string,
{ el: any; local: any }
> = {
"zh-cn": {el: el_zh_cn, local: lang_zh_cn},
en: {el: el_en, local: lang_en},
ja: {el: el_ja, local: lang_ja},
vi: {el: el_vi, local: lang_vi},
ms: {el: el_ms, local: lang_ms},
};

22
src/locales/setup.ts Normal file
View File

@ -0,0 +1,22 @@
import i18n from "@/locales/index";
import config from "@/config";
import tools from "@/utils/tools";
import api from "@/api";
import {LANGUAGE_MAP} from "@/locales/languages";
export async function setupI18n(locale: string = null) {
locale = locale || tools.data.get("APP_LANG") || config.LANG;
const langConfig = LANGUAGE_MAP[locale];
// 先从缓存中取
var messages = tools.data.get("LOCALE:" + locale)
if (!messages) {
const res = await api.system.translation.load({locale});
messages = tools.deepMerge(langConfig.el, langConfig.local, res['data'] || {})
tools.data.set("LOCALE:" + locale, messages, 86400)
}
// 设置语言包内容
i18n.global.setLocaleMessage(locale, messages);
// 切换语言
i18n.global.locale.value = locale;
return i18n;
}

View File

@ -1,21 +1,29 @@
import { createApp } from 'vue'
import {createApp} from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import 'element-plus/theme-chalk/display.css'
import i18n from './locales'
import {setupI18n} from "@/locales/setup"
import App from './App.vue'
import pinia from './store'
import router from './router'
import pi from './pi'
const app = createApp(App);
async function bootstrap() {
await setupI18n();
const app = createApp(App);
app.use(pinia);
app.use(router);
app.use(ElementPlus);
app.use(i18n);
app.use(pi);
//挂载app
app.mount('#app');
}
bootstrap()
app.use(pinia);
app.use(router);
app.use(ElementPlus);
app.use(i18n);
app.use(pi);
//挂载app
app.mount('#app');

View File

@ -19,6 +19,9 @@ axios.interceptors.request.use((config) => {
config.params = config.params || {};
config.params['_'] = new Date().getTime();
}
// 多语言
const lang = tools.data.get("APP_LANG") || 'zh-cn'
config.headers['Accept-Language'] = lang
return config;
})
//响应拦截

View File

@ -214,6 +214,28 @@ const tools = {
}
}
}
},
// 深度合并数据
deepMerge: (...objects: any[]) => {
const result: any = {}
for (const obj of objects) {
tools.mergeObject(result, obj)
}
return result
},
mergeObject: (target: any, source: any) => {
if (!source || typeof source !== 'object') return
for (const key in source) {
const value = source[key]
if (value && typeof value === 'object' && !Array.isArray(value)) {
if (!target[key] || typeof target[key] !== 'object') {
target[key] = {}
}
tools.mergeObject(target[key], value)
} else {
target[key] = value
}
}
}
}
export default tools

View File

@ -71,6 +71,7 @@
<script setup>
import {getCurrentInstance, ref, onMounted, watch} from 'vue'
import {useI18n} from 'vue-i18n'
import {setupI18n} from "@/locales/setup"
import {useRouter, useRoute} from 'vue-router'
import tools from "@/utils/tools";
import sysConfig from "@/config/index"
@ -81,7 +82,7 @@ defineOptions({
})
const {proxy} = getCurrentInstance()
const {t, locale} = useI18n()
const {t} = useI18n()
const router = useRouter()
const route = useRoute()
import bg from '@/assets/images/bg.jpg'
@ -98,6 +99,14 @@ const langs = ref([
{
name: 'English',
value: 'en',
},
{
name: '日本語',
value: 'ja',
},
{
name: 'Tiếng Việt',
value: 'vi',
}
])
@ -140,7 +149,7 @@ onMounted(() => {
})
watch(lang, (val) => {
locale.value = val
setupI18n(val)
tools.data.set("APP_LANG", val)
})