引入语言包
This commit is contained in:
16
src/hooks/useI18n.js
Normal file
16
src/hooks/useI18n.js
Normal 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
15
src/i18n/index.js
Normal 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
103
src/i18n/locales/en-US.js
Normal 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
103
src/i18n/locales/zh-CN.js
Normal 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} 个字符'
|
||||||
|
}
|
||||||
|
}
|
||||||
91
src/layouts/components/LanguageSwitcher.vue
Normal file
91
src/layouts/components/LanguageSwitcher.vue
Normal 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>
|
||||||
@@ -3,10 +3,17 @@ import { createApp } from 'vue'
|
|||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
import pinia from './stores'
|
import pinia from './stores'
|
||||||
|
import i18n from './i18n'
|
||||||
|
import { useI18nStore } from './stores/modules/i18n'
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
|
||||||
app.use(router)
|
app.use(router)
|
||||||
app.use(pinia)
|
app.use(pinia)
|
||||||
|
app.use(i18n)
|
||||||
|
|
||||||
|
// 初始化 i18n store,从 localStorage 读取保存的语言设置
|
||||||
|
const i18nStore = useI18nStore()
|
||||||
|
i18nStore.initLocale()
|
||||||
|
|
||||||
app.mount('#app')
|
app.mount('#app')
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
import { useI18n } from '@/hooks/useI18n'
|
||||||
|
import LanguageSwitcher from '@/layouts/components/LanguageSwitcher.vue'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
const loginForm = ref({
|
const loginForm = ref({
|
||||||
username: '',
|
username: '',
|
||||||
@@ -16,46 +20,51 @@ const handleLogin = () => {
|
|||||||
console.log('登录信息:', loginForm.value)
|
console.log('登录信息:', loginForm.value)
|
||||||
loading.value = false
|
loading.value = false
|
||||||
// 这里可以添加实际的登录逻辑
|
// 这里可以添加实际的登录逻辑
|
||||||
alert('登录成功!(模拟)')
|
alert(t('login.loginSuccess') + '!(模拟)')
|
||||||
}, 1000)
|
}, 1000)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="login-container">
|
<div class="login-container">
|
||||||
|
<div class="language-switcher-wrapper">
|
||||||
|
<LanguageSwitcher />
|
||||||
|
</div>
|
||||||
<div class="login-card">
|
<div class="login-card">
|
||||||
<div class="login-header">
|
<div class="login-header">
|
||||||
<h2>用户登录</h2>
|
<h2>{{ t('login.title') }}</h2>
|
||||||
<p>欢迎回来,请登录您的账户</p>
|
<p>{{ t('login.subtitle') }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form @submit.prevent="handleLogin" class="login-form">
|
<form @submit.prevent="handleLogin" class="login-form">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="username">用户名</label>
|
<label for="username">{{ t('common.username') }}</label>
|
||||||
<input id="username" v-model="loginForm.username" type="text" placeholder="请输入用户名" required />
|
<input id="username" v-model="loginForm.username" type="text"
|
||||||
|
:placeholder="t('login.usernamePlaceholder')" required />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="password">密码</label>
|
<label for="password">{{ t('common.password') }}</label>
|
||||||
<input id="password" v-model="loginForm.password" type="password" placeholder="请输入密码" required />
|
<input id="password" v-model="loginForm.password" type="password"
|
||||||
|
:placeholder="t('login.passwordPlaceholder')" required />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-options">
|
<div class="form-options">
|
||||||
<label class="remember-me">
|
<label class="remember-me">
|
||||||
<input type="checkbox" />
|
<input type="checkbox" />
|
||||||
<span>记住我</span>
|
<span>{{ t('common.rememberMe') }}</span>
|
||||||
</label>
|
</label>
|
||||||
<a href="#" class="forgot-password">忘记密码?</a>
|
<a href="#" class="forgot-password">{{ t('common.forgotPassword') }}</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="login-btn" :disabled="loading">
|
<button type="submit" class="login-btn" :disabled="loading">
|
||||||
<span v-if="!loading">登录</span>
|
<span v-if="!loading">{{ t('login.loginButton') }}</span>
|
||||||
<span v-else>登录中...</span>
|
<span v-else>{{ t('common.loading') }}</span>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="login-footer">
|
<div class="login-footer">
|
||||||
<p>还没有账户? <a href="#">立即注册</a></p>
|
<p>{{ t('login.noAccount') }} <a href="#">{{ t('login.registerNow') }}</a></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -73,6 +82,13 @@ const handleLogin = () => {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.language-switcher-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
/* 网格背景 */
|
/* 网格背景 */
|
||||||
.login-container::before {
|
.login-container::before {
|
||||||
content: '';
|
content: '';
|
||||||
|
|||||||
34
src/stores/modules/i18n.js
Normal file
34
src/stores/modules/i18n.js
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user