引入语言包

This commit is contained in:
2026-01-14 12:14:32 +08:00
parent e01690803d
commit 2ce76820da
9 changed files with 406 additions and 21 deletions

16
src/hooks/useI18n.js Normal file
View File

@@ -0,0 +1,16 @@
import { useI18n as useVueI18n } from 'vue-i18n'
import { useI18nStore } from '@/stores/modules/i18n'
export function useI18n() {
const { t, locale, availableLocales } = useVueI18n()
const i18nStore = useI18nStore()
return {
t,
locale,
availableLocales,
setLocale: i18nStore.setLocale,
currentLocale: i18nStore.currentLocale,
localeLabel: i18nStore.localeLabel
}
}

15
src/i18n/index.js Normal file
View File

@@ -0,0 +1,15 @@
import { createI18n } from 'vue-i18n'
import zh from './locales/zh-CN'
import en from './locales/en-US'
const i18n = createI18n({
legacy: false,
locale: 'zh-CN',
fallbackLocale: 'en-US',
messages: {
'zh-CN': zh,
'en-US': en
}
})
export default i18n

103
src/i18n/locales/en-US.js Normal file
View File

@@ -0,0 +1,103 @@
export default {
common: {
welcome: 'Welcome',
login: 'Login',
logout: 'Logout',
register: 'Register',
username: 'Username',
password: 'Password',
confirmPassword: 'Confirm Password',
email: 'Email',
phone: 'Phone',
rememberMe: 'Remember Me',
forgotPassword: 'Forgot Password?',
submit: 'Submit',
cancel: 'Cancel',
save: 'Save',
edit: 'Edit',
delete: 'Delete',
add: 'Add',
search: 'Search',
reset: 'Reset',
confirm: 'Confirm',
back: 'Back',
next: 'Next',
previous: 'Previous',
refresh: 'Refresh',
export: 'Export',
import: 'Import',
download: 'Download',
upload: 'Upload',
view: 'View',
detail: 'Detail',
settings: 'Settings',
profile: 'Profile',
language: 'Language',
theme: 'Theme',
dark: 'Dark',
light: 'Light',
loading: 'Loading...',
noData: 'No Data',
success: 'Operation Successful',
error: 'Operation Failed',
warning: 'Warning',
info: 'Info',
confirmDelete: 'Are you sure you want to delete?',
confirmLogout: 'Are you sure you want to logout?',
required: 'This field is required',
operation: 'Operation',
time: 'Time',
status: 'Status',
enabled: 'Enabled',
disabled: 'Disabled',
yes: 'Yes',
no: 'No'
},
menu: {
dashboard: 'Dashboard',
userManagement: 'User Management',
roleManagement: 'Role Management',
permissionManagement: 'Permission Management',
systemSettings: 'System Settings',
logManagement: 'Log Management'
},
login: {
title: 'User Login',
subtitle: 'Welcome back, please login to your account',
loginButton: 'Login',
loginSuccess: 'Login Successful',
loginFailed: 'Login Failed',
usernamePlaceholder: 'Please enter username',
passwordPlaceholder: 'Please enter password',
noAccount: "Don't have an account?",
registerNow: 'Register Now'
},
layout: {
toggleSidebar: 'Toggle Sidebar',
collapse: 'Collapse',
expand: 'Expand',
logout: 'Logout'
},
table: {
total: 'Total {total} items',
selected: '{selected} items selected',
actions: 'Actions',
noData: 'No Data',
sort: 'Sort',
filter: 'Filter'
},
pagination: {
goTo: 'Go to',
page: 'Page',
total: 'Total {total} items',
itemsPerPage: '{size} items per page'
},
form: {
required: 'This field is required',
invalidEmail: 'Please enter a valid email address',
invalidPhone: 'Please enter a valid phone number',
passwordMismatch: 'Passwords do not match',
minLength: 'Minimum {min} characters required',
maxLength: 'Maximum {max} characters allowed'
}
}

103
src/i18n/locales/zh-CN.js Normal file
View File

@@ -0,0 +1,103 @@
export default {
common: {
welcome: '欢迎使用',
login: '登录',
logout: '退出登录',
register: '注册',
username: '用户名',
password: '密码',
confirmPassword: '确认密码',
email: '邮箱',
phone: '手机号',
rememberMe: '记住我',
forgotPassword: '忘记密码?',
submit: '提交',
cancel: '取消',
save: '保存',
edit: '编辑',
delete: '删除',
add: '添加',
search: '搜索',
reset: '重置',
confirm: '确认',
back: '返回',
next: '下一步',
previous: '上一步',
refresh: '刷新',
export: '导出',
import: '导入',
download: '下载',
upload: '上传',
view: '查看',
detail: '详情',
settings: '设置',
profile: '个人资料',
language: '语言',
theme: '主题',
dark: '暗色',
light: '亮色',
loading: '加载中...',
noData: '暂无数据',
success: '操作成功',
error: '操作失败',
warning: '警告',
info: '提示',
confirmDelete: '确定要删除吗?',
confirmLogout: '确定要退出登录吗?',
required: '此项为必填项',
operation: '操作',
time: '时间',
status: '状态',
enabled: '启用',
disabled: '禁用',
yes: '是',
no: '否'
},
menu: {
dashboard: '仪表板',
userManagement: '用户管理',
roleManagement: '角色管理',
permissionManagement: '权限管理',
systemSettings: '系统设置',
logManagement: '日志管理'
},
login: {
title: '用户登录',
subtitle: '欢迎回来,请登录您的账户',
loginButton: '登录',
loginSuccess: '登录成功',
loginFailed: '登录失败',
usernamePlaceholder: '请输入用户名',
passwordPlaceholder: '请输入密码',
noAccount: '还没有账户?',
registerNow: '立即注册'
},
layout: {
toggleSidebar: '切换侧边栏',
collapse: '折叠',
expand: '展开',
logout: '退出登录'
},
table: {
total: '共 {total} 条',
selected: '已选择 {selected} 项',
actions: '操作',
noData: '暂无数据',
sort: '排序',
filter: '筛选'
},
pagination: {
goTo: '前往',
page: '页',
total: '共 {total} 条',
itemsPerPage: '每页 {size} 条'
},
form: {
required: '此项为必填项',
invalidEmail: '请输入有效的邮箱地址',
invalidPhone: '请输入有效的手机号',
passwordMismatch: '两次输入的密码不一致',
minLength: '最少需要 {min} 个字符',
maxLength: '最多允许 {max} 个字符'
}
}

View File

@@ -0,0 +1,91 @@
<script setup>
import { ref } from 'vue'
import { useI18nStore } from '@/stores/modules/i18n'
const i18nStore = useI18nStore()
const showDropdown = ref(false)
const toggleDropdown = () => {
showDropdown.value = !showDropdown.value
}
const handleLanguageChange = (locale) => {
i18nStore.setLocale(locale)
showDropdown.value = false
}
const closeDropdown = () => {
showDropdown.value = false
}
</script>
<template>
<div class="language-switcher" @click="toggleDropdown">
<span class="language-dropdown">
{{ i18nStore.localeLabel }}
<svg class="arrow-icon" :class="{ 'rotate': showDropdown }" viewBox="0 0 1024 1024" version="1.1"
xmlns="http://www.w3.org/2000/svg" width="16" height="16">
<path
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3 0.1-12.7-6.4-12.7z" />
</svg>
</span>
<div v-show="showDropdown" class="dropdown-menu">
<div v-for="locale in i18nStore.availableLocales" :key="locale.value" class="dropdown-item"
@click.stop="handleLanguageChange(locale.value)">
{{ locale.label }}
</div>
</div>
</div>
</template>
<style scoped>
.language-switcher {
position: relative;
display: inline-block;
cursor: pointer;
user-select: none;
}
.language-dropdown {
display: flex;
align-items: center;
gap: 4px;
padding: 4px 8px;
border-radius: 4px;
transition: background-color 0.3s;
}
.language-dropdown:hover {
background-color: rgba(0, 0, 0, 0.05);
}
.arrow-icon {
transition: transform 0.3s;
}
.arrow-icon.rotate {
transform: rotate(180deg);
}
.dropdown-menu {
position: absolute;
top: calc(100% + 4px);
right: 0;
min-width: 120px;
background: white;
border: 1px solid #e4e7ed;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
z-index: 1000;
overflow: hidden;
}
.dropdown-item {
padding: 8px 16px;
transition: background-color 0.3s;
}
.dropdown-item:hover {
background-color: #f5f7fa;
}
</style>

View File

@@ -3,10 +3,17 @@ import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import pinia from './stores'
import i18n from './i18n'
import { useI18nStore } from './stores/modules/i18n'
const app = createApp(App)
app.use(router)
app.use(pinia)
app.use(i18n)
// 初始化 i18n store从 localStorage 读取保存的语言设置
const i18nStore = useI18nStore()
i18nStore.initLocale()
app.mount('#app')

View File

@@ -1,5 +1,9 @@
<script setup>
import { ref } from 'vue'
import { useI18n } from '@/hooks/useI18n'
import LanguageSwitcher from '@/layouts/components/LanguageSwitcher.vue'
const { t } = useI18n()
const loginForm = ref({
username: '',
@@ -16,46 +20,51 @@ const handleLogin = () => {
console.log('登录信息:', loginForm.value)
loading.value = false
// 这里可以添加实际的登录逻辑
alert('登录成功!(模拟')
alert(t('login.loginSuccess') + '(模拟)')
}, 1000)
}
</script>
<template>
<div class="login-container">
<div class="language-switcher-wrapper">
<LanguageSwitcher />
</div>
<div class="login-card">
<div class="login-header">
<h2>用户登录</h2>
<p>欢迎回来请登录您的账户</p>
<h2>{{ t('login.title') }}</h2>
<p>{{ t('login.subtitle') }}</p>
</div>
<form @submit.prevent="handleLogin" class="login-form">
<div class="form-group">
<label for="username">用户名</label>
<input id="username" v-model="loginForm.username" type="text" placeholder="请输入用户名" required />
<label for="username">{{ t('common.username') }}</label>
<input id="username" v-model="loginForm.username" type="text"
:placeholder="t('login.usernamePlaceholder')" required />
</div>
<div class="form-group">
<label for="password">密码</label>
<input id="password" v-model="loginForm.password" type="password" placeholder="请输入密码" required />
<label for="password">{{ t('common.password') }}</label>
<input id="password" v-model="loginForm.password" type="password"
:placeholder="t('login.passwordPlaceholder')" required />
</div>
<div class="form-options">
<label class="remember-me">
<input type="checkbox" />
<span>记住我</span>
<span>{{ t('common.rememberMe') }}</span>
</label>
<a href="#" class="forgot-password">忘记密码</a>
<a href="#" class="forgot-password">{{ t('common.forgotPassword') }}</a>
</div>
<button type="submit" class="login-btn" :disabled="loading">
<span v-if="!loading">登录</span>
<span v-else>登录中...</span>
<span v-if="!loading">{{ t('login.loginButton') }}</span>
<span v-else>{{ t('common.loading') }}</span>
</button>
</form>
<div class="login-footer">
<p>还没有账户 <a href="#">立即注册</a></p>
<p>{{ t('login.noAccount') }} <a href="#">{{ t('login.registerNow') }}</a></p>
</div>
</div>
</div>
@@ -73,6 +82,13 @@ const handleLogin = () => {
overflow: hidden;
}
.language-switcher-wrapper {
position: absolute;
top: 20px;
right: 20px;
z-index: 10;
}
/* 网格背景 */
.login-container::before {
content: '';

View File

@@ -0,0 +1,34 @@
import { defineStore } from 'pinia'
import i18n from '@/i18n'
export const useI18nStore = defineStore('i18n', {
state: () => ({
currentLocale: 'zh-CN',
availableLocales: [
{ label: '简体中文', value: 'zh-CN' },
{ label: 'English', value: 'en-US' }
]
}),
getters: {
localeLabel: (state) => {
const locale = state.availableLocales.find((item) => item.value === state.currentLocale)
return locale ? locale.label : ''
}
},
actions: {
setLocale(locale) {
this.currentLocale = locale
i18n.global.locale.value = locale
localStorage.setItem('locale', locale)
},
initLocale() {
const savedLocale = localStorage.getItem('locale')
if (savedLocale && this.availableLocales.some((item) => item.value === savedLocale)) {
this.setLocale(savedLocale)
}
}
}
})

View File

@@ -6,13 +6,13 @@ import vueDevTools from 'vite-plugin-vue-devtools'
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
vueDevTools(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
},
},
plugins: [
vue(),
vueDevTools(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
},
},
})