【vue】I18N国际化管理系统

随着系统走向国际化,支持多语言成为必备功能。先前已经成功的把若依的管理系统更换名字成功,由于若依框架提供了良好的国际化(i18n)支持,本文将详细介绍如何为若依前后端分离版添加英语支持,实现中英文切换。

安装vue-i18n插件

根据您的Vue版本安装对应的vue-i18n插件:

html 复制代码
npm install --save vue-i18n

参考若依的官方手册,下面是完整的步骤:

前端国际化流程

1、package.jsondependencies节点添加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',
    },
}
相关推荐
一只游鱼3 小时前
vue集成dplayer
前端·javascript·vue.js·播放器·dplayer
Ro Jace3 小时前
模式识别与机器学习课程笔记(4):线性判决函数
人工智能·笔记·机器学习
Rousson4 小时前
硬件学习笔记--82 连接器的选用原则与流程
笔记·单片机·学习
Larry_Yanan6 小时前
QML学习笔记(四十)QML的ApplicationWindow和StackView
c++·笔记·qt·学习·ui
技术钱10 小时前
vue3 封装图片上传预览组件支持docx、excel、pdf、图片、txt格式
vue.js·pdf·excel
kyle~10 小时前
C++--- override 关键字 强制编译器验证当前函数是否重写基类的虚函数
java·前端·c++
Light6010 小时前
像素退场,曲线登场:现代响应式 CSS 全家桶 | 领码课堂
前端·css·响应式设计·css函数·布局系统·相对单位·设计令牌
爱生活的苏苏11 小时前
elementUI 表单验证-联动型校验
前端·javascript·elementui
摇滚侠11 小时前
Spring Boot 3零基础教程,Spring Boot 日志的归档与切割,笔记22
spring boot·redis·笔记