UniApp 实现精美的用户登录和注册页面
前言
在开发鸿蒙APP时,登录注册页面作为用户与应用交互的第一道门槛,其体验与质量往往决定了用户的第一印象。去年我接手了一个电商小程序项目,产品经理特别强调要做一个既美观又实用的登录注册页面。经过一番摸索和实践,我用UniApp实现了一套完整的登录注册流程,今天就把这个过程分享给大家。
设计思路
一个好的登录注册页面应该具备以下特点:
- 简洁美观 - 视觉上吸引用户
- 交互流畅 - 包括动画过渡、表单验证反馈等
- 功能完整 - 常规登录、手机验证码登录、第三方登录等
- 兼容适配 - 适应不同设备尺寸
基于这些考虑,我采用了卡片式设计、渐变色背景,并加入适当的动效,让整个页面既现代又不失实用性。
页面结构
我们的登录注册系统包含三个主要页面:
- 登录页面 - 支持账号密码登录和手机验证码登录
- 注册页面 - 新用户注册表单
- 忘记密码页面 - 用于密码找回
下面我们逐一实现这些页面。
登录页面实现
首先来看登录页面的结构代码:
<template>
<view class="login-container">
<!-- 背景 -->
<view class="bg-wrapper">
<image class="bg-image" src="/static/images/login-bg.jpg" mode="aspectFill"></image>
<view class="bg-mask"></view>
</view>
<!-- 顶部Logo -->
<view class="logo-box">
<image class="logo" src="/static/images/logo.png" mode="widthFix"></image>
<text class="slogan">探索移动应用的无限可能</text>
</view>
<!-- 登录表单 -->
<view class="login-form">
<view class="tab-header">
<view
class="tab-item"
:class="{ active: loginType === 'password' }"
@tap="switchLoginType('password')"
>
账号登录
</view>
<view
class="tab-item"
:class="{ active: loginType === 'sms' }"
@tap="switchLoginType('sms')"
>
短信登录
</view>
<view class="tab-line" :style="tabLineStyle"></view>
</view>
<!-- 账号密码登录 -->
<view v-if="loginType === 'password'" class="form-content">
<view class="input-item">
<uni-icons type="person" size="20" color="#999"></uni-icons>
<input
class="input"
type="text"
placeholder="请输入账号/手机号"
v-model="account"
/>
</view>
<view class="input-item">
<uni-icons type="locked" size="20" color="#999"></uni-icons>
<input
class="input"
:type="showPassword ? 'text' : 'password'"
placeholder="请输入密码"
v-model="password"
/>
<view class="eye-icon" @tap="togglePasswordVisibility">
<uni-icons :type="showPassword ? 'eye' : 'eye-slash'" size="20" color="#999"></uni-icons>
</view>
</view>
</view>
<!-- 手机验证码登录 -->
<view v-else class="form-content">
<view class="input-item">
<uni-icons type="phone" size="20" color="#999"></uni-icons>
<input
class="input"
type="number"
maxlength="11"
placeholder="请输入手机号"
v-model="phone"
/>
</view>
<view class="input-item">
<uni-icons type="chat" size="20" color="#999"></uni-icons>
<input
class="input"
type="number"
maxlength="6"
placeholder="请输入验证码"
v-model="smsCode"
/>
<view
class="code-btn"
:class="{ disabled: counting }"
@tap="sendSmsCode"
>
{{ codeText }}
</view>
</view>
</view>
<!-- 登录按钮 -->
<view class="action-area">
<button
class="login-btn"
:disabled="!isFormValid"
:class="{ disabled: !isFormValid }"
@tap="handleLogin"
>
登录
</button>
<view class="additional-links">
<navigator url="/pages/auth/register" class="link-item">注册账号</navigator>
<navigator url="/pages/auth/forgot-password" class="link-item">忘记密码</navigator>
</view>
</view>
<!-- 第三方登录 -->
<view class="third-party-login">
<view class="divider">
<view class="line"></view>
<text class="text">其他登录方式</text>
<view class="line"></view>
</view>
<view class="icons-row">
<view class="icon-item" @tap="thirdPartyLogin('wechat')">
<image src="/static/images/wechat.png" mode="widthFix"></image>
</view>
<view class="icon-item" @tap="thirdPartyLogin('apple')">
<image src="/static/images/apple.png" mode="widthFix"></image>
</view>
</view>
</view>
</view>
<!-- 底部隐私政策 -->
<view class="privacy-policy">
登录即表示您同意<text class="policy-link" @tap="openPrivacyPolicy">《用户协议》</text>和<text class="policy-link" @tap="openPrivacyPolicy">《隐私政策》</text>
</view>
</view>
</template>
上面的模板看起来代码挺多,但结构非常清晰。接下来实现逻辑部分:
<script>
export default {
data() {
return {
loginType: 'password', // 登录类型:password-密码登录,sms-短信登录
account: '',
password: '',
phone: '',
smsCode: '',
showPassword: false,
counting: false, // 是否正在倒计时
countDown: 60, // 倒计时秒数
codeText: '获取验证码'
};
},
computed: {
tabLineStyle() {
return {
transform: this.loginType === 'password' ? 'translateX(0)' : 'translateX(100%)'
};
},
isFormValid() {
if (this.loginType === 'password') {
return this.account.trim() && this.password.trim();
} else {
return this.phone.trim().length === 11 && this.smsCode.trim().length === 6;
}
}
},
methods: {
// 切换登录方式
switchLoginType(type) {
this.loginType = type;
},
// 切换密码可见性
togglePasswordVisibility() {
this.showPassword = !this.showPassword;
},
// 发送短信验证码
sendSmsCode() {
if (this.counting) return;
// 验证手机号
if (!this.validatePhone()) {
return;
}
// 开始倒计时
this.counting = true;
this.countDown = 60;
this.codeText = `${this.countDown}秒后重发`;
const timer = setInterval(() => {
this.countDown--;
this.codeText = `${this.countDown}秒后重发`;
if (this.countDown <= 0) {
clearInterval(timer);
this.counting = false;
this.codeText = '获取验证码';
}
}, 1000);
// 发送验证码请求
this.requestSmsCode();
},
// 验证手机号
validatePhone() {
if (!/^1[3-9]\d{9}$/.test(this.phone)) {
uni.showToast({
title: '请输入正确的手机号',
icon: 'none'
});
return false;
}
return true;
},
// 请求发送验证码
requestSmsCode() {
// 实际项目中这里应该调用API发送验证码
uni.showLoading({ title: '发送中...' });
setTimeout(() => {
uni.hideLoading();
uni.showToast({
title: '验证码已发送',
icon: 'success'
});
}, 1500);
},
// 处理登录
handleLogin() {
if (!this.isFormValid) return;
uni.showLoading({ title: '登录中...' });
// 登录验证逻辑
setTimeout(() => {
uni.hideLoading();
// 模拟登录成功
uni.setStorageSync('token', 'demo_token_' + Date.now());
uni.setStorageSync('userInfo', {
id: 1,
nickname: '测试用户',
avatar: '/static/images/avatar.png'
});
uni.showToast({
title: '登录成功',
icon: 'success',
duration: 1500,
success: () => {
// 延迟跳转,让用户看到成功提示
setTimeout(() => {
uni.switchTab({
url: '/pages/home/home'
});
}, 1500);
}
});
}, 2000);
},
// 第三方登录
thirdPartyLogin(type) {
uni.showToast({
title: `正在尝试${type === 'wechat' ? '微信' : '苹果'}登录`,
icon: 'none'
});
// 这里应该调用对应的第三方登录API
// 微信登录示例
if (type === 'wechat' && uni.getSystemInfoSync().platform !== 'devtools') {
uni.login({
provider: 'weixin',
success: (loginRes) => {
console.log('微信登录成功', loginRes);
// 获取用户信息
uni.getUserInfo({
provider: 'weixin',
success: (infoRes) => {
console.log('获取用户信息成功', infoRes);
// 将用户信息传给后端进行登录验证
// this.wxLoginToServer(loginRes.code, infoRes.userInfo);
}
});
},
fail: (err) => {
console.error('微信登录失败', err);
}
});
}
},
// 打开隐私政策
openPrivacyPolicy() {
uni.navigateTo({
url: '/pages/common/privacy-policy'
});
}
}
};
</script>
最后是样式部分,一个漂亮的UI很大程度上取决于CSS:
<style lang="scss">
.login-container {
position: relative;
width: 100%;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
overflow: hidden;
}
// 背景样式
.bg-wrapper {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
.bg-image {
width: 100%;
height: 100%;
}
.bg-mask {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(to bottom, rgba(29, 36, 52, 0.5), rgba(29, 36, 52, 0.8));
}
}
// Logo区域
.logo-box {
margin-top: 80rpx;
display: flex;
flex-direction: column;
align-items: center;
.logo {
width: 180rpx;
margin-bottom: 20rpx;
}
.slogan {
font-size: 28rpx;
color: #ffffff;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
}
// 登录表单
.login-form {
width: 85%;
margin-top: 80rpx;
background-color: rgba(255, 255, 255, 0.95);
border-radius: 16rpx;
padding: 40rpx 30rpx;
box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.15);
// 标签头部
.tab-header {
display: flex;
position: relative;
border-bottom: 1px solid #f2f2f2;
margin-bottom: 50rpx;
.tab-item {
flex: 1;
text-align: center;
font-size: 32rpx;
color: #666;
padding-bottom: 20rpx;
transition: all 0.3s;
&.active {
color: #3b7aff;
font-weight: 500;
}
}
.tab-line {
position: absolute;
bottom: 0;
left: 0;
width: 50%;
height: 4rpx;
background-color: #3b7aff;
border-radius: 4rpx;
transition: all 0.3s ease;
}
}
// 表单内容
.form-content {
margin-bottom: 50rpx;
.input-item {
display: flex;
align-items: center;
height: 100rpx;
border-bottom: 1rpx solid #eee;
margin-bottom: 30rpx;
.input {
flex: 1;
height: 100%;
font-size: 30rpx;
padding-left: 20rpx;
}
.eye-icon {
padding: 0 10rpx;
}
.code-btn {
width: 200rpx;
height: 70rpx;
line-height: 70rpx;
background: #3b7aff;
color: #fff;
font-size: 26rpx;
text-align: center;
border-radius: 35rpx;
&.disabled {
background: #cccccc;
}
}
}
}
// 操作区域
.action-area {
.login-btn {
width: 100%;
height: 90rpx;
line-height: 90rpx;
background: linear-gradient(135deg, #4b8eff, #3b7aff);
color: #fff;
font-size: 32rpx;
border-radius: 45rpx;
margin-bottom: 30rpx;
&.disabled {
background: linear-gradient(135deg, #cccccc, #aaaaaa);
}
}
.additional-links {
display: flex;
justify-content: space-between;
font-size: 26rpx;
color: #666;
margin-bottom: 40rpx;
.link-item {
padding: 10rpx;
}
}
}
// 第三方登录
.third-party-login {
margin-top: 30rpx;
.divider {
display: flex;
align-items: center;
margin-bottom: 40rpx;
.line {
flex: 1;
height: 1rpx;
background-color: #eee;
}
.text {
padding: 0 20rpx;
font-size: 26rpx;
color: #999;
}
}
.icons-row {
display: flex;
justify-content: center;
.icon-item {
width: 80rpx;
height: 80rpx;
margin: 0 40rpx;
image {
width: 100%;
height: 100%;
}
}
}
}
}
// 底部隐私政策
.privacy-policy {
position: absolute;
bottom: 40rpx;
font-size: 24rpx;
color: rgba(255, 255, 255, 0.8);
text-align: center;
.policy-link {
color: #4b8eff;
}
}
</style>
注册页面实现
注册页面与登录页面类似,但表单内容有所不同。这里我们简化一下代码,只展示关键部分:
<template>
<view class="register-container">
<!-- 背景和顶部与登录页类似 -->
<view class="register-form">
<view class="form-header">
<text class="title">新用户注册</text>
<text class="subtitle">加入我们,体验更多精彩</text>
</view>
<view class="form-content">
<view class="input-item">
<uni-icons type="phone" size="20" color="#999"></uni-icons>
<input
class="input"
type="number"
maxlength="11"
placeholder="请输入手机号"
v-model="form.phone"
/>
</view>
<view class="input-item">
<uni-icons type="chat" size="20" color="#999"></uni-icons>
<input
class="input"
type="number"
maxlength="6"
placeholder="请输入验证码"
v-model="form.smsCode"
/>
<view
class="code-btn"
:class="{ disabled: counting }"
@tap="sendSmsCode"
>
{{ codeText }}
</view>
</view>
<view class="input-item">
<uni-icons type="locked" size="20" color="#999"></uni-icons>
<input
class="input"
:type="showPassword ? 'text' : 'password'"
placeholder="请设置6-20位密码"
v-model="form.password"
/>
<view class="eye-icon" @tap="togglePasswordVisibility">
<uni-icons :type="showPassword ? 'eye' : 'eye-slash'" size="20" color="#999"></uni-icons>
</view>
</view>
<view class="input-item">
<uni-icons type="locked" size="20" color="#999"></uni-icons>
<input
class="input"
:type="showPassword ? 'text' : 'password'"
placeholder="请确认密码"
v-model="form.confirmPassword"
/>
</view>
</view>
<view class="action-area">
<button
class="register-btn"
:disabled="!isFormValid"
:class="{ disabled: !isFormValid }"
@tap="handleRegister"
>
注册
</button>
<view class="login-link">
已有账号?<text class="link" @tap="goToLogin">去登录</text>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
form: {
phone: '',
smsCode: '',
password: '',
confirmPassword: ''
},
showPassword: false,
counting: false,
countDown: 60,
codeText: '获取验证码'
};
},
computed: {
isFormValid() {
return this.form.phone.length === 11 &&
this.form.smsCode.length === 6 &&
this.form.password.length >= 6 &&
this.form.password === this.form.confirmPassword;
}
},
methods: {
// 密码可见性切换
togglePasswordVisibility() {
this.showPassword = !this.showPassword;
},
// 发送验证码(与登录页类似)
sendSmsCode() {
// 实现与登录页类似
},
// 处理注册
handleRegister() {
if (!this.isFormValid) return;
// 表单验证
if (this.form.password !== this.form.confirmPassword) {
uni.showToast({
title: '两次密码不一致',
icon: 'none'
});
return;
}
uni.showLoading({ title: '注册中...' });
// 模拟注册请求
setTimeout(() => {
uni.hideLoading();
uni.showToast({
title: '注册成功',
icon: 'success',
duration: 1500,
success: () => {
// 延迟跳转到登录页
setTimeout(() => {
uni.navigateTo({
url: '/pages/auth/login'
});
}, 1500);
}
});
}, 2000);
},
// 跳转到登录页
goToLogin() {
uni.navigateBack();
}
}
};
</script>
注册页面的样式可以参考登录页面,只需对一些细节进行调整即可。
实际效果与优化方案
在实际项目中,我们实现的登录注册页面已经投入使用,用户反馈非常好。不过在使用过程中也发现了一些问题和优化点:
1. 动画效果优化
为了让登录体验更流畅,我们加入了一些过渡动画:
// 切换标签时添加动画
switchLoginType(type) {
// 添加动画类
this.animating = true;
setTimeout(() => {
this.loginType = type;
setTimeout(() => {
this.animating = false;
}, 300);
}, 150);
}
对应的CSS:
.form-content {
transition: opacity 0.3s ease;
opacity: 1;
&.animating {
opacity: 0;
}
}
2. 表单验证优化
在用户输入过程中实时验证并给出友好提示:
<view class="input-item" :class="{ error: phoneError }">
<uni-icons type="phone" size="20" color="#999"></uni-icons>
<input
class="input"
type="number"
maxlength="11"
placeholder="请输入手机号"
v-model="phone"
@input="validatePhoneInput"
/>
<text v-if="phoneError" class="error-tip">{{ phoneErrorMsg }}</text>
</view>
validatePhoneInput() {
if (this.phone && !/^1[3-9]\d{9}$/.test(this.phone)) {
this.phoneError = true;
this.phoneErrorMsg = '请输入正确的手机号';
} else {
this.phoneError = false;
this.phoneErrorMsg = '';
}
}
3. 记住登录状态
用户体验优化,记住登录状态避免频繁登录:
// 登录成功后保存状态
handleLoginSuccess(userInfo, token) {
// 保存用户信息和token
uni.setStorageSync('token', token);
uni.setStorageSync('userInfo', userInfo);
uni.setStorageSync('loginTime', Date.now());
// 如果用户勾选了"记住登录状态"
if (this.rememberMe) {
uni.setStorageSync('rememberMe', true);
} else {
uni.removeStorageSync('rememberMe');
}
}
在App启动时检查登录状态:
// 在App.vue的onLaunch中
checkLoginStatus() {
const token = uni.getStorageSync('token');
const rememberMe = uni.getStorageSync('rememberMe');
const loginTime = uni.getStorageSync('loginTime');
const currentTime = Date.now();
// 如果有token且选择了记住登录,或者登录时间在7天内
if (token && (rememberMe || (currentTime - loginTime < 7 * 24 * 60 * 60 * 1000))) {
// token验证
this.verifyToken(token);
} else {
// 清除登录信息,跳转到登录页
this.clearLoginInfo();
uni.reLaunch({
url: '/pages/auth/login'
});
}
}
总结与心得
通过这次实践,我总结了几点关于实现好的登录注册页面的经验:
- 设计先行 - 在编码前先做好设计稿,确保视觉效果和交互流程
- 拆分组件 - 将复杂页面拆分为多个可复用组件,便于维护
- 验证健壮 - 表单验证要全面且给出友好提示
- 安全考虑 - 密码加密传输,防止中间人攻击
- 适配兼容 - 适配不同尺寸的设备,兼容不同平台
UniApp提供了很多实用组件和API,极大简化了我们的开发过程。通过合理利用这些功能,我们能够快速实现一个既美观又实用的登录注册系统。
希望这篇文章对你在UniApp中实现登录注册页面有所帮助。如果有任何问题或建议,欢迎在评论区交流!
参考资料
- UniApp官方文档:https://uniapp.dcloud.io/
- uni-ui组件库:https://ext.dcloud.net.cn/plugin?id=55