【uniapp】用图鸟UI设计登录页面

发布于:2025-03-03 ⋅ 阅读:(111) ⋅ 点赞:(0)

用图鸟UI设计登录页面

先看效果:
在这里插入图片描述

1.UI设计

1、UI设计可以自己从0开始,这里用到的工具推荐:https://www.diygw.com/
在这里插入图片描述
2、可以从各种原型图设计工具中参考,这些平台上提供了丰富的原型图、组件、图标,比如墨刀:https://modao.cc/com24/home?category=project_basic&platform=app
在这里插入图片描述
3、可以从DCloud市场上查找符合自己需求的原型图、组件、图标,DCloud:https://ext.dcloud.net.cn/?cat1=3&cat2=33&type=UpdatedDate&page=4
在这里插入图片描述

2.元素及组件

我是这样理解的,uniapp的页面的最小单位是元素,比如文本、图标、按钮、图片、输入框等。而组件是对各种元素进行组合,并加以位置、颜色、大小等的调整,形成比较通用的组合体。
可以看到,最底层的是元素,而之上的都是布局。
在这里插入图片描述

3.UI模板

uiapp生态提供了丰富的开源UI模板,在DCloud可以看到丰富的UI模板,如果是刚入门的,可以选择一个适合自己的模板库,我这里选择了图鸟UI,原因:
1、提供了案例并且不需要调试就能运行:https://ext.dcloud.net.cn/plugin?id=8503
2、有文档说明:https://vue2.tuniaokj.com/components/icon.html
3、颜色、布局搭配个人比较喜欢
每一种UI模板提供的元素及组件都相差不大,即便用uniapp原生的的元素及组件也是非常丰富的。
在这里插入图片描述
在这里插入图片描述

4.功能设计

1、登录的默认页面设计为密码登录,即通过用户名、密码、验证码登录。
2、除了密码登录以外,还可以自定义其他登录方式,这里设计了短信验证、微信验证、QQ验证。
3、除了登录功能外,还有用户注册、忘记密码(重置密码)功能
因此一共有6个页面,代码目录结构如下:
在这里插入图片描述

5.前端实现

5.1.pages.json

在此文件中增加页面的路径。

{
  "pages": [
   {
     "path": "pages/login",
     "style": {
       "navigationBarTitleText": "授权登录",
       "enablePullDownRefresh": false
     }
   }
 ],
"subPackages": [
   {
     "root": "pages/login",
     "pages": [
         {
         "path": "sms",
         "style": {
           "navigationBarTitleText": "短信登录",
           "enablePullDownRefresh": false
         }
       },
       {
         "path": "wechat",
         "style": {
           "navigationBarTitleText": "微信登录",
           "enablePullDownRefresh": false
         }
       },
       {
         "path": "tq",
         "style": {
           "navigationBarTitleText": "微信登录",
           "enablePullDownRefresh": false
         }
       },
       {
         "path": "register",
         "style": {
           "navigationBarTitleText": "注册账号",
           "enablePullDownRefresh": false
         }
       },
       {
         "path": "forgetpw",
         "style": {
           "navigationBarTitleText": "忘记密码",
           "enablePullDownRefresh": false
         }
       }
     ]
   }
 ]
}

5.2.App.vue

这是uniapp启动的地方,我们加上首次启动的页面

<script>
  import Vue from 'vue'
  import store from './store/index.js'
  import updateCustomBarInfo from './tuniao-ui/libs/function/updateCustomBarInfo.js'
  import config from './config'
  import { getToken } from '@/utils/auth'

  export default {
	onLaunch: function() {
	// ……
	this.initApp()
  },
  methods: {
    // 初始化应用
    initApp() {
      // 初始化应用配置
      this.initConfig()
      // 检查用户登录状态
      //#ifdef H5
      this.checkLogin()
      //#endif
    },
    initConfig() {
      this.globalData.config = config
    },
    checkLogin() {
      if (!getToken()) {
        uni.reLaunch({ url: '/pages/login' })
      }
    }
  }
</script>

<style lang="scss">
  /* 注意要写在第一行,同时给style标签加入lang="scss"属性 */
  @import './tuniao-ui/index.scss';
  @import './tuniao-ui/iconfont.css';
</style>

5.3.login.vue

5.3.1.组件(components)设计

组件也就是可以重复使用的,比如此次设计中的短信登录、qq登录、微信登录在各自的页面中都会重复使用。
view-login-pw.vue

<script>
export default {
  name: "view-login-pw",
  methods: {
    // 跳转
    tn(e) {
      uni.navigateTo({
        url: e,
      });
    },
  },
}
</script>
<template>
  <view class="tn-padding-sm tn-margin-xs" @click="tn('/pages/login')">
    <view
        class="login__way__item--icon tn-flex tn-flex-row-center tn-flex-col-center tn-bg-purple tn-color-white tn-margin-xs"
        >
      <view class="tn-icon-password"></view>
    </view>
    <text hover-class="tn-hover" :hover-stay-time="150">
      密码登录
    </text>
  </view>
</template>

<style scoped lang="scss">
@import "static/scss/login.scss";
</style>

在使用的时候

<template>
  <view class="template-loginsms">
  <!-- 其他代码 -->
  <!--         其他登录方式 -->
        <view class="tn-flex tn-flex-col-center tn-flex-row-center" style="width: 100%;">
          <view class="tn-padding-sm tn-margin-xs">
            <text hover-class="tn-hover" :hover-stay-time="150">
              其他登录方式
            </text>
          </view>
        </view>
        <view class="tn-flex tn-flex-col-center tn-flex-row-center">
          <view-login-pw/>
          <view-login-wechat/>
          <view-login-tq/>
        </view>
  </view>
</template>

5.3.2.login完整代码

<template>
  <view class="template-login">
    <view class="login">
      <!-- 顶部背景图片-->
      <view class="login__bg login__bg--top">
        <image class="bg" src="https://resource.tuniaokj.com/images/login/1/login_top2.jpg" mode="widthFix"></image>
      </view>
      <view class="login__bg login__bg--top">
        <image class="rocket rocket-sussuspension" src="https://resource.tuniaokj.com/images/login/1/login_top3.png"
               mode="widthFix"></image>
      </view>

      <view class="login__wrapper">
        <view class="tn-flex tn-flex-direction-row tn-flex-nowrap tn-flex-col-center tn-flex-row-center">
        </view>
        <!-- 输入框内容-->
        <view class="login__info tn-flex tn-flex-direction-column tn-flex-col-center tn-flex-row-center">
          <view
              class="login__info__item__input tn-flex tn-flex-direction-row tn-flex-nowrap tn-flex-col-center tn-flex-row-left">
            <view class="login__info__item__input__left-icon">
              <view class="tn-icon-my"></view>
            </view>
            <view class="login__info__item__input__content">
              <input v-model="loginForm.username" maxlength="20" placeholder-class="input-placeholder"
                     placeholder="手机号/用户名/邮箱"/>
            </view>
          </view>

          <view
              class="login__info__item__input tn-flex tn-flex-direction-row tn-flex-nowrap tn-flex-col-center tn-flex-row-left">
            <view class="login__info__item__input__left-icon">
              <view class="tn-icon-lock"></view>
            </view>
            <view class="login__info__item__input__content">
              <input :password="!showPassword" v-model="loginForm.password" placeholder-class="input-placeholder"
                     placeholder="密码"/>
            </view>
            <view class="login__info__item__input__right-icon" @click="showPassword = !showPassword">
              <view :class="[showPassword ? 'tn-icon-eye' : 'tn-icon-eye-hide']"></view>
            </view>
          </view>
          <view class="tn-flex tn-flex-col-center" style="width: 100%;">
            <view
                class="login__info__item__input tn-flex tn-flex-direction-row tn-flex-nowrap tn-flex-col-center tn-flex-row-left">
              <view class="login__info__item__input__left-icon">
                <view class="tn-icon-safe"></view>
              </view>
              <view class="login__info__item__input__content">
                <input maxlength="4" v-model="loginForm.captcha_input" placeholder-class="input-placeholder" placeholder="验证码"/>
              </view>
            </view>
            <view
                class="tn-flex tn-flex-direction-row tn-flex-nowrap tn-flex-col-center tn-flex-row-left"
                style="margin-top:31px;">
              <image :src="codeUrl" @click="getCode" mode="heightFix" class=""
                     style="height: 38px;width: 100px;"></image>
            </view>
          </view>
          <view class="">
          </view>

          <view class="tn-flex login__info__item__button">
            <view class="tn-flex-1 justify-content-item tn-text-center">
              <tn-button shape="round" backgroundColor="tn-cool-bg-color-7--reverse"
                         padding="40rpx 0" width="100%" shadow fontBold @click="handleLogin">
                <text class="tn-color-white" hover-class="tn-hover" :hover-stay-time="150">
                  登 录
                </text>
              </tn-button>
            </view>
          </view>

          <view class="tn-flex tn-flex-row-center" style="width: 100%;">
            <tn-button width="100%" @click="tn('/pages/login/register')">
              <text hover-class="tn-hover" :hover-stay-time="150">
                注册账号
              </text>
            </tn-button>
            <tn-button width="100%" @click="tn('/pages/login/forgetpw')">
              <text hover-class="tn-hover" :hover-stay-time="150">
                忘记密码
              </text>
            </tn-button>
          </view>

        </view>

        <!--         其他登录方式 -->
        <view class="tn-flex tn-flex-col-center tn-flex-row-center" style="width: 100%;">
          <view class="tn-padding-sm tn-margin-xs">
            <text hover-class="tn-hover" :hover-stay-time="150">
              其他登录方式
            </text>
          </view>
        </view>
        <view class="tn-flex tn-flex-col-center tn-flex-row-center">
          <view-login-sms/>
          <view-login-wechat/>
          <view-login-tq/>
        </view>
      </view>


      <!-- 底部背景图片-->
      <view class="login__bg login__bg--bottom">
        <image src="https://resource.tuniaokj.com/images/login/1/login_bottom_bg.jpg" mode="widthFix"></image>
      </view>
    </view>

    <!-- 验证码倒计时 -->
    <!--    <tn-verification-code-->
    <!--        ref="code"-->
    <!--        uniqueKey="login-demo-1"-->
    <!--        :seconds="60"-->
    <!--        @change="codeChange">-->
    <!--    </tn-verification-code>-->
  </view>
</template>

<script>
import template_page_mixin from '@/libs/mixin/template_page_mixin.js'
import {getCodeImg, login, register} from '@/api/login'
import ViewLoginSms from "@/components/login/view-login-sms.vue";
import ViewLoginWechat from "@/components/login/view-login-wechat.vue";
import ViewLoginTq from "@/components/login/view-login-tq.vue";

export default {
  name: 'templateLogin',
  components: {ViewLoginTq, ViewLoginWechat, ViewLoginSms},
  mixins: [template_page_mixin],
  data() {
    return {
      globalConfig: getApp().globalData.config,
      // 是否显示密码
      showPassword: false,
      // 倒计时提示文字
      tips: '获取验证码',
      codeUrl: "",
      loginForm: {
        username: "admin",
        password: "123456",
        captcha_input: "",
        captcha_crypt: '',
      }
    }
  },
  created() {
    this.getCode()
  },
  methods: {
    // 跳转
    tn(e) {
      uni.navigateTo({
        url: e,
      });
    },
    // 获取验证码
    getCode() {
      getCodeImg().then(res => {
        let captcha = res.captcha
        this.captchaEnabled = captcha.captchaEnabled === undefined ? true : captcha.captchaEnabled
        if (this.captchaEnabled) {
          this.codeUrl = 'data:image/gif;base64,' + captcha.captcha_img
          this.loginForm.captcha_crypt = captcha.captcha_crypt
        }
      })
    },
    // 登录方法
    async handleLogin() {
      if (this.loginForm.username === "") {
        this.$modal.msgError("请输入您的账号")
      } else if (this.loginForm.password === "") {
        this.$modal.msgError("请输入您的密码")
      } else if (this.loginForm.captcha_input === "" && this.captchaEnabled) {
        this.$modal.msgError("请输入验证码")
      } else {
        this.$modal.loading("登录中,请耐心等待...")
        this.pwdLogin()
      }
    },
    // 密码登录
    async pwdLogin() {
      this.$store.dispatch('Login', this.loginForm).then(() => {
        this.$modal.closeLoading()
        this.loginSuccess()
      }).catch(() => {
        if (this.captchaEnabled) {
          this.getCode()
        }
      })
    },
    // 登录成功后,处理函数
    loginSuccess(result) {
      // 设置用户信息
      this.$store.dispatch('GetInfo').then(res => {
        this.$tab.reLaunch('/pages/index')
      })
    }
  }
}
</script>

<style lang="scss" scoped>
@import "static/scss/login.scss";
/* 胶囊*/
.tn-custom-nav-bar__back {
  width: 100%;
  height: 100%;
  position: relative;
  display: flex;
  justify-content: space-evenly;
  align-items: center;
  box-sizing: border-box;
  background-color: rgba(0, 0, 0, 0.15);
  border-radius: 1000rpx;
  border: 1rpx solid rgba(255, 255, 255, 0.5);
  color: #FFFFFF;
  font-size: 18px;

  .icon {
    display: block;
    flex: 1;
    margin: auto;
    text-align: center;
  }

  &:before {
    content: " ";
    width: 1rpx;
    height: 110%;
    position: absolute;
    top: 22.5%;
    left: 0;
    right: 0;
    margin: auto;
    transform: scale(0.5);
    transform-origin: 0 0;
    pointer-events: none;
    box-sizing: border-box;
    opacity: 0.7;
    background-color: #FFFFFF;
  }
}

/* 悬浮 */
.rocket-sussuspension {
  animation: suspension 3s ease-in-out infinite;
}

@keyframes suspension {
  0%, 100% {
    transform: translate(0, 0);
  }
  50% {
    transform: translate(-0.8rem, 1rem);
  }
}

/deep/ .input-placeholder {
  font-size: 30rpx;
  color: #C6D1D8;
}

</style>

6.后端代码

后端基于odoo17框架实现,odoo17与odoo18在登录验证时有一些区别。

6.1.login

@http.route('/uniapp/login', type='http', auth='none', readonly=False, csrf=False, cors='*')
    def uniapp_login(self, redirect=None, **kw):
        """
        uniapp 登录
        """
        ensure_db()
        if request.httprequest.method != 'POST':
            return json.dumps({'code': 401, 'result': 'fail', 'message': '需要POST请求'})
        # simulate hybrid auth=user/auth=public, despite using auth=none to be able
        # to redirect users when no db is selected - cfr ensure_db()
        if request.env.uid is None:
            if request.session.uid is None:
                # no user -> auth=public with specific website public user
                request.env["ir.http"]._auth_method_public()
            else:
                # auth=user
                request.update_env(user=request.session.uid)
        params = json.loads(request.httprequest.data)
        try:
            # token过期校验
            token = params.get('token')
            if token:
                user_token = request.env['uniapp.user.token'].sudo().search([('token', '=', token)])
                if user_token and user_token.validate_token_timeout():
                    return json.dumps({'code': 200, 'result': 'success', 'message': '登录成功'})
            # 密码验证
            # odoo18用的是credential
            # credential = {key: value for key, value in params.items() if key in CREDENTIAL_PARAMS and value}
            # credential.setdefault('type', 'password')
            request.session.authenticate(request.db, params['login'], params['password'])
            # 验证码验证
            captcha_crypt = params.get('captcha_crypt', '')
            captcha_input = params.pop('captcha_input', '')
            captcha_client = Captcha()
            is_captcha_valid = captcha_client.is_captcha_valid(captcha_crypt, captcha_input)
            if not is_captcha_valid:
                captcha, captcha_crypt = captcha_client._captcha()
                captcha_data = {
                    'captcha': captcha,
                    'captcha_crypt': captcha_crypt,
                }
                return json.dumps({'code': 400, 'result': 'fail', 'message': '验证码错误', 'captcha_data': captcha_data})
            token = generate_jwt(request.env.user.name)
            return json.dumps({'code': 200, 'result': 'success', 'message': '登录成功', 'token': token})
        except odoo.exceptions.AccessDenied as e:
            return json.dumps({'code': 400, 'result': 'fail', 'message': '用户名或密码错误'})

6.2.jwt

def generate_jwt(username):
    payload = {
        'username': username,
        'exp': datetime.utcnow() + timedelta(seconds=JWT_EXPIRATION_DELTA_SECONDS)
    }
    return jwt.encode(payload, SECRET_KEY, algorithm='HS256')

def jwt_required(func):
    """
    # 辅助函数:装饰器,用于保护路由
    """
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        token = None
        if 'Authorization' in request.httprequest.headers:
            token = request.httprequest.headers['Authorization'].split(" ")[1]
        if not token:
            return json.dumps({'code': 400, 'result': 'fail', 'message': 'Token is missing!'})
        try:
            data = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
        except jwt.ExpiredSignatureError:
            return json.dumps({'code': 400, 'result': 'fail', 'message': 'Token has expired!'})
        except jwt.InvalidTokenError:
            return json.dumps({'code': 400, 'result': 'fail', 'message': 'Token is invalid!'})
        username = data['username']
        user_id = request.env['res.users'].sudo().search([('name', '=', username)])
        if not user_id:
            return json.dumps({'code': 400, 'result': 'fail', 'message': 'User not found!'})
        request.user_id = user_id
        return func(*args, **kwargs)
    return wrapper

7.代码总结

1、经过uniapp的开发,对web的html布局也有更好的理解了。
2、在看了uniapp的组件开发后,也对odoo目前的组件中比如slot有了一定的了解。


网站公告

今日签到

点亮在社区的每一天
去签到