Vite+Vue3+Typescript后台管理项目 i18n国际化

2023-10-19 15:12:34 浏览数 (2)

vue3已经出来很久了,因为工作只是再维护老项目,没有做技术更新,所以对vue3的使用上面会差很多,但是现在又有许多公司要求有vue3使用经验,所以对Vue3 ts自学写的模板项目

这里会写明全部流程及要点。

Vite Vue3 Typescript项目地址 https://github.com/Seven7v/vue3-Ts-admin

后台管理首页后台管理首页
登录页面登录页面

需要的话可以自行下载

  • vite使用的Rollup进行打包,相对来说是比webpack更加轻量级的,这里从项目的启动速度就可以体现出来。
  • vite 天生支持 typescript 使用ts更加友好
  • vite 带有css 预处理器,包括less scss 使用都可以不用安装loader,(在webpack中需要安装loader)
  • vite在修改config文件后不需要重启项目,会自动更新页面

对比Vue3 对比Vue2 的更新

  • 在vue2中,同一元素上的v-for的优先级高于v-if,vue3更改了两者的优先级,v-if的优先级高于v-for
  • destroyed生命周期选项被重命名为 unmounted
  • beforeDestroy 生命周期选项被重命名为 beforeUnmount
  • Proxy 代替Obiect.defineProperty 重构了响应式系统可以监听到数组下标变化,及对象新增属性,因为监听的不是对象属性,而是对象本身,还可拦截 apply、has 等13种方法
  • 支持在<style></style> 里使用 v-bind,给CSS绑定S变量(color: v-bind(str))
  • 新增Composition API 可以更好的逻辑复用和代码组织,同一功能的代码不至于像以前一样太分散

安装vite

使用npm init vite 进行安装

代码语言:txt复制
PS F:v3> npm init vite
Need to install the following packages:
  create-vite@4.4.1
Ok to proceed? (y) y
√ Project name: ... vite-project
√ Select a framework: » Vue
√ Select a variant: » TypeScript

Scaffolding project in F:v3vite-project...

随后执行 vite 项目就跑起来了。

代码语言:txt复制
  cd vite-project
  npm install
  npm run dev

vite官网还提供了其他创建方式

npm create vite@latest

使用yarnyarn create vite

使用pnpmpnpm create vite

项目运行成功后 NetWork 不显示链接,

代码语言:txt复制
  ➜  Local:   http://127.0.0.1:5173/
  ➜  Network: use --host to expose
  ➜  press h to show help

这里我们可以更新vite.config.ts

代码语言:txt复制
 server: {
    host: '0.0.0.0',
    port: 5800, //设置服务启动端口号,是一个可选项,不要设置为本机的端口号,可能会发生冲突
    open: true, //是否自动打开浏览器,可选项
    }

这时终端就会更新为

代码语言:txt复制
  ➜  Local:   http://localhost:5800/
  ➜  Network: http://192.168.xxx.xx:5800/
  ➜  Network: http://xx.xx.xxxxx:5800/

项目路由

创建好项目后为我们的项目配置路由

代码语言:txt复制
npm install vue-router@4

vue-router文档提供了使用的手册,

新建 router文件夹,index.ts中的内容如下

代码语言:txt复制
import { createRouter, createWebHistory } from 'vue-router'
import { App } from 'vue'
import { getUserInfoApi } from '../sever/api'
import routes from './routes' // 页面中 配置的路由
const router = createRouter({
  history: createWebHistory(), //history模式
  routes
})

router.beforeEach(async (to, from) => {
  // 这里可以输入一些页面重定向内容,判断token跳转登录页
})
// export default route 将路由导出的写法

// 这里只导出一个方法,在页面外不可以修改router里的内容
// 封装路由方法,传入app <Element> 代表页面内的标签元素
export const initRouter = (app: App<Element>) => {
  app.use(router)
}

route.ts配置项目路由

代码语言:txt复制
import { RouteRecordRaw } from 'vue-router'
const Layout = () => import('../layout/index.vue') //页面layout 
const Login = () => import('../pages/login/index.vue')
const Homepage = () => import('../pages/homePage/index.vue')
const Chart = () => import('../pages/chart/index.vue')
const DocumentSetting = () => import('../pages/document/setting.vue')
const DocumentPreview = () => import('../pages/document/preview.vue')
const UserConsole = () => import('../pages/console/userConsole.vue')
const PermissionConsole = () => import('../pages/console/permissionConsole.vue')

const routes: RouteRecordRaw[] = [
  {
    path: '/admin',
    component: Layout,
    name: 'admin',
    meta: {
      icon: 'Menu',
      isNav: true
    },
    children: [
      {
        path: '/admin/home',
        component: Homepage,
        name: 'home',
        meta: {
          isNav: true
        }
      }
    ]
  },
  {
    path: '/login',
    component: Login,
    name: 'login'
  },
  {
    path: '/',
    redirect: '/admin/home'
  }
]

const asyncRouter: RouteRecordRaw[] = [
  {
    path: '/statistics',
    component: Layout,
    name: 'statistics',
    meta: {
      icon: 'PieChart',
      isNav: true,
    },
    children: [
      {
        path: '/statistics/chart',
        component: Chart,
        name: 'chart',
        meta: {
          isNav: true,
        }
      }
    ]
  },
    {
    path: '/document',
    component: Layout,
    name: 'document',
    meta: {
      isNav: true,
      icon: 'Document'
    },
    children: [
      {
        path: '/document/setting',
        component: DocumentSetting,
        name: 'setting',
        meta: {
          isNav: true,
          role: ['admin', 'editor', 'normal']
        }
      },
      {
        path: '/document/table',
        component: DocumentPreview,
        name: 'table',
        meta: {
          isNav: true,
          role: ['admin', 'editor', 'normal']
        }
      }
    ]
  },
]
const CurrentRoute: RouteRecordRaw[] = routes.concat(...asyncRouter)
export default CurrentRoute

i18n

安装 vue-i18n 创建i18n文件 使用方法如下

代码语言:txt复制
import { App } from 'vue'
import { createI18n } from 'vue-i18n'
import { zh } from './zh'
import { en } from './en'


const language = (navigator.language || 'en').toLocaleLowerCase() // 获取浏览器的语言设置
const i18n = createI18n({
  legacy: false,
  locale: localStorage.getItem('lang') || language, // 优先从本地存储获取语言设置,如果没有则使用浏览器默认语言
  fallbackLocale: 'en', // 当前语言无法找到匹配的翻译时,使用的备选语言
  messages: {
    en,
    zh
  }
})

// 封装i18n方法
export const initI18n = (app: App<Element>) => {
  app.use(i18n)
}

element-plus

成功后可以安装element-plus,官网里包括安装,全局引用及自动按需引用都有配置教程

代码语言:txt复制
$ npm install element-plus --save

main.ts

main.ts中引入路由, i18n,全局样式

代码语言:txt复制
import { createApp } from 'vue'
import './style.css'
import './assets/style/common.css'
import 'element-plus/dist/index.css'
import App from './App.vue'
import { initRouter } from './routes'
import { initI18n } from './i18n'
import * as ElIcons from '@element-plus/icons-vue'

setTeam()
const app = createApp(App)
initI18n(app)
// 在页面中调用router封装的方法挂载路由
initRouter(app)
app.mount('#app')
for (const name in ElIcons) app.component(name, (ElIcons as any)[name])

做了这些工作,在页面内修改path就可以进行页面切换了,

切换语言

封装切换项目语言组件,可以写在项目公用组件库里 components文件夹里

changeLang组件内容

代码语言:txt复制
<script setup lang="ts">
import { getCurrentInstance, ref, watchEffect } from 'vue'
import { useI18n } from 'vue-i18n'

// 切换语言
const { proxy } = getCurrentInstance() as any
const { t } = useI18n()
const lang = ref('chinese')

watchEffect(() => {
  if (lang.value === 'chinese') {
    proxy.$i18n.locale = 'zh'
    localStorage.setItem('lang', 'zh')
  }
  if (lang.value === 'english') {
    proxy.$i18n.locale = 'en'
    localStorage.setItem('lang', 'en')
  }
})
</script>

<template>
  <el-select style="width: 100px" v-model="lang">
    <el-option :label="t('Chinese')" value="chinese" />
    <el-option :label="t('English')" value="english" />
  </el-select>
</template>

封装请求方法

这里我们使用axios,请求,对于接口 单是前端项目可以考虑用rap2实现模拟请求,

创建server目录,server目录中index.ts进行封装,api.ts或其他文件用来管理接口内容

index.ts

代码语言:txt复制
import axios from 'axios'
import { AxiosInstance } from 'axios'
import { ElMessage } from 'element-plus'

const baseUrl = '/api'
export const $axios: AxiosInstance = axios.create({
  baseURL: baseUrl,
  headers: {
    'Content-Type': 'application/json',
    'X-Requested-With': 'XMLHttpRequest'
  }
})

$axios.interceptors.request.use(config => {
  var xtoken: any = localStorage.getItem('token')
  if (xtoken) {
    xtoken = xtoken
    config.headers['Authorization'] = xtoken
  }
  return config
})
$axios.interceptors.response.use(
  (res: any) => {
    console.log(res)
    const { code, message } = res.data
    if (code === 200) {
      if (message) {
        ElMessage.success(message)
      }
      return res
    } else {
      ElMessage.error(res.massage)
      return Promise.reject(new Error(res.message))
    }
  },
  (err: any) => {
    console.error(err)
    const message = err.response.data
    ElMessage.error(message)
    return Promise.reject(new Error(err.message))
  }
)

api.ts

代码语言:txt复制
import { $axios } from './index.ts'
import { InterfaceLoginReq } from '../type'

// 登录
export const loginApi = (params: InterfaceLoginReq) => {
  return $axios.post('/login', params)
}

// 创建用户
export const createUserApi = (params: InterfaceLoginReq) => {
  return $axios.post('/create', params)
}

// 获取用户信息
export const getUserInfoApi = () => {
  return $axios.get('/userInfo')
}

// 登出账号
export const logoutApi = () => {
  return $axios.post('/logout')
}

登录页 表单提交

代码语言:txt复制
<template>
  <div class="login cen disflex ai-cen">
    <div class="login-form-wrapper disflex">
      <div class="login-img-wrapper bg-prim">
        <div class="login-title fw900">{{ $t('login.management') }}</div>
        <img class="login-img" src="../../assets/img/login.svg" alt="" />
      </div>
      <div class="login-box">
        <el-card class="login-inner">
          <changeLanguage class="login-lang" />

          <el-form
            ref="formRef"
            :model="dynamicValidateForm"
            label-width="100px"
            label-position="left"
            class="login-form mb30"
          >
            <el-form-item
              prop="username"
              :label="t('login.username')"
              :rules="[
                {
                  required: true,
                  message: t('login.usernameRequire'),
                  trigger: 'blur'
                }
              ]"
            >
              <el-input v-model="dynamicValidateForm.username" />
            </el-form-item>
            <el-form-item
              prop="password"
              :label="t('login.password')"
              :rules="[
                {
                  required: true,
                  message: t('login.passwordRequire'),
                  trigger: 'blur'
                }
              ]"
            >
              <el-input type="password" v-model="dynamicValidateForm.password" />
            </el-form-item>
          </el-form>
          <el-button type="primary" class="login-btn" @click="submitForm(formRef)">{{
            t('login.login')
          }}</el-button>
          <el-button class="login-btn" @click="resetForm(formRef)">{{
            t('login.concel')
          }}</el-button>
        </el-card>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { reactive, ref } from 'vue'
import { FormInstance } from 'element-plus'
import { InterfaceLoginReq } from '../../type'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import { loginApi, createUserApi } from '../../sever/api'
import { setLoginTimeApi } from '../../sever/data'
import changeLanguage from '../components/changeLanguage.vue'

const { t } = useI18n()
const formRef = ref<FormInstance>()
const dynamicValidateForm = reactive<InterfaceLoginReq>({
  username: '',
  password: ''
})
const router = useRouter()

const submitForm = async (formEl: FormInstance | undefined) => {
  if (!formEl) return
  formEl.validate(async valid => {
    if (valid) {
      // 创建用户时使用
      // await createUserApi(dynamicValidateForm)
      const res = await loginApi(dynamicValidateForm)
      if (res.data.code == 200) {
        localStorage.setItem('token', res.data.token)
        const loginTimeParams = {
          username: dynamicValidateForm.username,
          loginTime: new Date()
        }
        const resq = await setLoginTimeApi(loginTimeParams)
        console.log(resq)
        router.push({
          name: 'home'
        })
      }
    } else {
      return false
    }
  })
}

const resetForm = (formEl: FormInstance | undefined) => {
  if (!formEl) return
  formEl.resetFields()
}
</script>

<style lang="less" scoped>
.login {
  width: 100%;
  height: 100%;
  &-form-wrapper {
    width: 100%;
    height: 100%;
  }
  &-img-wrapper {
    width: 50%;
  }
  &-title {
    font-size: 40px;
    font-family: Verdana, Geneva, Tahoma, sans-serif;
    color: #fff;
    margin-top: 15%;
    margin-bottom: 15%;
    margin-left: 10%;
  }
  &-img {
    width: 60%;
    margin-left: 30%;
  }
  &-form {
    margin-bottom: 20px;
  }
  &-box {
    width: 50%;
    background-color: #ffffff;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  &-inner {
    position: relative;
    width: 60%;
    height: 400px;
    background-color: #fff;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  &-lang {
    position: absolute;
    right: 20px;
    top: 20px;
  }
  &-btn {
    width: 100%;
    margin-bottom: 15px;
  }
}
/deep/ .el-button   .el-button {
  margin-left: 0;
}
</style>

0 人点赞