• Vue企业级优雅实战05-框架开发01-登录界面


    预览本文的实现效果:

    # gitee
    git clone git@gitee.com:cloudyly/dscloudy-admin-single.git
    # github
    git clone git@github.com:cloudyly/dscloudy-admin-single.git
    git checkout 03_LoginPage
     

    经过前面的一系列准备工作,现在终于进入到框架基础功能及组件的正式开发阶段!我会从登录界面到主界面、业务功能一步步迭代开发,在迭代开发过程中逐步实现通用组件及基础功能框架、第三方框架的整合。首先是登录,依次从界面、表单校验,到网络请求、mock 数据、状态管理等展开描述。本文主要内容:开发登录界面,实现登录界面的国际化及登录表单校验。后续文章将围绕登录,从网络请求 axios 的封装、Mock JS、状态管理等方面进行实现。登录界面效果图如下:

    登录界面效果图

    Git 本地仓库切换新分支:

    git checkout -b 03_LoginPage

    确认分支:

    git branch
     

    1 准备工作

    1.1 定义插座

    通过 vue-cli 创建的工程,App.vue 文件中默认会生成很多内容,将里面的模板、JS、样式都删除,只保留路由的插座即可:

    <template>
      <div id="app">
        <router-view/>
      </div>
    </template>
    <style lang="scss">
    </style>
     

    1.2 创建页面

    登录功能属于核心模块,在 core module 中创建登录页面 login.vue,该文件暂时保留最简代码。

    src/modules/core/pages/login.vue:

    <template>
        <div>登录页面</div>
    </template>
    <script>
    export default {
      name: 'login'
    }
    </script>
    <style scoped>
    </style>
     

    1.3 配置路由

    修改路由配置,删除脚手架默认创建的路由,定义登录界面的路由。

    src/router/index.js

    import Vue from 'vue'
    import VueRouter from 'vue-router'
    Vue.use(VueRouter)
    const routes = [
      {
        path: '/login',
        name: 'Login',
        component: () => import(/* webpackChunkName: "about" */ '../modules/core/pages/login')
      }
    ]
    const router = new VueRouter({
      mode: 'history',
      base: process.env.BASE_URL,
      routes
    })
    export default router
     

    1.4 安装模块

    我们采用模块化的架构,需要将模块安装到主工程中才能加载该模块。首先在 main.js 中安装 core 模块:

     ...
    import moduleCore from '@/modules/core'
    ...
    Vue.use(moduleCore, store)
    ...
     

    1.5 测试

    经过上面的几个步骤,已经添加并完成登录界面、核心模块的配置。先测试上述操作是否正确,再进行后续的编码实现工作。在浏览器中访问 http://localhost:8080/login, 如果能否访问刚才创建的登录页面(如下图所示)、控制台不报错,则说明上面的改造没有问题。

    测试创建登录页面

    1.6 特别说明

    这里需要特别说明:上面的路由设置只是临时性的,后面会对路由进行改造,包括动态路由加载、路由配置规则、路由权限控制等内容。不要幻想着一步登天、一口气吃成一个大胖子。实战驱动,需要明确目标、专注。循序渐进、目标分解。跟着文章的节奏,逐个功能点开发实现,最终会潜移默化的得到想得到的。

    2 国际化改造

    由于系统模块较多,需要国际化的文本也多,如果按照之前定义国家化语言文件的方式,会导致 src/i18n 中的 en.jszh.js 文件太过庞大,故上面两个文件只保留框架级的语言,其他语言放在各个模块中、并按功能模块进行拆分配置。

    2.1 创建目录及文件

    src/modules/core/ 按照如下结构创建目录及文件:

     
    modules/
    |- core/
        |- i18n/              存放 core module 的语言文件
            |- login/         存放登录功能的语言文件
                |- en.js        登录功能的英文
                |- zh.js        登录功能的中文
            |- index.js   导入并导出 core module 所有功能涉及的英文和中文
        |- ...
        |- index.js
     

    每个 module 中的 i18n 目录,都用于存放语言文件。在该目录下可以创建多个子目录,每个子目录对应一个功能,里面分别包含 en.jszh.js 文件。 在与子目录平级(i18n目录下)创建 index.js,该文件引入各个子目录功能的语言配置。 最后只需要在 src/i18n 中引入各个模块的语言文件即可。 这样便将语言文件分散到各个 module 中,便于各个 module 自行维护。

    2.2 配置 core module 语言文件

    src/modules/core/i18n/login/en.js:

     
    export default {
      loginTitle: 'User Sign In',
      usernamePlaceHolder: 'Please input username',
      passwordPlaceHolder: 'Please input password',
      validCodePlaceHolder: 'Please input valid code',
      login: 'SIGN IN',
      username: 'Username',
      password: 'Password',
      validCode: 'Valid Code',
      loginError: 'Sign Error',
      userInfoError: 'Get User Info Error',
      noFunctionPermission: 'the account has no function permission'
    }
     

    src/modules/core/i18n/login/zh.js:

     
    export default {
      loginTitle: '用户登录',
      usernamePlaceHolder: '请输入用户名',
      passwordPlaceHolder: '请输入密码',
      validCodePlaceHolder: '请输入验证码',
      login: '登录',
      username: '用户名',
      password: '密码',
      validCode: '验证码',
      loginError: '登录失败',
      userInfoError: '获取用户信息失败',
      noFunctionPermission: '该用户没有菜单权限'
    }
     

    src/modules/core/i18n/index.js 中将其进行聚合并导出:

     import loginEn from './login/en'
    import loginZh from './login/zh'
    export default {
      en: {
        login: loginEn
      },
      zh: {
        login: loginZh
      }
    }

    2.3 全局引入

    完成 core 中登录功能的语言配置后,需要将其引入到全局的语言配置(src/i18n/)中

    src/i18n/en.js

     import core from '@/modules/core/i18n/index'
    export default {
      app: {
        appName: 'Micro Service Micro Front Platform'
      },
      valid: {
        notNull: ' Not Blank',
        minLength: ' min length is ',
        maxLength: ' max length is ',
        lengthIs: ' length must '
      },
      core: core.en
    }
     

    src/i18n/zh.js

    import core from '@/modules/core/i18n/index'
    export default {
      app: {
        appName: '微服务微前端基础框架'
      },
      valid: {
        notNull: ' 不能为空',
        minLength: ' 最小长度是 ',
        maxLength: ' 最大长度是 ',
        lengthIs: ' 长度必须是 '
      },
      core: core.zh
    }
     

    3 登录界面开发

    3.1 布局结构

    界面布局结构如下:

    登录界面布局结构

    整个界面的布局采用弹性盒子模型(flex),在《全局设置》中,创建了 mixin.scss,里面定义了一些样式,通过 @import 引入该文件后,便可以方便的使用 @include 混入里面的样式类,这里使用到了 flex()flex-col()

    3.2 代码实现

    src/modules/core/pages/login.vue:

     <template>
      <div class="site">
        <div class="login-container">
          <div class="title">{{ $t('app.appName') }}</div>
          <div class="login-wrap">
            <div class="login-title">{{ $t('core.login.loginTitle') }}</div>
            <div class="login-form">
              <el-form ref="form" label-position="left" :model="loginForm">
                <el-form-item prop="username">
                  <el-input v-model="loginForm.username" :placeholder="$t('core.login.usernamePlaceHolder')">
                    <ds-svg-icon slot="prefix" class-name="login-form-icon" icon="user"></ds-svg-icon>
                  </el-input>
                </el-form-item>
                <el-form-item prop="password">
                  <el-input v-model="loginForm.password" :type="passwordType" :placeholder="$t('core.login.passwordPlaceHolder')">
                    <ds-svg-icon slot="prefix" class-name="login-form-icon" icon="pwd"></ds-svg-icon>
                    <span class="icon-eye" slot="suffix" @click="onEyeClick">
                      <ds-svg-icon class-name="login-form-icon" :icon="passwordType === 'password' ? 'eye-close' : 'eye-open'"></ds-svg-icon>
                    </span>
                  </el-input>
                </el-form-item>
                <el-form-item prop="validCode">
                  <el-input v-model="loginForm.validCode" :placeholder="$t('core.login.validCodePlaceHolder')">
                    <ds-svg-icon slot="prefix" class-name="login-form-icon" icon="valid-code"></ds-svg-icon>
                    <span slot="suffix">
                      <img v-if="this.key" :src="validCodeUrl" @click="onCheckCodeClick" class="valid-code-img">
                    </span>
                  </el-input>
                </el-form-item>
                <el-button type="primary" class="submit-btn" @click="onLoginBtnClick">{{ $t('core.login.login') }}</el-button>
              </el-form>
            </div>
          </div>
        </div>
      </div>
    </template>
    <script>
    export default {
      name: 'login',
      data () {
        return {
          passwordType: 'password',
          key: new Date().getTime(),
          validCodeUrl: require('@/assets/demo/valid-code.png'),
          loginForm: {
            username: '',
            password: '',
            validCode: ''
          }
        }
      },
      methods: {
        onEyeClick () {
          this.passwordType = this.passwordType === 'password' ? 'text' : 'password'
        },
        onCheckCodeClick () {
        },
        onLoginBtnClick () {
        }
      }
    }
    </script>
    <style scoped lang="scss">
      @import "~@/assets/scss/config.scss";
      @import "~@/assets/scss/mixin.scss";
      .site {
        background: url("~@/assets/img/login-bg.jpg") no-repeat;
        background-size: 100% 100%;
        @include flex-col(center, center);
        color: $color6;
        .login-container {
           100%;
          height: 120px;
          background-color: rgba(37, 88, 132, 0.4);
          padding: 0 20px;
          @include flex();
          position: relative;
          .title {
            font-size: 48px;
            margin-left: 10%;
          }
          .login-wrap {
             400px;
            height: 290px;
            background: rgba(255, 255, 255, 0.7);
            position: absolute;
            right: 10%;
            border-radius: 3px;
            box-shadow: 1px 2px 2px 1px #FBFBFB;
            .login-title {
              font-size: 18px;
              font-weight: bold;
              color: $colorM4;
              line-height: 60px;
              border-bottom: 1px solid $colorM4;
              margin: 0 20px;
            }
            .login-form {
              margin: 20px 20px;
              ::v-deep .el-input__inner {
                border-radius: 0;
              }
              .login-form-icon {
                font-size: 14px;
                margin-bottom: -1px;
                margin-left: 2px;
                color: $color3;
              }
              .icon-eye {
                margin-right: 5px;
                cursor: pointer;
              }
              .valid-code-img {
                 75px;
                height: 24px;
                margin-top: 2px;
                cursor: pointer;
              }
              .submit-btn {
                margin-top: 20px;
                 100%;
              }
              .el-form-item.is-error .login-form-icon {
                color: red;
              }
            }
          }
        }
      }
    </style>
     

    登录表单设计了三个字段:用户名 username,密码 password,验证码 validCode。实现了密码的明文、暗文切换。

    验证码采用本地图片模拟,后面会专门用一篇文章记录登录的逻辑(前端使用公钥对密码进行处理、后端解析后加密存储、生成 jwt token等,整个过程使用 RSA、Spring Cloud OAuth 2、 JWT、非对称加密等知识)。

    4 表单验证

    4.1 通用规则封装

    在中台后台管理系统中,表单验证是个必备的功能,很多规则可以复用,于是我定义了 rules.js 文件。该文件用于存放通用的规则。现在只使用到非空校验、长度校验规则,后续有新的规则(如时间校验、数字校验、手机号校验等),我会把规则添加到这个文件中,不断迭代完善。

    src/common/rules.js

    import i18n from '@/i18n'
    const validNotNullAndLengthRange = (rule, value, callback, displayName, minLength, maxLength) => {
      if (!value) {
        callback(new Error(`${displayName}${i18n.t('valid.notNull')}`))
      } else {
        if ((minLength > -1) && (value.length < minLength)) {
          callback(new Error(`${displayName}${i18n.t('valid.minLength')}${minLength}`))
        } else if ((maxLength > -1) && (value.length > maxLength)) {
          callback(new Error(`${displayName}${i18n.t('valid.maxLength')}${maxLength}`))
        } else {
          callback()
        }
      }
    }
    const validNotNullAndLength = (rule, value, callback, displayName, length) => {
      if (!value) {
        callback(new Error(`${displayName}${i18n.t('valid.notNull')}`))
      } else {
        if (value.length !== length) {
          callback(new Error(`${displayName}${i18n.t('valid.lengthIs')}${length}`))
        } else {
          callback()
        }
      }
    }
    export default {
      /**
       * 校验字符串: 非空
       * @param displayName 错误提示展示的字段名
       */
      notNull: (displayName) => {
        return { required: true, message: `${displayName}${i18n.t('valid.notNull')}` }
      },
      /**
       * 校验字符串: 非空 且 长度区间
       * @param displayName 错误提示展示的字段名
       * @param minLength   最小长度(-1表示不限制)
       * @param maxLength   最大长度(-1表示不限制)
       */
      notNullAndLengthRange: (displayName, minLength, maxLength) => {
        return {
          validator: (rule, value, callback) => validNotNullAndLengthRange(rule, value, callback, displayName, minLength, maxLength)
        }
      },
      /**
       * 校验字符串: 非空 且 长度等于
       * @param displayName 错误提示展示的字段名
       * @param length   长度值
       */
      notNullAndLength: (displayName, length) => {
        return {
          validator: (rule, value, callback) => validNotNullAndLength(rule, value, callback, displayName, length)
        }
      }
    }
     

    4.2 全局挂载

    由于系统中很多地方都要使用校验规则,将上面的校验规则挂到 Vue 原型的 commonRules 属性上,这样在 Vue 文件便可以方便的通过 this.commonRules.xxx 进行使用。

    main.js

     ...
    import rules from '@/common/rules'
    ...
    Vue.prototype.commonRules = rules
    ...
     

    4.3 登录表单

    改造登录表单,使其支持验证。首先修改 login.vue 的表单,为其添加校验规则 :rule="loginRules"

     <el-form ref="form" label-position="left" :model="loginForm" :rules="loginRules">
     

    然后在 data 中定义规则 loginRules:

       data () {
        return {
          // ...
          loginRules: {
            username: [
              this.commonRules.notNullAndLengthRange(this.$t('core.login.username'), 4, 18)
            ],
            password: [
              this.commonRules.notNullAndLengthRange(this.$t('core.login.password'), 6, 18)
            ],
            validCode: [
              this.commonRules.notNullAndLength(this.$t('core.login.validCode'), 4)
            ]
          }
        }
      },
     

    上面的规则分表表示:

    • 用户名:不能为空,长度 4 - 18 位;

    • 密码:不能为空,长度 6 - 18 位;

    • 验证码:不能为空,长度 4 位。

    刷新页面,现在便可以在输入内容或失去焦点时进行该字段的校验了。但点击按钮,还不会校验表单。

    4.4 提交时校验

    实现点击 ‘登录’ 按钮时,对登录表单进行校验,这里使用 es6 中的 async await 方式进行异步操作:

     
     
     
     
     
     
     
        async onLoginBtnClick () {
          const valid = await this.$refs.form.validate()
          if (!valid) {
            return
          }
          console.log('表单校验通过,提交数据')
        }
     

    此时,点击按钮会对表单进行校验,校验通过才会执行后面的逻辑。

    下一篇将会整合 axios 到工程中。

     

    提交代码:

     git add .
    git cz
    [框架开发] 登录界面
     

    合并到 master 分支:

     git checkout master
    git merge 03_LoginPage
     

    将本地分支分别全部推送到 Gitee 和 GitHub

     git push --all gitee_origin
    git push --all github_origin
     

    更多内容请关注我的个人公众号,留言可加我个人微信或交流问题

    程序员搞艺术

  • 相关阅读:
    题解报告:hdu1995汉诺塔V(递推dp)
    黑色CSS3立体动画菜单
    jQuery计算器插件
    CSS3动画库animate.css
    缩略图悬浮效果的jQuery焦点图
    CSS伪元素实现的3D按钮
    CSS3 3D旋转按钮对话框
    jQuery仿Android锁屏图案应用
    jQuery横向图片手风琴
    jQuery滑动杆打分插件
  • 原文地址:https://www.cnblogs.com/yeahui/p/13844550.html
Copyright © 2020-2023  润新知