Merge pull request #7 from InoueKenta/feature/i18n

Feature/i18n
This commit is contained in:
mizhexiaoxiao 2023-11-02 11:08:36 +08:00 committed by GitHub
commit 33335909f9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 985 additions and 714 deletions

View File

@ -17,7 +17,7 @@ export const PROXY_CONFIG = {
* @转发路径 http://localhost:9999/api/v1/user
*/
'/api/v1': {
target: 'http://localhost:9999',
target: 'http://127.0.0.1:9999',
changeOrigin: true,
},
}

16
web/i18n/index.js Normal file
View File

@ -0,0 +1,16 @@
import { createI18n } from 'vue-i18n'
import { sStorage } from '@/utils'
import messages from './messages'
const currentLocale = sStorage.get('locale')
const i18n = createI18n({
legacy: false,
globalInjection: true,
locale: currentLocale || 'en',
fallbackLocale: 'en',
messages: messages
})
export default i18n

62
web/i18n/messages/cn.json Normal file
View File

@ -0,0 +1,62 @@
{
"lang": "中文",
"app_name": "Vue FastAPI Admin",
"header": {
"label_profile": "个人信息",
"label_logout": "退出登录",
"label_logout_dialog_title": "提示",
"text_logout_confirm": "确认退出?",
"text_logout_success": "已退出登录"
},
"views": {
"login": {
"text_login": "登录",
"message_input_username_password": "请输入用户名和密码",
"message_verifying": "正在验证...",
"message_login_success": "登录成功"
},
"workbench": {
"label_workbench": "工作台",
"text_hello": "hello, {username}",
"text_welcome": "今天又是元气满满的一天!",
"label_number_of_items": "项目数",
"label_upcoming": "待办",
"label_information": "消息",
"label_project": "项目",
"label_more": "更多"
},
"profile": {
"label_profile": "个人中心",
"label_modify_information": "修改信息",
"label_change_password": "修改密码",
"label_avatar": "头像",
"label_username": "用户姓名",
"label_email": "邮箱",
"label_old_password": "旧密码",
"label_new_password": "新密码",
"label_confirm_password": "确认密码",
"placeholder_username": "请填写姓名",
"placeholder_email": "请填写邮箱",
"placeholder_old_password": "请输入旧密码",
"placeholder_new_password": "请输入新密码",
"placeholder_confirm_password": "请再次输入新密码",
"message_username_required": "请输入昵称",
"message_old_password_required": "请输入旧密码",
"message_new_password_required": "请输入新密码",
"message_password_confirmation_required": "请再次输入密码",
"message_password_confirmation_diff": "两次密码输入不一致"
},
"errors": {
"label_error": "错误页",
"text_back_to_home": "返回首页"
}
},
"common": {
"text": {
"update_success": "修改成功"
},
"buttons": {
"update": "修改"
}
}
}

62
web/i18n/messages/en.json Normal file
View File

@ -0,0 +1,62 @@
{
"lang": "English",
"app_name": "Vue FastAPI Admin",
"header": {
"label_profile": "Profile",
"label_logout": "Logout",
"label_logout_dialog_title": "Hint",
"text_logout_confirm": "Logout confirm",
"text_logout_success": "Logout success"
},
"views": {
"login": {
"text_login": "Login",
"message_input_username_password": "Please enter username and password",
"message_verifying": "Verifying...",
"message_login_success": "Login successful"
},
"workbench": {
"label_workbench": "Workbench",
"text_hello": "hello, {username}",
"text_welcome": "Today is another day full of energy!",
"label_number_of_items": "Number of items",
"label_upcoming": "Upcoming",
"label_information": "Information",
"label_project": "Project",
"label_more": "More"
},
"profile": {
"label_profile": "Profile",
"label_modify_information": "Modify your information",
"label_change_password": "Change password",
"label_avatar": "Avatar",
"label_username": "Username",
"label_email": "Email",
"label_old_password": "Old password",
"label_new_password": "New password",
"label_confirm_password": "Password confirmation",
"placeholder_username": "Please fill in your name",
"placeholder_email": "Please fill in your email address",
"placeholder_old_password": "Please enter the old password",
"placeholder_new_password": "Please enter a new password",
"placeholder_confirm_password": "Please enter the confirm password",
"message_username_required": "Please enter username",
"message_old_password_required": "Please enter the old password",
"message_new_password_required": "Please enter a new password",
"message_password_confirmation_required": "Please enter confirm password",
"message_password_confirmation_diff": "Two password inputs are inconsistent"
},
"errors": {
"label_error": "Error",
"text_back_to_home": "Back to home"
}
},
"common": {
"text": {
"update_success": "Update success"
},
"buttons": {
"update": "Update"
}
}
}

View File

@ -0,0 +1,7 @@
import * as en from './en.json'
import * as cn from './cn.json'
export default {
en,
cn
}

View File

@ -34,6 +34,7 @@
"vite-plugin-html": "^3.2.0",
"vite-plugin-svg-icons": "^2.0.1",
"vue": "^3.3.4",
"vue-i18n": "9",
"vue-router": "^4.2.4"
},
"devDependencies": {

1377
web/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,30 @@
<template>
<n-dropdown :options="options" @select="handleChangeLocale">
<n-icon mr-20 size="18" style="cursor: pointer">
<icon-mdi:globe/>
</n-icon>
</n-dropdown>
</template>
<script setup>
import {useI18n} from 'vue-i18n'
import {useAppStore} from "@/store";
const store = useAppStore()
const { availableLocales, t } = useI18n()
const options = computed(() => {
let select = []
availableLocales.forEach(locale => {
select.push({
label: t('lang', 1, {'locale': locale}),
key: locale
})
})
return select
})
const handleChangeLocale = (value) => {
store.setLocale(value)
}
</script>

View File

@ -11,6 +11,9 @@
import { useUserStore } from '@/store'
import { renderIcon } from '@/utils'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
const {t} = useI18n()
const router = useRouter()
@ -18,12 +21,12 @@ const userStore = useUserStore()
const options = [
{
label: '个人信息',
label: t('header.label_profile'),
key: 'profile',
icon: renderIcon('mdi-account-arrow-right-outline', { size: '14px' }),
},
{
label: '退出登录',
label: t('header.label_logout'),
key: 'logout',
icon: renderIcon('mdi:exit-to-app', { size: '14px' }),
},
@ -34,12 +37,12 @@ function handleSelect(key) {
router.push('/profile')
} else if (key === 'logout') {
$dialog.confirm({
title: '提示',
title: t('header.label_logout_dialog_title'),
type: 'warning',
content: '确认退出?',
content: t('header.text_logout_confirm'),
confirm() {
userStore.logout()
$message.success('已退出登录')
$message.success(t('header.text_logout_success'))
},
})
}

View File

@ -4,6 +4,7 @@
<BreadCrumb ml-15 hidden sm:block />
</div>
<div ml-auto flex items-center>
<Languages />
<ThemeMode />
<GithubSite />
<FullScreen />
@ -18,4 +19,5 @@ import FullScreen from './components/FullScreen.vue'
import UserAvatar from './components/UserAvatar.vue'
import GithubSite from './components/GithubSite.vue'
import ThemeMode from './components/ThemeMode.vue'
import Languages from './components/Languages.vue';
</script>

View File

@ -10,6 +10,7 @@ import { setupStore } from '@/store'
import App from './App.vue'
import { setupDirectives } from './directives'
import { useResize } from '@/utils'
import i18n from '~/i18n'
async function setupApp() {
const app = createApp(App)
@ -19,6 +20,7 @@ async function setupApp() {
await setupRouter(app)
setupDirectives(app)
app.use(useResize)
app.use(i18n)
app.mount('#app')
}

View File

@ -1,8 +1,11 @@
import i18n from '~/i18n'
const {t} = i18n.global
const Layout = () => import('@/layout/index.vue')
export const basicRoutes = [
{
name: '工作台',
name: t('views.workbench.label_workbench'),
path: '/',
component: Layout,
redirect: '/workbench', // 默认跳转到首页
@ -10,9 +13,9 @@ export const basicRoutes = [
{
path: 'workbench',
component: () => import('@/views/workbench/index.vue'),
name: '工作台',
name: t('views.workbench.label_workbench'),
meta: {
title: '工作台',
title: t('views.workbench.label_workbench'),
icon: 'icon-park-outline:workbench',
affix: true,
},
@ -21,7 +24,7 @@ export const basicRoutes = [
meta: { order: 0 },
},
{
name: '个人中心',
name: t('views.profile.label_profile'),
path: '/',
component: Layout,
isHidden: true,
@ -29,9 +32,9 @@ export const basicRoutes = [
{
path: 'profile',
component: () => import('@/views/profile/index.vue'),
name: '个人中心',
name: t('views.profile.label_profile'),
meta: {
title: '个人中心',
title: t('views.profile.label_profile'),
icon: 'user',
affix: true,
},
@ -45,7 +48,7 @@ export const basicRoutes = [
component: Layout,
redirect: '/error-page/404',
meta: {
title: '错误页',
title: t('views.errors.label_error'),
icon: 'mdi:alert-circle-outline',
order: 99,
},

View File

@ -1,5 +1,10 @@
import { defineStore } from 'pinia'
import { useDark } from '@vueuse/core'
import { sStorage } from '@/utils'
import i18n from '~/i18n'
const currentLocale = sStorage.get('locale')
const {locale} = i18n.global
const isDark = useDark()
export const useAppStore = defineStore('app', {
@ -11,6 +16,7 @@ export const useAppStore = defineStore('app', {
/** keepAlive路由的key重新赋值可重置keepAlive */
aliveKeys: {},
isDark,
locale: currentLocale || 'en'
}
},
actions: {
@ -45,5 +51,10 @@ export const useAppStore = defineStore('app', {
toggleDark() {
this.isDark = !this.isDark
},
setLocale(newLocale) {
this.locale = newLocale
locale.value = newLocale
sStorage.set('locale', newLocale)
}
},
})

View File

@ -5,7 +5,7 @@
<icon-custom-unauthorized text-400px text-primary></icon-custom-unauthorized>
</template>
<template #footer>
<n-button type="primary" @click="replace('/')">返回首页</n-button>
<n-button type="primary" @click="replace('/')">{{ $t('views.errors.text_back_to_home') }}</n-button>
</template>
</n-result>
</AppPage>

View File

@ -5,7 +5,7 @@
<icon-custom-forbidden text-400px text-primary></icon-custom-forbidden>
</template>
<template #footer>
<n-button type="primary" @click="replace('/')">返回首页</n-button>
<n-button type="primary" @click="replace('/')">{{ $t('views.errors.text_back_to_home') }}</n-button>
</template>
</n-result>
</AppPage>

View File

@ -5,7 +5,7 @@
<icon-custom-not-found text-400px text-primary></icon-custom-not-found>
</template>
<template #footer>
<n-button type="primary" @click="replace('/')">返回首页</n-button>
<n-button type="primary" @click="replace('/')">{{ $t('views.errors.text_back_to_home') }}</n-button>
</template>
</n-result>
</AppPage>

View File

@ -5,7 +5,7 @@
<icon-custom-server-error text-400px text-primary></icon-custom-server-error>
</template>
<template #footer>
<n-button type="primary" @click="replace('/')">返回首页</n-button>
<n-button type="primary" @click="replace('/')">{{ $t('views.errors.text_back_to_home') }}</n-button>
</template>
</n-result>
</AppPage>

View File

@ -11,7 +11,7 @@
<div w-320 flex-col px-20 py-35>
<h5 f-c-c text-24 font-normal color="#6a6a6a">
<icon-custom-logo mr-10 text-50 color-primary />{{ title }}
<icon-custom-logo mr-10 text-50 color-primary />{{ $t('app_name') }}
</h5>
<div mt-30>
<n-input
@ -44,7 +44,7 @@
:loading="loading"
@click="handleLogin"
>
登录
{{ $t('views.login.text_login') }}
</n-button>
</div>
</div>
@ -57,11 +57,11 @@ import { lStorage, setToken } from '@/utils'
import bgImg from '@/assets/images/login_bg.webp'
import api from '@/api'
import { addDynamicRoutes } from '@/router'
const title = import.meta.env.VITE_TITLE
import {useI18n} from 'vue-i18n'
const router = useRouter()
const { query } = useRoute()
const {t} = useI18n({ useScope: "global" })
const loginInfo = ref({
username: '',
@ -82,14 +82,14 @@ const loading = ref(false)
async function handleLogin() {
const { username, password } = loginInfo.value
if (!username || !password) {
$message.warning('请输入用户名和密码')
$message.warning(t('views.login.message_input_username_password'))
return
}
try {
loading.value = true
$message.loading('正在验证...')
$message.loading(t('views.login.message_login_success'))
const res = await api.login({ username, password: password.toString() })
$message.success('登录成功')
$message.success(t('views.login.message_login_success'))
setToken(res.data.access_token)
await addDynamicRoutes()
if (query.redirect) {

View File

@ -1,12 +1,13 @@
<script setup>
import { ref } from 'vue'
import { NButton, NForm, NFormItem, NInput, NTabPane, NTabs, NImage } from 'naive-ui'
import {useI18n} from "vue-i18n";
import CommonPage from '@/components/page/CommonPage.vue'
import { useUserStore } from '@/store'
import api from '@/api'
import { is } from '~/src/utils'
const {t} = useI18n()
const userStore = useUserStore()
const isLoading = ref(false)
@ -26,7 +27,7 @@ async function updateProfile() {
.then(() => {
userStore.setUserInfo(infoForm.value)
isLoading.value = false
$message.success('修改成功')
$message.success(t('common.text.update_success'))
})
.catch(() => {
isLoading.value = false
@ -37,7 +38,7 @@ const infoFormRules = {
username: [
{
required: true,
message: '请输入昵称',
message: t('views.profile.message_username_required'),
trigger: ['input', 'blur', 'change'],
},
],
@ -77,31 +78,31 @@ const passwordFormRules = {
old_password: [
{
required: true,
message: '请输入旧密码',
message: t('views.profile.message_old_password_required'),
trigger: ['input', 'blur', 'change'],
},
],
new_password: [
{
required: true,
message: '请输入新密码',
message: t('views.profile.message_new_password_required'),
trigger: ['input', 'blur', 'change'],
},
],
confirm_password: [
{
required: true,
message: '请再次输入密码',
message: t('views.profile.message_password_confirmation_required'),
trigger: ['input', 'blur'],
},
{
validator: validatePasswordStartWith,
message: '两次密码输入不一致',
message: t('views.profile.message_password_confirmation_diff'),
trigger: 'input',
},
{
validator: validatePasswordSame,
message: '两次密码输入不一致',
message: t('views.profile.message_password_confirmation_diff'),
trigger: ['blur', 'password-input'],
},
],
@ -121,7 +122,7 @@ function validatePasswordSame(rule, value) {
<template>
<CommonPage :show-header="false">
<NTabs type="line" animated>
<NTabPane name="website" tab="修改信息">
<NTabPane name="website" :tab="$t('views.profile.label_modify_information')">
<div class="m-30 flex items-center">
<NForm
ref="infoFormRef"
@ -132,56 +133,56 @@ function validatePasswordSame(rule, value) {
:rules="infoFormRules"
class="w-400"
>
<NFormItem label="头像" path="avatar">
<NFormItem :label="$t('views.profile.label_avatar')" path="avatar">
<NImage width="100" :src="infoForm.avatar"></NImage>
</NFormItem>
<NFormItem label="用户姓名" path="username">
<NInput v-model:value="infoForm.username" type="text" placeholder="请填写姓名" />
<NFormItem :label="$t('views.profile.label_username')" path="username">
<NInput v-model:value="infoForm.username" type="text" :placeholder="$t('views.profile.placeholder_username')" />
</NFormItem>
<NFormItem label="邮箱" path="email">
<NInput v-model:value="infoForm.email" type="text" placeholder="请填写邮箱" />
<NFormItem :label="$t('views.profile.label_email')" path="email">
<NInput v-model:value="infoForm.email" type="text" :placeholder="$t('views.profile.placeholder_email')" />
</NFormItem>
<NButton type="primary" :loading="isLoading" @click="updateProfile"> 修改 </NButton>
<NButton type="primary" :loading="isLoading" @click="updateProfile"> {{$t("common.buttons.update")}} </NButton>
</NForm>
</div>
</NTabPane>
<NTabPane name="contact" tab="修改密码">
<NTabPane name="contact" :tab="$t('views.profile.label_change_password')">
<NForm
ref="passwordFormRef"
label-placement="left"
label-align="left"
:model="passwordForm"
label-width="100"
label-width="200"
:rules="passwordFormRules"
class="m-30 w-400"
class="m-30 w-500"
>
<NFormItem label="旧密码" path="old_password">
<NFormItem :label="$t('views.profile.label_old_password')" path="old_password">
<NInput
v-model:value="passwordForm.old_password"
type="password"
show-password-on="mousedown"
placeholder="请输入旧密码"
:placeholder="$t('views.profile.placeholder_old_password')"
/>
</NFormItem>
<NFormItem label="新密码" path="new_password">
<NFormItem :label="$t('views.profile.label_new_password')" path="new_password">
<NInput
v-model:value="passwordForm.new_password"
:disabled="!passwordForm.old_password"
type="password"
show-password-on="mousedown"
placeholder="请输入新密码"
:placeholder="$t('views.profile.placeholder_new_password')"
/>
</NFormItem>
<NFormItem label="确认密码" path="confirm_password">
<NFormItem :label="$t('views.profile.label_confirm_password')" path="confirm_password">
<NInput
v-model:value="passwordForm.confirm_password"
:disabled="!passwordForm.new_password"
type="password"
show-password-on="mousedown"
placeholder="请再次输入新密码"
:placeholder="$t('views.profile.placeholder_confirm_password')"
/>
</NFormItem>
<NButton type="primary" :loading="isLoading" @click="updatePassword"> 修改 </NButton>
<NButton type="primary" :loading="isLoading" @click="updatePassword"> {{$t("common.buttons.update")}} </NButton>
</NForm>
</NTabPane>
</NTabs>

View File

@ -6,8 +6,8 @@
<div flex items-center>
<img rounded-full width="60" :src="userStore.avatar" />
<div ml-10>
<p text-20 font-semibold>hello, {{ userStore.name }}</p>
<p mt-5 text-14 op-60>今天又是元气满满的一天!</p>
<p text-20 font-semibold> {{ $t('views.workbench.text_hello', {username: userStore.name}) }}</p>
<p mt-5 text-14 op-60>{{ $t('views.workbench.text_welcome') }}</p>
</div>
</div>
<n-space :size="12" :wrap="false">
@ -16,9 +16,9 @@
</div>
</n-card>
<n-card title="项目" size="small" :segmented="true" mt-15 rounded-10>
<n-card :title="$t('views.workbench.label_project')" size="small" :segmented="true" mt-15 rounded-10>
<template #header-extra>
<n-button text type="primary">更多</n-button>
<n-button text type="primary">{{$t('views.workbench.label_more')}}</n-button>
</template>
<div flex flex-wrap justify-between>
<n-card
@ -29,7 +29,7 @@
title="Vue FastAPI Admin"
size="small"
>
<p op-60>一个基于 Vue3.0FastAPINaive UI 的轻量级后台管理模板</p>
<p op-60>{{dummyText}}</p>
</n-card>
</div>
</n-card>
@ -39,24 +39,28 @@
<script setup>
import { useUserStore } from '@/store'
import { useI18n } from 'vue-i18n'
const statisticData = [
const dummyText = "一个基于 Vue3.0、FastAPI、Naive UI 的轻量级后台管理模板"
const {t} = useI18n({ useScope: "global" })
const statisticData = computed(() => [
{
id: 0,
label: '项目数',
label: t('views.workbench.label_number_of_items'),
value: '25',
},
{
id: 1,
label: '待办',
label: t('views.workbench.label_upcoming'),
value: '4/16',
},
{
id: 2,
label: '消息',
label: t('views.workbench.label_information'),
value: '12',
},
]
])
const userStore = useUserStore()
</script>