用图鸟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有了一定的了解。