随着系统走向国际化,支持多语言成为必备功能。先前已经成功的把若依的管理系统更换名字成功,由于若依框架提供了良好的国际化(i18n)支持,本文将详细介绍如何为若依前后端分离版添加英语支持,实现中英文切换。
安装vue-i18n插件
根据您的Vue版本安装对应的vue-i18n插件:
html
npm install --save vue-i18n
参考若依的官方手册,下面是完整的步骤:
前端国际化流程
1、package.json
中dependencies
节点添加vue-i18n
"vue-i18n": "7.3.2",
2、src
目录下创建lang目录,存放国际化文件
此处包含三个文件,分别是 index.js
zh.js
en.js
javascript
// index.js
import Vue from 'vue'
import VueI18n from 'vue-i18n'
import Cookies from 'js-cookie'
import elementEnLocale from 'element-ui/lib/locale/lang/en' // element-ui lang
import elementZhLocale from 'element-ui/lib/locale/lang/zh-CN'// element-ui lang
import enLocale from './en'
import zhLocale from './zh'
Vue.use(VueI18n)
const messages = {
en_US: {
...enLocale,
...elementEnLocale
},
zh_CN: {
...zhLocale,
...elementZhLocale
}
}
const i18n = new VueI18n({
// 设置语言 选项 en | zh
locale: Cookies.get('language') || 'zh_CN',
// 设置文本内容
messages
})
export default i18n
javascript
// zh.js
export default {
login: {
title: '若依后台管理系统',
logIn: '登录',
username: '账号',
password: '密码',
code: '验证码',
rememberMe: '记住密码'
},
tagsView: {
refresh: '刷新',
close: '关闭',
closeOthers: '关闭其它',
closeAll: '关闭所有'
},
settings: {
title: '系统布局配置',
theme: '主题色',
tagsView: '开启 Tags-View',
fixedHeader: '固定 Header',
sidebarLogo: '侧边栏 Logo'
}
}
javascript
// en.js
export default {
login: {
title: 'RuoYi Login Form',
logIn: 'Login in',
username: 'Username',
password: 'Password',
code: 'Code',
rememberMe: 'Remember Me'
},
tagsView: {
refresh: 'Refresh',
close: 'Close',
closeOthers: 'Close Others',
closeAll: 'Close All'
},
settings: {
title: 'Page style setting',
theme: 'Theme Color',
tagsView: 'Open Tags-View',
fixedHeader: 'Fixed Header',
sidebarLogo: 'Sidebar Logo'
}
}
3、在src/main.js
中增量添加i18n
javascript
import i18n from './lang'
Vue.use(Element, {
i18n: (key, value) => i18n.t(key, value),
size: Cookies.get('size') || 'medium'
})
Vue.config.productionTip = false
new Vue({
el: '#app',
router,
store,
i18n,
render: h => h(App)
})
4、在src/store/getters.js
中添加language
language: state => state.app.language,
5、在src/store/modules/app.js
中增量添加i18n
javascript
const state = {
language: Cookies.get('language') || 'en'
}
const mutations = {
SET_LANGUAGE: (state, language) => {
state.language = language
Cookies.set('language', language)
}
}
const actions = {
setLanguage({ commit }, language) {
commit('SET_LANGUAGE', language)
}
}
6、在src/components/LangSelect/index.vue
中创建汉化组件
html
<template>
<el-dropdown trigger="click" class="international" @command="handleSetLanguage">
<div>
<svg-icon class-name="international-icon" icon-class="language" />
</div>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item :disabled="language==='zh_CN'" command="zh_CN">
中文
</el-dropdown-item>
<el-dropdown-item :disabled="language==='en_US'" command="en_US">
English
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</template>
<script>
import { changeLanguage } from "@/api/login";
export default {
computed: {
language() {
return this.$store.getters.language
}
},
methods: {
handleSetLanguage(value) {
this.$i18n.locale = value
this.$store.dispatch('app/setLanguage', value)
this.$message({ message: '设置语言成功', type: 'success' })
changeLanguage(value).then(response => {
window.location.reload();
});
}
}
}
</script>
7、在login.js
新增修改语言方法
javascript
// 修改语言
export function changeLanguage(lang){
return request({
url: '/changeLanguage',
method: 'get',
headers: {
isToken: false,
},
params: {
lang: lang
}
})
}
8、登录页面汉化
html
<template>
<div class="login">
<el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form">
<h3 class="title">{{ $t('login.title') }}</h3>
<lang-select class="set-language" />
<el-form-item prop="username">
<el-input
v-model="loginForm.username"
type="text"
auto-complete="off"
:placeholder="$t('login.username')"
>
<svg-icon slot="prefix" icon-class="user" class="el-input__icon input-icon" />
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="loginForm.password"
type="password"
auto-complete="off"
:placeholder="$t('login.password')"
@keyup.enter.native="handleLogin"
>
<svg-icon slot="prefix" icon-class="password" class="el-input__icon input-icon" />
</el-input>
</el-form-item>
<el-form-item prop="code" v-if="captchaEnabled">
<el-input
v-model="loginForm.code"
auto-complete="off"
:placeholder="$t('login.code')"
style="width: 63%"
@keyup.enter.native="handleLogin"
>
<svg-icon slot="prefix" icon-class="validCode" class="el-input__icon input-icon" />
</el-input>
<div class="login-code">
<img :src="codeUrl" @click="getCode" class="login-code-img"/>
</div>
</el-form-item>
<el-checkbox v-model="loginForm.rememberMe" style="margin:0px 0px 25px 0px;">{{ $t('login.rememberMe') }}</el-checkbox>
<el-form-item style="width:100%;">
<el-button
:loading="loading"
size="medium"
type="primary"
style="width:100%;"
@click.native.prevent="handleLogin"
>
<span v-if="!loading">{{ $t('login.logIn') }}</span>
<span v-else>登 录 中...</span>
</el-button>
<div style="float: right;" v-if="register">
<router-link class="link-type" :to="'/register'">立即注册</router-link>
</div>
</el-form-item>
</el-form>
<!-- 底部 -->
<div class="el-login-footer">
<span>Copyright © 2018-2024 ruoyi.vip All Rights Reserved.</span>
</div>
</div>
</template>
<script>
import LangSelect from '@/components/LangSelect'
import { getCodeImg } from "@/api/login";
import Cookies from "js-cookie";
import { encrypt, decrypt } from '@/utils/jsencrypt'
export default {
name: "Login",
components: { LangSelect },
data() {
return {
codeUrl: "",
loginForm: {
username: "admin",
password: "admin123",
rememberMe: false,
code: "",
uuid: ""
},
loginRules: {
username: [
{ required: true, trigger: "blur", message: "请输入您的账号" }
],
password: [
{ required: true, trigger: "blur", message: "请输入您的密码" }
],
code: [{ required: true, trigger: "change", message: "请输入验证码" }]
},
loading: false,
// 验证码开关
captchaEnabled: true,
// 注册开关
register: false,
redirect: undefined
};
},
watch: {
$route: {
handler: function(route) {
this.redirect = route.query && route.query.redirect;
},
immediate: true
}
},
created() {
this.getCode();
this.getCookie();
},
methods: {
getCode() {
getCodeImg().then(res => {
this.captchaEnabled = res.captchaEnabled === undefined ? true : res.captchaEnabled;
if (this.captchaEnabled) {
this.codeUrl = "data:image/gif;base64," + res.img;
this.loginForm.uuid = res.uuid;
}
});
},
getCookie() {
const username = Cookies.get("username");
const password = Cookies.get("password");
const rememberMe = Cookies.get('rememberMe')
this.loginForm = {
username: username === undefined ? this.loginForm.username : username,
password: password === undefined ? this.loginForm.password : decrypt(password),
rememberMe: rememberMe === undefined ? false : Boolean(rememberMe)
};
},
handleLogin() {
this.$refs.loginForm.validate(valid => {
if (valid) {
this.loading = true;
if (this.loginForm.rememberMe) {
Cookies.set("username", this.loginForm.username, { expires: 30 });
Cookies.set("password", encrypt(this.loginForm.password), { expires: 30 });
Cookies.set('rememberMe', this.loginForm.rememberMe, { expires: 30 });
} else {
Cookies.remove("username");
Cookies.remove("password");
Cookies.remove('rememberMe');
}
this.$store.dispatch("Login", this.loginForm).then(() => {
this.$router.push({ path: this.redirect || "/" }).catch(()=>{});
}).catch(() => {
this.loading = false;
if (this.captchaEnabled) {
this.getCode();
}
});
}
});
}
}
};
</script>
<style rel="stylesheet/scss" lang="scss">
.login {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
background-image: url("../assets/images/login-background.jpg");
background-size: cover;
}
.title {
margin: 0px auto 30px auto;
text-align: center;
color: #707070;
}
.login-form {
border-radius: 6px;
background: #ffffff;
width: 400px;
padding: 25px 25px 5px 25px;
.el-input {
height: 38px;
input {
height: 38px;
}
}
.input-icon {
height: 39px;
width: 14px;
margin-left: 2px;
}
}
.login-tip {
font-size: 13px;
text-align: center;
color: #bfbfbf;
}
.login-code {
width: 33%;
height: 38px;
float: right;
img {
cursor: pointer;
vertical-align: middle;
}
}
.el-login-footer {
height: 40px;
line-height: 40px;
position: fixed;
bottom: 0;
width: 100%;
text-align: center;
color: #fff;
font-family: Arial;
font-size: 12px;
letter-spacing: 1px;
}
.login-code-img {
height: 38px;
}
</style>
到这里是刚刚开始哦,目前只是把基础打好了
后台国际化流程
如果不做接口返回的数据国际化的话,做下面两步就够了。
1、在SysLoginController.java新增修改语言方法
java
@GetMapping("/changeLanguage")
public AjaxResult changeLanguage(String lang)
{
return AjaxResult.success();
}
2、在SecurityConfig.java允许匿名访问此方法
java
.antMatchers("/changeLanguage").permitAll()
页面国际化
上面都是官方手册中的基础配置,现在开始替换各页面和组件的部分
切换语言组件
官方的切换组件不太好看,下面是我从其他博客中找到的简单的组件,如果不需要可以跳过这一步;组件效果如下:

代码如下:
汉化组件
在src/components/LangSwitch/index.vue
中创建汉化组件
html
<template>
<el-dropdown
trigger="click"
class="dingtalk-language-switcher"
@command="handleSetLanguage"
>
<div class="language-trigger">
<span class="current-language">
{{ currentLanguage.label }}
</span>
<i class="el-icon-arrow-down"></i>
</div>
<el-dropdown-menu slot="dropdown" class="language-menu">
<el-dropdown-item
v-for="lang in languages"
:key="lang.value"
:command="lang.value"
:class="{ 'active': language === lang.value }"
>
{{ lang.label }}
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</template>
<script>
import { changeLanguage } from "@/api/login";
export default {
data() {
return {
languages: [
{ value: 'en_US', label: 'English' },
{ value: 'zh_CN', label: '简体中文' }
// 可以继续添加其他语言
]
}
},
computed: {
language() {
return this.$store.getters.language || 'zh_CN'
},
currentLanguage() {
return this.languages.find(lang => lang.value === this.language) || this.languages[0]
}
},
methods: {
handleSetLanguage(lang) {
if (this.language === lang) return;
this.$i18n.locale = lang;
this.$store.dispatch('app/setLanguage', lang);
changeLanguage(lang).then(() => {
this.$message.success(this.$t('navbar.switchLanguageSuccess'));
}).catch(() => {
this.$message.error(this.$t('navbar.switchLanguageFailed'));
});
}
}
}
</script>
<style lang="scss" scoped>
.dingtalk-language-switcher {
cursor: pointer;
margin: 0 10px;
font-size: 14px;
color: #333;
.language-trigger {
display: flex;
align-items: center;
padding: 0 5px;
height: 100%;
.current-language {
margin-right: 4px;
}
.el-icon-arrow-down {
font-size: 12px;
color: #999;
transition: transform 0.3s;
}
}
&:hover {
color: var(--el-color-primary);
.el-icon-arrow-down {
color: var(--el-color-primary);
}
}
// 下拉菜单展开时箭头旋转
&.is-active {
.el-icon-arrow-down {
transform: rotate(180deg);
}
}
}
.language-menu {
min-width: 160px;
padding: 5px 0;
.el-dropdown-menu__item {
padding: 0 16px;
line-height: 36px;
font-size: 14px;
&:hover {
color: var(--el-color-primary);
background-color: #f5f5f5;
}
&.active {
color: var(--el-color-primary);
background-color: #f0f7ff;
}
}
}
</style>
登录界面修改
src/views/login.vue
html
<template>
<div class="login">
<el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form">
<h3 class="title">{{ $t('login.title') }}</h3>
<el-form-item prop="username">
<el-input
v-model="loginForm.username"
type="text"
auto-complete="off"
:placeholder="$t('login.username')"
>
<svg-icon slot="prefix" icon-class="user" class="el-input__icon input-icon" />
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="loginForm.password"
type="password"
auto-complete="off"
:placeholder="$t('login.password')"
@keyup.enter.native="handleLogin"
>
<svg-icon slot="prefix" icon-class="password" class="el-input__icon input-icon" />
</el-input>
</el-form-item>
<el-form-item prop="code" v-if="captchaEnabled">
<el-input
v-model="loginForm.code"
auto-complete="off"
:placeholder="$t('login.code')"
style="width: 63%"
@keyup.enter.native="handleLogin"
>
<svg-icon slot="prefix" icon-class="validCode" class="el-input__icon input-icon" />
</el-input>
<div class="login-code">
<img :src="codeUrl" @click="getCode" class="login-code-img"/>
</div>
</el-form-item>
<!-- 底部选项区域 -->
<div class="login-options">
<div class="options-left">
<el-checkbox v-model="loginForm.rememberMe" style="margin:0px 0px 25px 0px;">{{ $t('login.rememberMe') }}</el-checkbox>
</div>
<div class="options-right" style="margin:0px 0px 25px 0px;">
<lang-switch />
</div>
</div>
<el-form-item style="width:100%;">
<el-button
:loading="loading"
size="medium"
type="primary"
style="width:100%;"
@click.native.prevent="handleLogin"
>
<span v-if="!loading">{{ $t('login.logIn') }}</span>
<span v-else>登 录 中...</span>
</el-button>
<div style="float: right;" v-if="register">
<router-link class="link-type" :to="'/register'">立即注册</router-link>
</div>
</el-form-item>
</el-form>
<!-- 底部 -->
<div class="el-login-footer">
<span>Copyright © 2018-2024 ruoyi.vip All Rights Reserved.</span>
</div>
</div>
</template>
<script>
import LangSwitch from '@/components/LangSwitch'
import { getCodeImg } from "@/api/login";
import Cookies from "js-cookie";
import { encrypt, decrypt } from '@/utils/jsencrypt'
export default {
name: "Login",
components: { LangSwitch },
data() {
return {
codeUrl: "",
loginForm: {
username: "admin",
password: "admin123",
rememberMe: false,
code: "",
uuid: ""
},
loginRules: {
username: [
{ required: true, trigger: "blur", message: "请输入您的账号" }
],
password: [
{ required: true, trigger: "blur", message: "请输入您的密码" }
],
code: [{ required: true, trigger: "change", message: "请输入验证码" }]
},
loading: false,
// 验证码开关
captchaEnabled: true,
// 注册开关
register: false,
redirect: undefined
};
},
watch: {
$route: {
handler: function(route) {
this.redirect = route.query && route.query.redirect;
},
immediate: true
}
},
created() {
this.getCode();
this.getCookie();
},
methods: {
getCode() {
getCodeImg().then(res => {
this.captchaEnabled = res.captchaEnabled === undefined ? true : res.captchaEnabled;
if (this.captchaEnabled) {
this.codeUrl = "data:image/gif;base64," + res.img;
this.loginForm.uuid = res.uuid;
}
});
},
getCookie() {
const username = Cookies.get("username");
const password = Cookies.get("password");
const rememberMe = Cookies.get('rememberMe')
this.loginForm = {
username: username === undefined ? this.loginForm.username : username,
password: password === undefined ? this.loginForm.password : decrypt(password),
rememberMe: rememberMe === undefined ? false : Boolean(rememberMe)
};
},
handleLogin() {
this.$refs.loginForm.validate(valid => {
if (valid) {
this.loading = true;
if (this.loginForm.rememberMe) {
Cookies.set("username", this.loginForm.username, { expires: 30 });
Cookies.set("password", encrypt(this.loginForm.password), { expires: 30 });
Cookies.set('rememberMe', this.loginForm.rememberMe, { expires: 30 });
} else {
Cookies.remove("username");
Cookies.remove("password");
Cookies.remove('rememberMe');
}
this.$store.dispatch("Login", this.loginForm).then(() => {
this.$router.push({ path: this.redirect || "/" }).catch(()=>{});
}).catch(() => {
this.loading = false;
if (this.captchaEnabled) {
this.getCode();
}
});
}
});
}
}
};
</script>
<style rel="stylesheet/scss" lang="scss">
.login {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
background-image: url("../assets/images/login-background.jpg");
background-size: cover;
}
.title {
margin: 0px auto 30px auto;
text-align: center;
color: #707070;
}
.login-form {
border-radius: 6px;
background: #ffffff;
width: 400px;
padding: 25px 25px 5px 25px;
.el-input {
height: 38px;
input {
height: 38px;
}
}
.input-icon {
height: 39px;
width: 14px;
margin-left: 2px;
}
}
.login-tip {
font-size: 13px;
text-align: center;
color: #bfbfbf;
}
.login-code {
width: 33%;
height: 38px;
float: right;
img {
cursor: pointer;
vertical-align: middle;
}
}
.el-login-footer {
height: 40px;
line-height: 40px;
position: fixed;
bottom: 0;
width: 100%;
text-align: center;
color: #fff;
font-family: Arial;
font-size: 12px;
letter-spacing: 1px;
}
.login-code-img {
height: 38px;
}
.login-options {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 25px;
height: 20px; /* 确保与复选框高度一致 */
line-height: 20px; /* 确保文字垂直居中 */
.options-left {
/* 左侧样式保持原样 */
.el-checkbox {
margin: 0;
}
}
.options-right {
/* 语言切换器样式 */
.language-switcher {
display: inline-block;
.language-trigger {
display: flex;
align-items: center;
color: #606266;
font-size: 14px;
cursor: pointer;
.current-language {
margin-right: 5px;
}
.el-icon-arrow-down {
font-size: 12px;
color: #c0c4cc;
transition: transform 0.2s;
}
}
&:hover {
.language-trigger {
color: var(--el-color-primary);
.el-icon-arrow-down {
color: var(--el-color-primary);
}
}
}
&.is-active {
.el-icon-arrow-down {
transform: rotate(180deg);
}
}
}
}
}
</style>
导航栏部分
在src\layout\components\Navbar.vue引入我们之前写好的组件
javascript
<lang-switch class="right-menu-item hover-effect"/>
import LangSwitch from '@/components/LangSwitch'
export default {
components: {
LangSwitch
},
}
完整代码如下:
html
<template>
<div class="navbar">
<hamburger id="hamburger-container" :is-active="sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" />
<breadcrumb v-if="!topNav" id="breadcrumb-container" class="breadcrumb-container" />
<top-nav v-if="topNav" id="topmenu-container" class="topmenu-container" />
<div class="right-menu">
<template v-if="device!=='mobile'">
<search id="header-search" class="right-menu-item" />
<screenfull id="screenfull" class="right-menu-item hover-effect" />
<el-tooltip content="布局大小" effect="dark" placement="bottom">
<size-select id="size-select" class="right-menu-item hover-effect" />
</el-tooltip>
<lang-switch class="right-menu-item hover-effect"/>
</template>
<el-dropdown class="avatar-container right-menu-item hover-effect" trigger="hover">
<div class="avatar-wrapper">
<img :src="avatar" class="user-avatar">
<span class="user-nickname"> {{ nickName }} </span>
</div>
<el-dropdown-menu slot="dropdown">
<router-link to="/user/profile">
<el-dropdown-item>{{ $t('navbar.personalCenter') }}</el-dropdown-item>
</router-link>
<el-dropdown-item @click.native="setLayout" v-if="setting">
<span>{{ $t('navbar.layoutSettings') }}</span>
</el-dropdown-item>
<el-dropdown-item divided @click.native="logout">
<span>{{ $t('navbar.logOut') }}</span>
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import Breadcrumb from '@/components/Breadcrumb'
import TopNav from '@/components/TopNav'
import Hamburger from '@/components/Hamburger'
import Screenfull from '@/components/Screenfull'
import SizeSelect from '@/components/SizeSelect'
import Search from '@/components/HeaderSearch'
import LangSwitch from '@/components/LangSwitch'
export default {
emits: ['setLayout'],
components: {
Breadcrumb,
TopNav,
Hamburger,
Screenfull,
SizeSelect,
Search,
LangSwitch
},
computed: {
...mapGetters([
'sidebar',
'avatar',
'device',
'nickName'
]),
setting: {
get() {
return this.$store.state.settings.showSettings
}
},
topNav: {
get() {
return this.$store.state.settings.topNav
}
}
},
methods: {
toggleSideBar() {
this.$store.dispatch('app/toggleSideBar')
},
setLayout(event) {
this.$emit('setLayout')
},
logout() {
this.$confirm('确定注销并退出系统吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$store.dispatch('LogOut').then(() => {
location.href = '/index'
})
}).catch(() => {})
}
}
}
</script>
<style lang="scss" scoped>
.navbar {
height: 50px;
overflow: hidden;
position: relative;
background: #fff;
box-shadow: 0 1px 4px rgba(0,21,41,.08);
.hamburger-container {
line-height: 46px;
height: 100%;
float: left;
cursor: pointer;
transition: background .3s;
-webkit-tap-highlight-color:transparent;
&:hover {
background: rgba(0, 0, 0, .025)
}
}
.breadcrumb-container {
float: left;
}
.topmenu-container {
position: absolute;
left: 50px;
}
.errLog-container {
display: inline-block;
vertical-align: top;
}
.right-menu {
float: right;
height: 100%;
line-height: 50px;
&:focus {
outline: none;
}
.right-menu-item {
display: inline-block;
padding: 0 8px;
height: 100%;
font-size: 18px;
color: #5a5e66;
vertical-align: text-bottom;
&.hover-effect {
cursor: pointer;
transition: background .3s;
&:hover {
background: rgba(0, 0, 0, .025)
}
}
}
.avatar-container {
margin-right: 0px;
padding-right: 0px;
.avatar-wrapper {
margin-top: 10px;
right: 8px;
position: relative;
.user-avatar {
cursor: pointer;
width: 30px;
height: 30px;
border-radius: 50%;
}
.user-nickname{
position: relative;
bottom: 10px;
left: 2px;
font-size: 14px;
font-weight: bold;
}
.el-icon-caret-bottom {
cursor: pointer;
position: absolute;
right: -20px;
top: 25px;
font-size: 12px;
}
}
}
}
}
</style>
侧边栏菜单
菜单的显示来源有两个,路由和菜单管理。所以这两个都需要分别进行国际化的修改
路由国际化
src\router\index.js,以首页为例:修改title部分
javascript
{
path: '',
component: Layout,
redirect: 'index',
children: [
{
path: 'index',
component: () => import('@/views/index'),
name: 'Index',
meta: { title: 'route.home', icon: 'dashboard', affix: true }
}
]
}
在语言包中分别添加这个值
javascript
//zh.js
route: {
home: '首页',
personalCenter: '个人中心',
}
//en.js
route: {
home: 'Home',
personalCenter: 'Personal Center',
},
修改侧边栏文件(src\layout\components\Sidebar\SidebarItem.vue)
主要是menusTitle方法,找到翻译语言包会替换翻译,没找到还是显示原文。
javascript
menusTitle(item) {
if (this.$te(item)) {
return this.$t(item);
} else {
return item;
}
},
完整代码如下:
html
<template>
<div v-if="!item.hidden">
<template
v-if="hasOneShowingChild(item.children, item) && (!onlyOneChild.children || onlyOneChild.noShowingChildren) && !item.alwaysShow">
<app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path, onlyOneChild.query)">
<el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{ 'submenu-title-noDropdown': !isNest }">
<item :icon="onlyOneChild.meta.icon || (item.meta && item.meta.icon)"
:title="menusTitle(onlyOneChild.meta.title)" />
</el-menu-item>
</app-link>
</template>
<el-submenu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body>
<template slot="title">
<item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="menusTitle(item.meta.title)" />
</template>
<sidebar-item v-for="(child, index) in item.children" :key="child.path + index" :is-nest="true" :item="child"
:base-path="resolvePath(child.path)" class="nest-menu" />
</el-submenu>
</div>
</template>
<script>
import path from 'path'
import { isExternal } from '@/utils/validate'
import Item from './Item'
import AppLink from './Link'
import FixiOSBug from './FixiOSBug'
export default {
name: 'SidebarItem',
components: { Item, AppLink },
mixins: [FixiOSBug],
props: {
// route object
item: {
type: Object,
required: true
},
isNest: {
type: Boolean,
default: false
},
basePath: {
type: String,
default: ''
}
},
data() {
this.onlyOneChild = null
return {}
},
methods: {
menusTitle(item) {
if (this.$te(item)) {
return this.$t(item);
} else {
return item;
}
},
hasOneShowingChild(children = [], parent) {
if (!children) {
children = [];
}
const showingChildren = children.filter(item => {
if (item.hidden) {
return false
}
// Temp set(will be used if only has one showing child)
this.onlyOneChild = item
return true
})
// When there is only one child router, the child router is displayed by default
if (showingChildren.length === 1) {
return true
}
// Show parent if there are no child router to display
if (showingChildren.length === 0) {
this.onlyOneChild = { ...parent, path: '', noShowingChildren: true }
return true
}
return false
},
resolvePath(routePath, routeQuery) {
if (isExternal(routePath)) {
return routePath
}
if (isExternal(this.basePath)) {
return this.basePath
}
if (routeQuery) {
let query = JSON.parse(routeQuery);
return { path: path.resolve(this.basePath, routePath), query: query }
}
return path.resolve(this.basePath, routePath)
}
}
}
</script>
菜单管理国际化
不是每个菜单都在(src\router\index.js)中配置了路由,有的只是在菜单管理里新增了一下,那么这种菜单如何显示为英文呢,请看下图,把菜单名称改成动态的语言包属性即可。

面包屑
src\components\Breadcrumb\index.vue
html
<template>
<el-breadcrumb class="app-breadcrumb" separator="/">
<transition-group name="breadcrumb">
<el-breadcrumb-item v-for="(item, index) in levelList" :key="item.path">
<span v-if="item.redirect === 'noRedirect' || index == levelList.length - 1" class="no-redirect">{{ $t(item.meta.title) }}</span>
<a v-else @click.prevent="handleLink(item)">{{ $t(item.meta.title) }}</a>
</el-breadcrumb-item>
</transition-group>
</el-breadcrumb>
</template>
<script>
export default {
data() {
return {
levelList: null
}
},
watch: {
$route(route) {
// if you go to the redirect page, do not update the breadcrumbs
if (route.path.startsWith('/redirect/')) {
return
}
this.getBreadcrumb()
}
},
created() {
this.getBreadcrumb()
},
methods: {
getBreadcrumb() {
// only show routes with meta.title
let matched = []
const router = this.$route
const pathNum = this.findPathNum(router.path)
// multi-level menu
if (pathNum > 2) {
const reg = /\/\w+/gi
const pathList = router.path.match(reg).map((item, index) => {
if (index !== 0) item = item.slice(1)
return item
})
this.getMatched(pathList, this.$store.getters.defaultRoutes, matched)
} else {
matched = router.matched.filter(item => item.meta && this.$t(item.meta.title))
}
// 判断是否为首页
if (!this.isDashboard(matched[0])) {
matched = [{ path: "/index", meta: { title: 'route.home'} }].concat(matched)
}
this.levelList = matched.filter(item => item.meta && this.$t(item.meta.title) && item.meta.breadcrumb !== false)
},
findPathNum(str, char = "/") {
let index = str.indexOf(char)
let num = 0
while (index !== -1) {
num++
index = str.indexOf(char, index + 1)
}
return num
},
getMatched(pathList, routeList, matched) {
let data = routeList.find(item => item.path == pathList[0] || (item.name += '').toLowerCase() == pathList[0])
if (data) {
matched.push(data)
if (data.children && pathList.length) {
pathList.shift()
this.getMatched(pathList, data.children, matched)
}
}
},
isDashboard(route) {
const name = route && route.name
if (!name) {
return false
}
return name.trim() === 'Index'
},
handleLink(item) {
const { redirect, path } = item
if (redirect) {
this.$router.push(redirect)
return
}
this.$router.push(path)
}
}
}
</script>
<style lang="scss" scoped>
.app-breadcrumb.el-breadcrumb {
display: inline-block;
font-size: 14px;
line-height: 50px;
margin-left: 8px;
.no-redirect {
color: #97a8be;
cursor: text;
}
}
</style>

tagsView
src\layout\components\TagsView\index.vue
html
<template>
<div id="tags-view-container" class="tags-view-container">
<scroll-pane ref="scrollPane" class="tags-view-wrapper" @scroll="handleScroll">
<router-link v-for="tag in visitedViews" ref="tag" :key="tag.path" :class="isActive(tag) ? 'active' : ''"
:to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }" tag="span" class="tags-view-item"
:style="activeStyle(tag)" @click.middle.native="!isAffix(tag) ? closeSelectedTag(tag) : ''"
@contextmenu.prevent.native="openMenu(tag, $event)">
{{ menusTitle(tag.title) }}
<span v-if="!isAffix(tag)" class="el-icon-close" @click.prevent.stop="closeSelectedTag(tag)" />
</router-link>
</scroll-pane>
<ul v-show="visible" :style="{ left: left + 'px', top: top + 'px' }" class="contextmenu">
<li @click="refreshSelectedTag(selectedTag)"><i class="el-icon-refresh-right"></i> {{ $t('tagsView.refresh') }}</li>
<li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)"><i class="el-icon-close"></i> {{ $t('tagsView.close') }}</li>
<li @click="closeOthersTags"><i class="el-icon-circle-close"></i> {{ $t('tagsView.closeOthers') }}</li>
<li v-if="!isFirstView()" @click="closeLeftTags"><i class="el-icon-back"></i> {{ $t('tagsView.closeLeft') }}</li>
<li v-if="!isLastView()" @click="closeRightTags"><i class="el-icon-right"></i> {{ $t('tagsView.closeRight') }}</li>
<li @click="closeAllTags(selectedTag)"><i class="el-icon-circle-close"></i> {{ $t('tagsView.closeAll') }}</li>
</ul>
</div>
</template>
<script>
import ScrollPane from './ScrollPane'
import path from 'path'
export default {
components: { ScrollPane },
data() {
return {
visible: false,
top: 0,
left: 0,
selectedTag: {},
affixTags: []
}
},
computed: {
visitedViews() {
return this.$store.state.tagsView.visitedViews
},
routes() {
return this.$store.state.permission.routes
},
theme() {
return this.$store.state.settings.theme;
}
},
watch: {
$route() {
this.addTags()
this.moveToCurrentTag()
},
visible(value) {
if (value) {
document.body.addEventListener('click', this.closeMenu)
} else {
document.body.removeEventListener('click', this.closeMenu)
}
}
},
mounted() {
this.initTags()
this.addTags()
},
methods: {
menusTitle(item) {
if (this.$te(item)) {
return this.$t(item);
} else {
return item;
}
},
isActive(route) {
return route.path === this.$route.path
},
activeStyle(tag) {
if (!this.isActive(tag)) return {};
return {
"background-color": this.theme,
"border-color": this.theme
};
},
isAffix(tag) {
return tag.meta && tag.meta.affix
},
isFirstView() {
try {
return this.selectedTag.fullPath === '/index' || this.selectedTag.fullPath === this.visitedViews[1].fullPath
} catch (err) {
return false
}
},
isLastView() {
try {
return this.selectedTag.fullPath === this.visitedViews[this.visitedViews.length - 1].fullPath
} catch (err) {
return false
}
},
filterAffixTags(routes, basePath = '/') {
let tags = []
routes.forEach(route => {
if (route.meta && route.meta.affix) {
const tagPath = path.resolve(basePath, route.path)
tags.push({
fullPath: tagPath,
path: tagPath,
name: route.name,
meta: { ...route.meta }
})
}
if (route.children) {
const tempTags = this.filterAffixTags(route.children, route.path)
if (tempTags.length >= 1) {
tags = [...tags, ...tempTags]
}
}
})
return tags
},
initTags() {
const affixTags = this.affixTags = this.filterAffixTags(this.routes)
for (const tag of affixTags) {
// Must have tag name
if (tag.name) {
this.$store.dispatch('tagsView/addVisitedView', tag)
}
}
},
addTags() {
const { name } = this.$route
if (name) {
this.$store.dispatch('tagsView/addView', this.$route)
}
},
moveToCurrentTag() {
const tags = this.$refs.tag
this.$nextTick(() => {
for (const tag of tags) {
if (tag.to.path === this.$route.path) {
this.$refs.scrollPane.moveToTarget(tag)
// when query is different then update
if (tag.to.fullPath !== this.$route.fullPath) {
this.$store.dispatch('tagsView/updateVisitedView', this.$route)
}
break
}
}
})
},
refreshSelectedTag(view) {
this.$tab.refreshPage(view);
if (this.$route.meta.link) {
this.$store.dispatch('tagsView/delIframeView', this.$route)
}
},
closeSelectedTag(view) {
this.$tab.closePage(view).then(({ visitedViews }) => {
if (this.isActive(view)) {
this.toLastView(visitedViews, view)
}
})
},
closeRightTags() {
this.$tab.closeRightPage(this.selectedTag).then(visitedViews => {
if (!visitedViews.find(i => i.fullPath === this.$route.fullPath)) {
this.toLastView(visitedViews)
}
})
},
closeLeftTags() {
this.$tab.closeLeftPage(this.selectedTag).then(visitedViews => {
if (!visitedViews.find(i => i.fullPath === this.$route.fullPath)) {
this.toLastView(visitedViews)
}
})
},
closeOthersTags() {
this.$router.push(this.selectedTag.fullPath).catch(() => { });
this.$tab.closeOtherPage(this.selectedTag).then(() => {
this.moveToCurrentTag()
})
},
closeAllTags(view) {
this.$tab.closeAllPage().then(({ visitedViews }) => {
if (this.affixTags.some(tag => tag.path === this.$route.path)) {
return
}
this.toLastView(visitedViews, view)
})
},
toLastView(visitedViews, view) {
const latestView = visitedViews.slice(-1)[0]
if (latestView) {
this.$router.push(latestView.fullPath)
} else {
// now the default is to redirect to the home page if there is no tags-view,
// you can adjust it according to your needs.
if (view.name === 'Dashboard') {
// to reload home page
this.$router.replace({ path: '/redirect' + view.fullPath })
} else {
this.$router.push('/')
}
}
},
openMenu(tag, e) {
const menuMinWidth = 105
const offsetLeft = this.$el.getBoundingClientRect().left // container margin left
const offsetWidth = this.$el.offsetWidth // container width
const maxLeft = offsetWidth - menuMinWidth // left boundary
const left = e.clientX - offsetLeft + 15 // 15: margin right
if (left > maxLeft) {
this.left = maxLeft
} else {
this.left = left
}
this.top = e.clientY
this.visible = true
this.selectedTag = tag
},
closeMenu() {
this.visible = false
},
handleScroll() {
this.closeMenu()
}
}
}
</script>
<style lang="scss" scoped>
.tags-view-container {
height: 34px;
width: 100%;
background: #fff;
border-bottom: 1px solid #d8dce5;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, .12), 0 0 3px 0 rgba(0, 0, 0, .04);
.tags-view-wrapper {
.tags-view-item {
display: inline-block;
position: relative;
cursor: pointer;
height: 26px;
line-height: 26px;
border: 1px solid #d8dce5;
color: #495060;
background: #fff;
padding: 0 8px;
font-size: 12px;
margin-left: 5px;
margin-top: 4px;
&:first-of-type {
margin-left: 15px;
}
&:last-of-type {
margin-right: 15px;
}
&.active {
background-color: #42b983;
color: #fff;
border-color: #42b983;
&::before {
content: '';
background: #fff;
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
position: relative;
margin-right: 2px;
}
}
}
}
.contextmenu {
margin: 0;
background: #fff;
z-index: 3000;
position: absolute;
list-style-type: none;
padding: 5px 0;
border-radius: 4px;
font-size: 12px;
font-weight: 400;
color: #333;
box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, .3);
li {
margin: 0;
padding: 7px 16px;
cursor: pointer;
&:hover {
background: #eee;
}
}
}
}
</style>
<style lang="scss">
//reset element css of el-icon-close
.tags-view-wrapper {
.tags-view-item {
.el-icon-close {
width: 16px;
height: 16px;
vertical-align: 2px;
border-radius: 50%;
text-align: center;
transition: all .3s cubic-bezier(.645, .045, .355, 1);
transform-origin: 100% 50%;
&:before {
transform: scale(.6);
display: inline-block;
vertical-align: -3px;
}
&:hover {
background-color: #b4bccc;
color: #fff;
}
}
}
}
</style>

settings组件
src\layout\components\Settings\index.vue
html
<template>
<el-drawer size="280px" :visible="showSettings" :with-header="false" :append-to-body="true" :before-close="closeSetting" :lock-scroll="false">
<div class="drawer-container">
<div>
<div class="setting-drawer-content">
<div class="setting-drawer-title">
<h3 class="drawer-title">{{ $t('settings.title') }}</h3>
</div>
<div class="setting-drawer-block-checbox">
<div class="setting-drawer-block-checbox-item" @click="handleTheme('theme-dark')">
<img src="@/assets/images/dark.svg" alt="dark">
<div v-if="sideTheme === 'theme-dark'" class="setting-drawer-block-checbox-selectIcon" style="display: block;">
<i aria-label="图标: check" class="anticon anticon-check">
<svg viewBox="64 64 896 896" data-icon="check" width="1em" height="1em" :fill="theme" aria-hidden="true" focusable="false" class="">
<path d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 0 0-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z"/>
</svg>
</i>
</div>
</div>
<div class="setting-drawer-block-checbox-item" @click="handleTheme('theme-light')">
<img src="@/assets/images/light.svg" alt="light">
<div v-if="sideTheme === 'theme-light'" class="setting-drawer-block-checbox-selectIcon" style="display: block;">
<i aria-label="图标: check" class="anticon anticon-check">
<svg viewBox="64 64 896 896" data-icon="check" width="1em" height="1em" :fill="theme" aria-hidden="true" focusable="false" class="">
<path d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 0 0-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z"/>
</svg>
</i>
</div>
</div>
</div>
<div class="drawer-item">
<span>{{ $t('settings.theme') }}</span>
<theme-picker style="float: right;height: 26px;margin: -3px 8px 0 0;" @change="themeChange" />
</div>
</div>
<el-divider/>
<h3 class="drawer-title">{{ $t('settings.configuration') }}</h3>
<div class="drawer-item">
<span>{{ $t('settings.topNav') }}</span>
<el-switch v-model="topNav" class="drawer-switch" />
</div>
<div class="drawer-item">
<span>{{ $t('settings.tagsView') }}</span>
<el-switch v-model="tagsView" class="drawer-switch" />
</div>
<div class="drawer-item">
<span>{{ $t('settings.fixedHeader') }}</span>
<el-switch v-model="fixedHeader" class="drawer-switch" />
</div>
<div class="drawer-item">
<span>{{ $t('settings.sidebarLogo') }}</span>
<el-switch v-model="sidebarLogo" class="drawer-switch" />
</div>
<div class="drawer-item">
<span>{{ $t('settings.dynamicTitle') }}</span>
<el-switch v-model="dynamicTitle" class="drawer-switch" />
</div>
<el-divider/>
<el-button size="small" type="primary" plain icon="el-icon-document-add" @click="saveSetting">{{ $t('settings.saveSetting') }}</el-button>
<el-button size="small" plain icon="el-icon-refresh" @click="resetSetting">{{ $t('settings.resetSetting') }}</el-button>
</div>
</div>
</el-drawer>
</template>
<script>
import ThemePicker from '@/components/ThemePicker'
export default {
components: { ThemePicker },
expose: ['openSetting'],
data() {
return {
theme: this.$store.state.settings.theme,
sideTheme: this.$store.state.settings.sideTheme,
showSettings: false
}
},
computed: {
fixedHeader: {
get() {
return this.$store.state.settings.fixedHeader
},
set(val) {
this.$store.dispatch('settings/changeSetting', {
key: 'fixedHeader',
value: val
})
}
},
topNav: {
get() {
return this.$store.state.settings.topNav
},
set(val) {
this.$store.dispatch('settings/changeSetting', {
key: 'topNav',
value: val
})
if (!val) {
this.$store.dispatch('app/toggleSideBarHide', false)
this.$store.commit("SET_SIDEBAR_ROUTERS", this.$store.state.permission.defaultRoutes)
}
}
},
tagsView: {
get() {
return this.$store.state.settings.tagsView
},
set(val) {
this.$store.dispatch('settings/changeSetting', {
key: 'tagsView',
value: val
})
}
},
tagsIcon: {
get() {
return this.$store.state.settings.tagsIcon
},
set(val) {
this.$store.dispatch('settings/changeSetting', {
key: 'tagsIcon',
value: val
})
}
},
sidebarLogo: {
get() {
return this.$store.state.settings.sidebarLogo
},
set(val) {
this.$store.dispatch('settings/changeSetting', {
key: 'sidebarLogo',
value: val
})
}
},
dynamicTitle: {
get() {
return this.$store.state.settings.dynamicTitle
},
set(val) {
this.$store.dispatch('settings/changeSetting', {
key: 'dynamicTitle',
value: val
})
this.$store.dispatch('settings/setTitle', this.$store.state.settings.title)
}
},
footerVisible: {
get() {
return this.$store.state.settings.footerVisible
},
set(val) {
this.$store.dispatch('settings/changeSetting', {
key: 'footerVisible',
value: val
})
}
}
},
methods: {
themeChange(val) {
this.$store.dispatch('settings/changeSetting', {
key: 'theme',
value: val
})
this.theme = val
},
handleTheme(val) {
this.$store.dispatch('settings/changeSetting', {
key: 'sideTheme',
value: val
})
this.sideTheme = val
},
openSetting() {
this.showSettings = true
},
closeSetting(){
this.showSettings = false
},
saveSetting() {
this.$modal.loading("正在保存到本地,请稍候...")
this.$cache.local.set(
"layout-setting",
`{
"topNav":${this.topNav},
"tagsView":${this.tagsView},
"tagsIcon":${this.tagsIcon},
"fixedHeader":${this.fixedHeader},
"sidebarLogo":${this.sidebarLogo},
"dynamicTitle":${this.dynamicTitle},
"footerVisible":${this.footerVisible},
"sideTheme":"${this.sideTheme}",
"theme":"${this.theme}"
}`
)
setTimeout(this.$modal.closeLoading(), 1000)
},
resetSetting() {
this.$modal.loading("正在清除设置缓存并刷新,请稍候...")
this.$cache.local.remove("layout-setting")
setTimeout("window.location.reload()", 1000)
}
}
}
</script>
<style lang="scss" scoped>
.setting-drawer-content {
.setting-drawer-title {
margin-bottom: 12px;
color: rgba(0, 0, 0, .85);
font-size: 14px;
line-height: 22px;
font-weight: bold;
}
.setting-drawer-block-checbox {
display: flex;
justify-content: flex-start;
align-items: center;
margin-top: 10px;
margin-bottom: 20px;
.setting-drawer-block-checbox-item {
position: relative;
margin-right: 16px;
border-radius: 2px;
cursor: pointer;
img {
width: 48px;
height: 48px;
}
.setting-drawer-block-checbox-selectIcon {
position: absolute;
top: 0;
right: 0;
width: 100%;
height: 100%;
padding-top: 15px;
padding-left: 24px;
color: #1890ff;
font-weight: 700;
font-size: 14px;
}
}
}
}
.drawer-container {
padding: 20px;
font-size: 14px;
line-height: 1.5;
word-wrap: break-word;
.drawer-title {
margin-bottom: 12px;
color: rgba(0, 0, 0, .85);
font-size: 14px;
line-height: 22px;
}
.drawer-item {
color: rgba(0, 0, 0, .65);
font-size: 14px;
padding: 12px 0;
}
.drawer-switch {
float: right
}
}
</style>
个人中心
src\views\system\user\profile\index.vue 修改<template>部分即可,把文字内容都替换成语言包中的名字:
html
<template>
<div class="app-container">
<el-row :gutter="20">
<el-col :span="6" :xs="24">
<el-card class="box-card">
<div slot="header" class="clearfix">
<span>{{ $t('profile.personalInformation') }}</span>
</div>
<div>
<div class="text-center">
<userAvatar />
</div>
<ul class="list-group list-group-striped">
<li class="list-group-item">
<svg-icon icon-class="user" />{{ $t('profile.userName') }}
<div class="pull-right">{{ user.userName }}</div>
</li>
<li class="list-group-item">
<svg-icon icon-class="phone" />{{ $t('profile.phonenumber') }}
<div class="pull-right">{{ user.phonenumber }}</div>
</li>
<li class="list-group-item">
<svg-icon icon-class="email" />{{ $t('profile.email') }}
<div class="pull-right">{{ user.email }}</div>
</li>
<li class="list-group-item">
<svg-icon icon-class="tree" />{{ $t('profile.dept') }}
<div class="pull-right" v-if="user.dept">{{ user.dept.deptName }} / {{ postGroup }}</div>
</li>
<li class="list-group-item">
<svg-icon icon-class="peoples" />{{ $t('profile.roleGroup') }}
<div class="pull-right">{{ roleGroup }}</div>
</li>
<li class="list-group-item">
<svg-icon icon-class="date" />{{ $t('profile.createTime') }}
<div class="pull-right">{{ user.createTime }}</div>
</li>
</ul>
</div>
</el-card>
</el-col>
<el-col :span="18" :xs="24">
<el-card>
<div slot="header" class="clearfix">
<span>{{ $t('profile.basicInformation') }}</span>
</div>
<el-tabs v-model="selectedTab">
<el-tab-pane :label="$t('profile.basicInformation')" name="userinfo">
<userInfo :user="user" />
</el-tab-pane>
<el-tab-pane :label="$t('profile.resetPwd')" name="resetPwd">
<resetPwd />
</el-tab-pane>
</el-tabs>
</el-card>
</el-col>
</el-row>
</div>
</template>
src\views\system\user\profile\userInfo.vue 修改<template>部分即可
html
<template>
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
<el-form-item :label="$t('profile.nickName')" prop="nickName">
<el-input v-model="form.nickName" maxlength="30" />
</el-form-item>
<el-form-item :label="$t('profile.phonenumber')" prop="phonenumber">
<el-input v-model="form.phonenumber" maxlength="11" />
</el-form-item>
<el-form-item :label="$t('profile.email')" prop="email">
<el-input v-model="form.email" maxlength="50" />
</el-form-item>
<el-form-item :label="$t('profile.sex')">
<el-radio-group v-model="form.sex">
<el-radio label="0">{{ $t('profile.men') }}</el-radio>
<el-radio label="1">{{ $t('profile.women') }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item>
<el-button type="primary" size="mini" @click="submit">{{ $t('common.save') }}</el-button>
<el-button type="danger" size="mini" @click="close">{{ $t('common.close') }}</el-button>
</el-form-item>
</el-form>
</template>

src\views\system\user\profile\resetPwd.vue 修改<template>部分即可
html
<template>
<el-form ref="form" :model="user" :rules="rules" label-width="80px">
<el-form-item :label="$t('profile.oldPassword')" prop="oldPassword">
<el-input v-model="user.oldPassword" :placeholder="$t('profile.oldPassword_placeholder')" type="password" show-password/>
</el-form-item>
<el-form-item :label="$t('profile.newPassword')" prop="newPassword">
<el-input v-model="user.newPassword" :placeholder="$t('profile.newPassword_placeholder')" type="password" show-password/>
</el-form-item>
<el-form-item :label="$t('profile.confirmPassword')" prop="confirmPassword">
<el-input v-model="user.confirmPassword" :placeholder="$t('profile.confirmPassword_placeholder')" type="password" show-password/>
</el-form-item>
<el-form-item>
<el-button type="primary" size="mini" @click="submit">{{ $t('common.save') }}</el-button>
<el-button type="danger" size="mini" @click="close">{{ $t('common.close') }}</el-button>
</el-form-item>
</el-form>
</template>

其他国际化就不一 一展示了,业务代码参照个人中心页面的国际化,对文字部分进行替换就行
语言包
zh.js
html
export default {
login: {
title: '管理系统登录',
logIn: '登录',
username: '账号',
password: '密码',
code: '验证码',
rememberMe: '记住密码'
},
tagsView: {
refresh: '刷新页面',
close: '关闭当前',
closeRight: '关闭右侧',
closeLeft: '关闭左侧',
closeOthers: '关闭其它',
closeAll: '全部关闭'
},
common: {
confirm: '确定',
cancle: '取消',
save: '保 存',
submit: '提 交',
approve2: '审 批',
reject: '驳 回',
close: '关闭',
option: '操作',
add: '新增',
add2: '添加',
edit: '修改',
delete: '删除',
export: '导出',
import: '导入',
approve: '审核',
search: '搜索',
reset: '重置',
date: '日期',
select_placeholder: '请选择',
startTime_placeholder: '开始时间',
endTime_placeholder: '结束时间',
startDate_placeholder: '开始日期',
endDate_placeholder: '结束日期',
index: '序号',
attachment: '附件',
attachment_select: '选取文件',
tip: '提示'
},
menus: {
system: '系统管理',
monitor: '系统监控',
},
route: {
home: '首页',
personalCenter: '个人中心',
changrole: '分配角色',
changuser: '分配用户',
data: '字典数据',
jobLog: '调度日志',
genEdit: '修改生成配置',
},
settings: {
title: '主题风格设置',
theme: '主题颜色',
configuration: '系统布局配置',
topNav: '开启 TopNav',
tagsView: '开启 Tags-View',
fixedHeader: '固定 Header',
sidebarLogo: '侧边栏 Logo',
dynamicTitle: '动态标题',
saveSetting: '保存配置',
resetSetting: '重置配置'
},
navbar: {
personalCenter: '个人中心',
layoutSettings: '布局设置',
logOut: '退出登录',
layoutSize: '布局大小',
confirmText: '确定注销并退出系统吗?',
switchLanguageSuccess: '设置语言成功',
},
profile: {
personalInformation: '个人信息',
basicInformation: '基本资料',
userName: '用户账号',
phonenumber: '手机号码',
email: '用户邮箱',
dept: '所属部门',
roleGroup: '所属角色',
createTime: '创建日期',
resetPwd: '修改密码',
nickName: '中文名',
sex: '性别',
men: '男',
women: '女',
oldPassword: '旧密码',
newPassword: '新密码',
confirmPassword: '确认密码',
oldPassword_placeholder: '请输入旧密码',
newPassword_placeholder: '请输入新密码',
confirmPassword_placeholder: '请确认新密码',
},
}
en.js
html
export default {
login: {
title: 'Management System',
logIn: 'Login in',
username: 'Username',
password: 'Password',
code: 'Code',
rememberMe: 'Remember Me'
},
tagsView: {
refresh: 'Refresh',
close: 'Close',
closeRight: 'Close Right',
closeLeft: 'Close Left',
closeOthers: 'Close Others',
closeAll: 'Close All'
},
common: {
confirm: 'Confirm',
cancle: 'Cancle',
save: 'Save',
submit: 'Submit',
close: 'Close',
approve2: 'Approve',
reject: 'Reject',
option: 'Option',
add: 'Add',
add2: 'Add',
edit: 'Edit',
delete: 'Delete',
export: 'Export',
import: 'Import',
approve: 'Approve',
search: 'Search',
reset: 'Reset',
date: 'Date',
select_placeholder: 'Please select',
startTime_placeholder: 'StartTime',
endTime_placeholder: 'EndTime',
startDate_placeholder: 'StartDate',
endDate_placeholder: 'EndDate',
index: 'Index',
attachment: 'Attachment',
attachment_select: 'Select',
tip: 'Tip'
},
menus: {
system: 'System Management',
monitor: 'System Monitoring',
},
route: {
home: 'Home',
personalCenter: 'Personal Center',
changrole: 'Assign Roles',
changuser: 'Assign Users',
data: 'Dictionary Data',
jobLog: 'Dispatching Log',
genEdit: 'Modify the generation configuration',
},
settings: {
title: 'Theme Style Setting',
theme: 'Theme Color',
configuration: 'System Layout Configuration',
topNav: 'Open TopNav',
tagsView: 'Open Tags-View',
fixedHeader: 'Fixed Header',
sidebarLogo: 'Sidebar Logo',
dynamicTitle: 'Dynamic Title',
saveSetting: 'Save',
resetSetting: 'Reset'
},
navbar: {
personalCenter: 'Personal Center',
layoutSettings: 'Layout Settings',
logOut: 'Log Out',
layoutSize: 'Layout Size',
confirmText: 'Are you sure to log out and exit the system?',
switchLanguageSuccess: 'Language Setting Successful',
},
profile: {
personalInformation: 'Personal Information',
basicInformation: 'Basic Information',
userName: 'User Name',
phonenumber: 'Phone',
email: 'Email',
dept: 'Dept',
roleGroup: 'Role',
createTime: 'Create Time',
resetPwd: 'Reset Password',
nickName: 'Name',
sex: 'Sex',
men: 'Men',
women: 'Women',
oldPassword: 'Old',
newPassword: 'New',
confirmPassword: 'Confirm',
oldPassword_placeholder: 'Please enter old password',
newPassword_placeholder: 'Please enter a new password',
confirmPassword_placeholder: 'Please confirm the new password',
},
}