• [Vue3]构建通用后台系统


    e7da404d5c3eec1a61ad682c48f0ead0.png

    前言

    基于Vue3 + Vite + Element-plus 来构建一个常见的后台,做这个的原因还是在于理清技术细节,虽然这玩意到处都是,但对于前端经验不算多的我而言,还是有必要自己捣鼓一次的,此外本次使用JavaScript来开发,而不适用TypeScript,核心原因是,遇到一些问题时,请教周围的前端朋友发现他们都不太熟悉TypeScript,所以一些小问题因为其类型推断系统搞了很久,后面和解了,个人项目用JavaScript也没啥问题,下面我们开始吧。

    本文会提供完整的代码,请放心食用。

    使用yarn构建vue3项目:

    1. yarn create vite DashboardFrameWork --template vue
    2. yarn && yarn dev

    然后安装相应的依赖:

    1. yarn add element-plus --save
    2. yarn add @element-plus/icons-vue --save
    3. yarn add axios --save
    4. yarn add sass --save
    5. yarn add vue-router@next --save
    6. yarn add vuex@next --save

    我将依赖都安装到dependencies(--save)中。

    就我个人理解,开发时使用的工具向的东西,比如vite、webpack、mockjs等,不需要在上线时依赖,那么就将其安装到devDependencies(--dev)中,反之则安装到dependencies中。

    上述安装的依赖中,element-plus是UI框架,element-plus/icons-vue是UI框架的图标相关的支撑库,axios是HTTP请求库、sass是用于编写样式的CSS超集语言、vue-router用于实现单页面路由,vuex用于实现状态存储,很常见的Vue3全家桶。

    封装axios

    为了方便开发和代码模块化,多数项目在开始前会进行一些基本的封装并构建出项目的骨架。

    首先,我们对axios库进行封装,如果单纯的使用axios库,其用法已经足够简单了,但结合后台业务情况,还是有必要对它进行二次封装,实现请求拦截、响应拦截。

    创建src/utils/request.js,用于实现封装JavaScript的逻辑,先创建axios对象:

    1. // 创建axios实例对象,添加全局配置
    2. const service = axios.create({
    3.   baseURL: config.baseApi,
    4.   timeout: 8000,
    5. });

    接着实现请求拦截,实现每次请求前身份的校验:

    1. // 请求拦截
    2. service.interceptors.request.use((req) => {
    3.   const headers = req.headers;
    4.   const { token } = storage.getItem("userInfo") || {};
    5.   if (token) {
    6.     if (!headers.Authorization) headers.Authorization = "Bearer " + token;
    7.   }
    8.   return req;
    9. });

    上述代码中,从storage中获取userInfo的数据,我们可以通过chrome的开发者工具Application查看到storage中存储的数据:

    c9fbd42543cfa7d61b1d0c9a135a5d6e.png

    我们在login时,将用户基础信息写入其中,每次请求前都会通过请求拦截做一次登录校验。

    以类似的方式,我们可以实现响应拦截:

    1. // 响应拦截
    2. service.interceptors.response.use((res) => {
    3.   const { code, data, msg } = res.data;
    4.   if (code === 200) {
    5.     return data;
    6.   } else if (code === 500001) {
    7.     ElMessage.error(TOKEN_INVALID);
    8.     // 让错误信息展示一下,再跳转
    9.     setTimeout(() => {
    10.       router.push("/login");
    11.     }, 1500);
    12.     // 抛出异常
    13.     return Promise.reject(TOKEN_INVALID);
    14.   } else {
    15.     ElMessage.error(msg || NETWORK_ERROR);
    16.     return Promise.reject(msg || NETWORK_ERROR);
    17.   }
    18. });

    如果响应的code是200,则将数据正常返回,如果code不为200,则通过ELMessage给用户展示相关的错误信息,并通过路由方法跳转到login页面,一个小技巧是,失败时,不要立刻跳转到login页面,因为我们希望用户看到相关的报错信息,最后返回Promise.reject对象。

    拦截相关的方法实现好后,再封装一下请求方法就好了:

    1. function request(options) {
    2.   options.method = options.method || "get";
    3.   if (options.method.toLowerCase() === "get") {
    4.     options.params = options.data;
    5.   }
    6.   let isMock = config.mock;
    7.   //  兼容局部Mock的用法
    8.   if (typeof options.mock != "undefined") {
    9.     isMock = options.mock;
    10.   }
    11.   service.defaults.baseURL = isMock ? config.mockApi : config.baseApi;
    12.   return service(options);
    13. }

    上述代码中封装了request方法,这里的核心在于,请求的URL是Mock地址还是真实的后台地址。

    通过Mock,前端可以在不需要后台提供出完整的API的情况下进行开发,很多前端开源项目会使用Mockjs来构建一个单独的serve来为前端项目提供数据,而这里我直接使用了在线的Mock服务(后文介绍)。

    至此,我们可以通过如下方式来实现http请求:

    1. request({
    2.   url: "/users/login",
    3.   method: "post",
    4.   data: {
    5.     username: "ayuliao",
    6.     pwd: "123"
    7.   }
    8. }).then((res) => {
    9.   console.log(res);
    10. });

    request.js github位置:https://github.com/ayuLiao/DashboardFrameWork/blob/master/src/utils/request.js

    配置文件

    多数项目中,都会使用配置文件来管理相关的配置,我们也不例外。

    创建src/config/config/index.js,代码如下:

    1. /**
    2.  * 环境配置封装
    3.  */
    4. // import.meta.env.MODE 当前项目环境
    5. const env = import.meta.env.MODE
    6. const EnvConfig = {
    7.     development:{
    8.         baseApi:'/api',
    9.         mockApi:'https://www.fastmock.site/mock/xxx/api'
    10.     },
    11.     production:{
    12.         baseApi:'//xxx.com/api',
    13.         mockApi:'https://www.fastmock.site/mock/xxx/api'
    14.     }
    15. }
    16. export default {
    17.     env,
    18.     // 是否开启Mock
    19.     mock:true
    20.     namespace:'manager',
    21.     ...EnvConfig[env]
    22. }

    通过env.MODE来判断当前的环境,在Vue3中,默认情况下,开发服务器 (dev 命令) 运行在 development (开发) 模式,而 build 命令则运行在 production (生产) 模式(更多可看:https://cn.vitejs.dev/guide/env-and-mode.html#intellisense)。

    配置中,提供mockApi来请求在线Mock,这里使用fastmock这个在线mock服务,当然我们可以通过mock.js来构建本地的Mock服务,这里图方便,就使用了fastmock。

    很常规的使用方式,注册账号,然后创建相应的接口:0c5e0c01a53e1d1be3a2cc727005b40d.png

    返回的值,给一个JSON则可,通常直接使用后端给的API对接文档中的内容则可:

    2499b87a31dfeb1caec2e81052cd5501.png

    封装状态管理

    我们使用vuex来进行状态管理,但只要我们一刷新浏览器,vuex中的数据便会丢失,为了避免这种情况,我们可以配合着浏览器的localStorage来存储数据,实现数据的持久化。

    创建 src/utils/storage.js,对localStorage的增删改查进行封装,思考一个问题:通过window.localStorage对象已经可以实现增删改查,为啥还要封装一层?

    主要是因为localStorage不能直接存储Object对象,只能存储字符串,所以常规的做法就是将通过JSON字符串的形式来存储数据,存入将对象转成JSON字符串,取出则从JSON字符串解码成对象,代码如下:

    1. /**
    2.  * Storage二次封装
    3.  */
    4.  import config from '@/config'
    5.  export default {
    6.      setItem(key,val){
    7.          let storage = this.getStroage();
    8.          storage[key] = val;
    9.          window.localStorage.setItem(config.namespace,JSON.stringify(storage));
    10.      },
    11.      getItem(key){
    12.          return this.getStroage()[key]
    13.      },
    14.      getStroage(){
    15.          return JSON.parse(window.localStorage.getItem(config.namespace) || "{}");
    16.      },
    17.      clearItem(key){
    18.          let storage = this.getStroage()
    19.          delete storage[key]
    20.          window.localStorage.setItem(config.namespace,JSON.stringify(storage));
    21.      },
    22.      clearAll(){
    23.          window.localStorage.clear()
    24.      }
    25.  }

    创建src/store/目录,在其中创建index.js和mutations.js,其中index.js中写vuex中state相关的逻辑,而mutations.js自然实现mutations相关逻辑,先看index.js,代码如下:

    1. /**
    2.  * Vuex状态管理
    3.  */
    4.  import { createStore } from 'vuex'
    5.  import mutations from './mutations'
    6.  import storage from './../utils/storage'
    7.  
    8.  const state = {
    9.     //  Vuex配合storage使用,Vuex强刷的话,数据会丢失,所以配合storage使用
    10.      userInfo: "" || storage.getItem("userInfo"// 获取用户信息
    11.  }
    12.  export default createStore({
    13.      state,
    14.      mutations
    15.  })

    mutation是vuex中的概念,是修改Vuex store中状态的唯一方法,简单理解就是定义方法,通过这些方法才能修改存储在vuex中的事件,项目通常会将state和mutations分开来实现:

    1. /**
    2.  * Mutations业务层数据提交
    3.  * 
    4.  */
    5.  import storage from './../utils/storage'
    6.  export default {
    7.      saveUserInfo(state,userInfo){
    8.          state.userInfo = userInfo;
    9.          storage.setItem('userInfo',userInfo)
    10.      }
    11.  }

    基础路由

    使用vue-router来实现路由,创建src/router/index.js。创建路由的逻辑是很机械化的,代码如下:

    1. import { createRouter, createWebHashHistory } from "vue-router";
    2. import Home from "@/components/Home.vue";
    3. const routes = [
    4.   {
    5.     name: "home",
    6.     path: "/",
    7.     meta: {
    8.       title: "首页",
    9.     },
    10.     component: Home,
    11.     redirect: "/welcome",
    12.     children: [
    13.       {
    14.         name: "welcome",
    15.         path: "/welcome",
    16.         meta: {
    17.           title: "Welcome use Dashboard Framework",
    18.         },
    19.         component: () => import("@/views/Welcome.vue"),
    20.       },
    21.     ],
    22.   },
    23.   {
    24.     name: "login",
    25.     path: "/login",
    26.     meta: {
    27.       title: "登录",
    28.     },
    29.     component: () => import("@/views/Login.vue"),
    30.   }
    31. ];
    32. const router = createRouter({
    33.   history: createWebHashHistory(),
    34.   routes,
    35. });
    36. export default router;

    路由需要配合router-view来使用,在App.vue中,直接使用router-view则可,router-view会渲染出一级路由的内容,对上述路由而言,便是home和login的内容。

    1. <!-- src/App.vue -->
    2. <template>
    3.   <router-view></router-view>
    4. </template>
    5. <script setup>
    6. </script>
    7. <style lang="scss">
    8. @import "./assets/style/reset.css";
    9. @import "./assets/style/index.scss";
    10. </style>

    home的子路由有welcome,要渲染welcome,就需要在home中使用router-view,router-view的嵌套模式与路由中一致。

    实现首页骨架

    创建components/Home.vue,其template如下:

    1. <template>
    2.   <div class="basic-layout">
    3.     <div class="nav-side">
    4.       <div class="logo">
    5.         <img src="./../assets/logo.png" alt="" class="src" />
    6.         <span>DashBoard</span>
    7.       </div>
    8.     </div>
    9.     <div class="content-right">
    10.       <div class="nav-top">
    11.         <div class="nav-left">
    12.      
    13.           <div class="bread">面包屑</div>
    14.         </div>
    15.         <div class="user-info">
    16.           用户信息
    17.         </div>
    18.       </div>
    19.       <div class="wrapper">
    20.         <router-view></router-view>
    21.       </div>
    22.     </div>
    23.   </div>
    24. </template>

    整个骨架我们使用div一块块搭建处理,接着来写CSS,我们使用scss来实现:

    1. <style lang="scss">
    2. .basic-layout {
    3.   // 侧边栏
    4.   .nav-side {
    5.     position: fixed;
    6.     width: 200px;
    7.     // 浏览器可见窗口的百分比
    8.     height: 100vh;
    9.     background-color: #001529;
    10.     color: #fff;
    11.     // 超出y轴部分
    12.     overflow-y: auto;
    13.     // 宽度变化时,带动画效果
    14.     transition: width 0.5s;
    15.     .logo {
    16.       display: flex;
    17.       align-items: center;
    18.       font-size: 18px;
    19.       height: 50px;
    20.       img {
    21.         margin: 0 16px;
    22.         width: 32px;
    23.         height: 32px;
    24.       }
    25.     }
    26.     .nav-menu {
    27.       height: calc(100vh - 50px);
    28.       border-right: none;
    29.     }
    30.   }
    31.   .content-right {
    32.     // 因为父元素position:relative,所以直接移200px
    33.     margin-left: 200px;
    34.     .nav-top {
    35.       height: 50px;
    36.       line-height: 50px;
    37.       display: flex;
    38.       // 利用flex,将元素排到两端
    39.       justify-content: space-between;
    40.       border-bottom: 1px solid #ddd;
    41.       padding: 0 20px;
    42.       .nav-left {
    43.         display: flex;
    44.         align-items: center;
    45.         .menu-fold {
    46.           margin-right: 15px;
    47.           font-size: 18px;
    48.         }
    49.       }
    50.       .user-info {
    51.         .notice {
    52.           line-height: 30px;
    53.           margin-right: 15px;
    54.         }
    55.         .user-link {
    56.           // 在鼠标指针悬停在元素上时显示相应样式
    57.           cursor: pointer;
    58.           color: #409eff;
    59.         }
    60.       }
    61.       
    62.     }
    63.     .wrapper {
    64.         background: #eef0f3;
    65.         padding: 20px;
    66.         height: calc(100vh - 50px);
    67.       }
    68.   }
    69. }
    70. </style>

    在使用scss编写css时,为了避免命名冲突,通常会通过一个div将组件或模块包裹起来,这里便是basic-layout,不同组件根div的class不同,再利用scss的语法来写css,就不会出现css命名冲突的问题了。

    position定位

    上述css中,使用了fixed定位,借此记录一下编写css时,我们常用的relative定位与fixed定位。

    relative定位会相对于默认位置(static定位)进行偏移:

    76c0f008d378c425a9f20240a053e604.png

    在浏览器中,每个元素默认通过static的形式来定(position的默认值),static定位下,元素会按HTML源码的顺序来排列,每个块级元素占据自己的位置,元素与元素之间不会重叠。

    当我们使用relative时,它会相对于static进行偏移,static即它原本的正常位置,改成relative后,配合top、bottom、left、right这四个属性来实现偏移:

    9768a39fdf1fbe9b59b9231daa4ff606.png

    接着聊fixed定位,我们后台的首页布局中使用了fixed定位来固定侧边栏和顶部栏,fixed会基于浏览器窗口定义,其效果就是元素不会随着页面滚动而变化,如同固定在页面上一样。

    035e8dfaa90b2dedc57bbea990b47d0e.png

    display布局

    天下苦布局久已,自从display出来后,一切便简单起来了,这里记录几种display中最常用的布局。

    先说居中布局:

    1. .box {
    2.   display: flex;
    3.   // 水平居中
    4.   justify-content: center;
    5.   // 垂直居中
    6.   align-items: center;
    7. }

    上述CSS会让box中的元素水平、垂直都居中,如果以骰子为例,效果为:

    63c45fa6016d29353c35196a79099786.png

    然后再说一下两端对其:

    1. .box {
    2.   display: flex;
    3.   justify-content: space-between;
    4. }

    效果为:

    33cb407cea651d4216ba3c894a714b40.png

    我们可以借助在线布局演示网站(https://xluos.github.io/demo/flexbox/)来体验flex布局的效果,从而理解flex其他样式:

    043afb15ff737b6f6b471ebed9d0d894.png

    首页路由

    首页中,有如下一段HTML,用于渲染子路由的页面,根据router/index.js配置的路由,这里会渲染Welcome.vue

    1. <div class="wrapper">
    2.   <router-view></router-view>
    3. </div>

    首页侧边栏

    在常见的后台中,侧边栏是按登录者的权限来展示的,不同的用户登录时,侧边中的内容有所不同,具体而言,权限控制由后端权限管理相关逻辑实现,而前端只需要通过后端返回的内容,动态渲染出侧边栏则可。

    我们将侧边栏相关的逻辑放在class为nav-side的div中:

    1. <div class="nav-side">
    2.   <div class="logo">
    3.     <img src="./../assets/logo.png" alt="" class="src" />
    4.     <span>DashBoard</span>
    5.   </div>
    6.   <!-- 导航菜单 -->
    7.   <!-- https://element-plus.org/zh-CN/component/menu.html#menu-%E5%B1%9E%E6%80%A7 -->
    8.   <el-menu
    9.            class="nav-menu"
    10.            background-color="#001529"
    11.            text-color="#fff"
    12.            router
    13.            >
    14.     <TreeMenu :menuList="menuList.data" />
    15.   </el-menu>
    16. </div>

    首先,我们使用element-plus中的el-menu来包裹出侧边栏,el-menu元素可使用的属性可自行读一下文档,而侧边栏真正的实现逻辑是TreeMenu子组件,其传入参数为menuList,TreeMenu.vue代码如下:

    1. <script setup>
    2. // https://v3.cn.vuejs.org/api/sfc-script-setup.html#defineprops-%E5%92%8C-defineemits
    3. const props = defineProps({
    4.   menuList: Array,
    5. });
    6. </script>
    7. <template>
    8.   <template v-for="menu in menuList" :key="menu._id">
    9.     <el-sub-menu
    10.       v-if="
    11.         menu.children &&
    12.         menu.children.length > 0 &&
    13.         menu.children[0].menuType == 1
    14.       "
    15.       :index="menu.path"
    16.     >
    17.       <!-- 目录父级 -->
    18.       <template #title>
    19.         <!-- https://segmentfault.com/q/1010000040569967 -->
    20.         <el-icon><component :is="menu.icon" /></el-icon>
    21.         <span>{{ menu.menuName }}</span>
    22.       </template>
    23.       <TreeMenu :menuList="menu.children" />
    24.     </el-sub-menu>
    25.     <!-- 目录子级 -->
    26.     <el-menu-item
    27.       v-else-if="menu.menuType == 1"
    28.       :index="menu.path"
    29.       >{{ menu.menuName }}</el-menu-item
    30.     >
    31.   </template>
    32. </template>

    这个项目中,我使用了最新的setup语法,TreeMenu子组件在接收父组件传参时,需要通过defineProps方法来实现参数的接收。

    TreeMenu子组件核心逻辑在template中,因为侧边栏有嵌套的情况,比如下图这种情况:

    4985148b7e76603096bbfd949f37d500.png

    对于嵌套情况,可以通过递归的方式来,通过v-if判断是否当前元素是否嵌套有子结构,有的话就再通过TreeMenu来构建出新的新的节点。

    TreeMenu代码中有一个小技巧,就是多个template节点的嵌套使用,template节点本身不会被渲染出相应的DOM,利用template节点来放置v-for、v-if等操作是很合适的做法。

    另外一个技巧是,element-plus展示图标的方式有所改变,我们需要通过动态组件的方式来放置合适的图标:

    <el-icon><component :is="menu.icon" /></el-icon>

    当然,要在项目中随意使用element-plus图标,需要在入口文件main.js中全局挂载一下:

    1. app.use(router).use(store).use(ElementPlus)
    2. // 全局挂载icon,方便icon在项目各处使用
    3. for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
    4.   app.component(key, component)
    5. }

    此外,在构建目录时,使用了Vue中的插槽:

    1. <!-- 目录父级 -->
    2. <template #title>
    3.   <!-- https://segmentfault.com/q/1010000040569967 -->
    4.   <el-icon><component :is="menu.icon" /></el-icon>
    5.   <span>{{ menu.menuName }}</span>
    6. </template>

    我们希望目录中父级目录可以显示图标和名字,而子级目录,不需要显示图标了,但Element-plus提供的el-menu并不直接支持,此时我们就需要通过Vue插槽动态的将template中的代码替换到el-menu中,el-menu组件提供了title插槽。

    顶部栏实现

    常见的后台其顶部栏会展示面包屑和用户相关信息:

    8c5831ba2013f83990a7592d395260f9.png相关的HTML如下:

    1. <div class="nav-top">
    2.   <div class="nav-left">
    3.     <div class="bread">
    4.       <BreadCrumb />
    5.     </div>
    6.   </div>
    7.   <div class="user-info">
    8.     <!-- 信息红点通知 -->
    9.     <el-badge
    10.               :is-dot="noticeCount > 0 ? true : false"
    11.               class="notice"
    12.               type="danger"
    13.               >
    14.       <el-icon><bell /></el-icon>
    15.     </el-badge>
    16.     <!-- command 点击菜单项触发的事件回调 -->
    17.     <el-dropdown @command="handleLogout">
    18.       <span class="user-link">
    19.         {{ userInfo.userName }}
    20.         <el-icon><right /></el-icon>
    21.       </span>
    22.       <template #dropdown>
    23.         <el-dropdown-menu>
    24.           <el-dropdown-item command="email"
    25.                             >邮箱:{{ userInfo.userEmail }}</el-dropdown-item
    26.             >
    27.           <el-dropdown-item command="logout">退出</el-dropdown-item>
    28.         </el-dropdown-menu>
    29.       </template>
    30.     </el-dropdown>
    31.   </div>
    32. </div>

    整体结构通过div构建,先看面包屑,它是一个独立的组件,通过BreadCrumb子组件实现。

    这里面包屑的主要效果是暂时当前用户访问页面其访问路径,vue-router中的属性可以让我们轻松实现效果效果:

    1. <script setup>
    2. import { computed } from '@vue/reactivity';
    3. import router from '../router';
    4. import { ArrowRight } from '@element-plus/icons-vue'
    5. const breadList = computed(() => router.currentRoute.value.matched)
    6. </script>
    7. <template>
    8.   <el-breadcrumb :separator-icon="ArrowRight">
    9.     <el-breadcrumb-item v-for="(item, index) in breadList" :key="item.path">
    10.         <router-link to="/welcome" v-if="index==0">{{item.meta.title}}</router-link>
    11.         <span v-else>{{item.meta.title}}</span>
    12.     </el-breadcrumb-item>
    13.   </el-breadcrumb>
    14. </template>

    这里通过computed获得路由路径,computed会构建一层缓存,当对象发生改变时,缓存会更新。

    通过router.currentRoute.value.matched可以获得当前路由以及访问当前路由的完整路由路由,这是vue-router提供的功能。

    要找到这种功能,最好的方式是使用debug大法,参考element-plus-admin源码剖析一文,debug起来,效果如下:

    dbde0961e1af5087280b82fdca98ae82.png

    对router对象,一层层看里面的属性,便可以找到需要的内容了,随后便通过el-breadcrumb标签展示出来则可。

    面包屑完成了,将注意力移动到顶部栏右侧的下拉按钮:

    1. <div class="user-info">
    2.     <!-- 信息红点通知 -->
    3.     <el-badge
    4.               :is-dot="noticeCount > 0 ? true : false"
    5.               class="notice"
    6.               type="danger"
    7.               >
    8.       <el-icon><bell /></el-icon>
    9.     </el-badge>
    10.     <!-- command 点击菜单项触发的事件回调 -->
    11.     <el-dropdown @command="handleLogout">
    12.       <span class="user-link">
    13.         {{ userInfo.userName }}
    14.         <el-icon><right /></el-icon>
    15.       </span>
    16.       <template #dropdown>
    17.         <el-dropdown-menu>
    18.           <el-dropdown-item command="email"
    19.                             >邮箱:{{ userInfo.userEmail }}</el-dropdown-item
    20.             >
    21.           <el-dropdown-item command="logout">退出</el-dropdown-item>
    22.         </el-dropdown-menu>
    23.       </template>
    24.     </el-dropdown>
    25.   </div>

    核心的下拉后展示相关内容的逻辑,主要通过el-dropdown的dropdown插槽实现。

    登录页面

    登录是个常见的功能,通过Element-plus提供的表单组件,来构建登录页的基础骨架:

    1. <template>
    2.   <div class="login-wrapper">
    3.     <div class="modal">
    4.       <!-- https://juejin.cn/post/7033953300731035655 -->
    5.       <!-- ref绑定变量, :ref绑定函数 -->
    6.       <el-form ref="userForm" :model="user" status-icon :rules="rules">
    7.         <div class="title">火星</div>
    8.         <el-form-item prop="userName">
    9.           <el-input
    10.             type="text"
    11.             prefix-icon="user"
    12.             v-model="user.userName"
    13.           />
    14.         </el-form-item>
    15.         <el-form-item prop="userPwd">
    16.           <el-input
    17.             type="password"
    18.             prefix-icon="view"
    19.             v-model="user.userPwd"
    20.           />
    21.         </el-form-item>
    22.         <el-form-item>
    23.           <el-button type="primary" class="btn-login" @click="login"
    24.             >登录</el-button
    25.           >
    26.         </el-form-item>
    27.       </el-form>
    28.     </div>
    29.   </div>
    30. </template>

    阅读el-form文档可知,表单数据会通过model绑定到user对象中,表单数据的前端验证会通过rules绑定到rules对象中,而el-form标签本身,我们通过ref将其绑定了userForm变量中,方便我们直接通过userForm变量来调用校验方法,相关JS如下:

    1. import { reactive, ref, getCurrentInstance  } from 'vue';
    2. import api from '../api';
    3. import router from '../router';
    4. import store from '../store';
    5. // 表单提交数据
    6. const user = reactive({
    7.   userName: "",
    8.   userPwd: ""
    9. })
    10. // 表单对象
    11. const userForm = ref();
    12. // 校验规则
    13. const rules = {
    14.     userName: [
    15.       {
    16.         required: true,
    17.         message: "请输入用户名",
    18.         trigger: "blur",
    19.       },
    20.     ],
    21.     userPwd: [
    22.       {
    23.         required: true,
    24.         message: "请输入密码",
    25.         trigger: "blur",
    26.       },
    27.     ],
    28. }
    29. // 登录方法
    30. function login() {
    31.   // 通过userForm表单对象调用validate方法,实现前端校验
    32.   userForm.value.validate((valid) => {
    33.     if (valid) {
    34.       api.login(user).then((res) => {
    35.         store.commit("saveUserInfo", res);
    36.         router.push("/welcome");
    37.       });
    38.     } else {
    39.       return false;
    40.     }
    41.   });
    42. }

    el-form标签对象与userForm变量关联,当用户点击登录时,会调用login方法,login方法首先会通过userForm变量调用其中的validate方法(el-form提供的校验方法),基于校验规则(rules变量)对前端内容进行校验,校验通过后,再请求后端登录api,如果登录成功,则将数据记录下来并访问welcome页面。

    这里,还有个细节,使用el-form-item时,要让element-plus帮我们验证,需要通过prop关联一下user对象中的属性:userName和userPwd。

    1. <el-form-item prop="userName">
    2.           <el-input
    3.             type="text"
    4.             prefix-icon="user"
    5.             v-model="user.userName"
    6.           />
    7.         </el-form-item>
    8.         <el-form-item prop="userPwd">
    9.           <el-input
    10.             type="password"
    11.             prefix-icon="view"
    12.             v-model="user.userPwd"
    13.           />
    14.         </el-form-item>

    这样就可以实现实时校验的效果了。e7c57b0a199c73c08f282e1543b43cad.png

    结尾

    虽然是一个很常见的后台,但对于我这种前端比较薄弱的后端同学,还是踩了一些坑的。

    项目代码:https://github.com/ayuliao/DashboardFrameWork

  • 相关阅读:
    轻量级神经网络算法-SqueezeNet
    【linux基础(五)】Linux中的开发工具(上)---yum和vim
    设备树——dtb格式到struct device node结构体的转换
    基于JAVA西藏民族大学论文管理系统计算机毕业设计源码+系统+mysql数据库+lw文档+部署
    React 中的延迟加载Lazy loading
    如何查看电脑连接的wifi的密码
    Confluence 可以用哪些开源知识库替换?盘点主流的11款
    高斯算法的原理及其与常规求和方法的区别
    CSS 媒体查询 @media【详解】
    HTML5与CSS3学习笔记【第十一章 用CSS进行布局(二)】
  • 原文地址:https://blog.csdn.net/weixin_30230009/article/details/124905915