完成一键登录和支付功能
This commit is contained in:
@@ -0,0 +1,691 @@
|
||||
<template>
|
||||
<!-- 登录弹窗组件 -->
|
||||
<view class="login-modal-mask" v-if="visible" @tap="handleMaskTap">
|
||||
<view class="login-modal" :class="{ 'login-modal--center': modalStyle === 'center' }" @tap.stop>
|
||||
<!-- 关闭按钮 -->
|
||||
<view class="login-modal__close" v-if="showClose" @tap="handleClose">
|
||||
<text class="icon-close">×</text>
|
||||
</view>
|
||||
|
||||
<!-- 弹窗内容 -->
|
||||
<view class="login-modal__content">
|
||||
<!-- 头部 -->
|
||||
<view class="login-modal__header">
|
||||
<view class="logo-wrapper">
|
||||
<image class="logo-image" src="/static/logo.png" mode="aspectFit" />
|
||||
<text class="app-name">活氧舱</text>
|
||||
</view>
|
||||
<text class="modal-title">{{ title }}</text>
|
||||
<text class="modal-subtitle">{{ subtitle }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 一键登录卡片 -->
|
||||
<view class="login-card" :class="{ active: activeCard === 'phone' }">
|
||||
<view class="card-body">
|
||||
<text class="card-title">本机号码登录</text>
|
||||
<text class="auth-tip">认证服务由中国移动提供</text>
|
||||
|
||||
<button class="login-btn primary" :class="{ disabled: isLoading, loading: isLoading }"
|
||||
@tap="handlePhoneLogin" :disabled="isLoading">
|
||||
<text>{{ isLoading ? '登录中...' : '一键登录' }}</text>
|
||||
</button>
|
||||
|
||||
<button class="login-btn switch-btn" @tap="switchToSms">
|
||||
<text>短信验证码登录</text>
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 短信验证码登录卡片 -->
|
||||
<view class="login-card sms-card" :class="{ active: activeCard === 'sms' }">
|
||||
<view class="card-body">
|
||||
<text class="card-title">验证码登录</text>
|
||||
<text class="auth-tip">认证服务由中国移动提供</text>
|
||||
|
||||
<view class="form-group">
|
||||
<view class="input-wrapper" :class="{ focused: isPhoneFocused }">
|
||||
<input v-model="smsPhone" type="number" placeholder="请输入手机号" maxlength="11"
|
||||
class="phone-input" confirm-type="next" @focus="isPhoneFocused = true"
|
||||
@blur="isPhoneFocused = false" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-group">
|
||||
<view class="input-wrapper code-input" :class="{ focused: isCodeFocused }">
|
||||
<input v-model="smsCode" type="number" placeholder="请输入验证码" maxlength="6"
|
||||
class="phone-input" confirm-type="done" @focus="isCodeFocused = true"
|
||||
@blur="isCodeFocused = false" />
|
||||
<button class="send-code-btn" :class="{ disabled: countdown > 0 || isLoading }"
|
||||
@tap="handleSendCode" :disabled="countdown > 0 || isLoading">
|
||||
<text>{{ countdown > 0 ? `${countdown}秒后重发` : '发送验证码' }}</text>
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<button class="login-btn primary" :class="{ disabled: !canSmsSubmit, loading: isLoading }"
|
||||
@tap="handleSmsLogin" :disabled="!canSmsSubmit || isLoading">
|
||||
<text>{{ isLoading ? '登录中...' : '登录' }}</text>
|
||||
</button>
|
||||
|
||||
<button class="login-btn switch-btn" @tap="switchToPhone">
|
||||
<text>一键登录</text>
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 协议 -->
|
||||
<view class="agreement-text">
|
||||
<checkbox-group @change="onAgreementChange">
|
||||
<checkbox :checked="agreed" value="agreed" color="#FF8C42" />
|
||||
</checkbox-group>
|
||||
<text>登录即表示同意</text>
|
||||
<text class="link" @tap="showAgreement">《用户协议》</text>
|
||||
<text>和</text>
|
||||
<text class="link" @tap="showPrivacy">《隐私政策》</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Toast提示 -->
|
||||
<view class="custom-toast" :class="{ show: toastShow }">
|
||||
<text class="toast-text">{{ toastMessage }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { oneClickLogin, loginWithPhone, sendCode } from '@/api/main.js'
|
||||
|
||||
// Props定义
|
||||
const props = defineProps({
|
||||
// 是否显示弹窗
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 弹窗标题
|
||||
title: {
|
||||
type: String,
|
||||
default: '登录后享受更多服务'
|
||||
},
|
||||
// 弹窗副标题
|
||||
subtitle: {
|
||||
type: String,
|
||||
default: '登录即表示同意相关协议'
|
||||
},
|
||||
// 是否显示关闭按钮
|
||||
showClose: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 弹窗样式: 'center'居中, 'bottom'底部
|
||||
modalStyle: {
|
||||
type: String,
|
||||
default: 'center'
|
||||
},
|
||||
// 是否点击遮罩层关闭
|
||||
maskClosable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 登录成功回调
|
||||
onLoginSuccess: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
// 登录失败回调
|
||||
onLoginError: {
|
||||
type: Function,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
// Emits定义
|
||||
const emit = defineEmits(['update:modelValue', 'loginSuccess', 'loginError', 'close'])
|
||||
|
||||
// 响应式状态
|
||||
const visible = ref(false)
|
||||
const activeCard = ref('phone') // 'phone'一键登录, 'sms'验证码登录
|
||||
const isLoading = ref(false)
|
||||
const isPhoneFocused = ref(false)
|
||||
const isCodeFocused = ref(false)
|
||||
const smsPhone = ref('')
|
||||
const smsCode = ref('')
|
||||
const countdown = ref(0)
|
||||
const agreed = ref(false)
|
||||
const toastShow = ref(false)
|
||||
const toastMessage = ref('')
|
||||
|
||||
// 计算属性
|
||||
const canSmsSubmit = computed(() => {
|
||||
return smsPhone.value.length === 11 && smsCode.value.length === 6 && agreed.value
|
||||
})
|
||||
|
||||
// 监听modelValue变化
|
||||
watch(() => props.modelValue, (newVal) => {
|
||||
visible.value = newVal
|
||||
if (newVal) {
|
||||
// 打开弹窗时重置状态
|
||||
resetState()
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
// 重置状态
|
||||
function resetState() {
|
||||
activeCard.value = 'phone'
|
||||
isLoading.value = false
|
||||
smsPhone.value = ''
|
||||
smsCode.value = ''
|
||||
countdown.value = 0
|
||||
agreed.value = false
|
||||
}
|
||||
|
||||
// 显示Toast
|
||||
function showToast(message, duration = 2000) {
|
||||
toastMessage.value = message
|
||||
toastShow.value = true
|
||||
setTimeout(() => {
|
||||
toastShow.value = false
|
||||
}, duration)
|
||||
}
|
||||
|
||||
// 显示用户协议
|
||||
function showAgreement() {
|
||||
uni.showModal({
|
||||
title: '用户协议',
|
||||
content: '这里是用户协议内容...',
|
||||
showCancel: false
|
||||
})
|
||||
}
|
||||
|
||||
// 显示隐私政策
|
||||
function showPrivacy() {
|
||||
uni.showModal({
|
||||
title: '隐私政策',
|
||||
content: '这里隐私政策内容...',
|
||||
showCancel: false
|
||||
})
|
||||
}
|
||||
|
||||
// 切换到短信登录
|
||||
function switchToSms() {
|
||||
activeCard.value = 'sms'
|
||||
}
|
||||
|
||||
// 切换到一键登录
|
||||
function switchToPhone() {
|
||||
activeCard.value = 'phone'
|
||||
}
|
||||
|
||||
// 协议勾选变化
|
||||
function onAgreementChange(e) {
|
||||
agreed.value = e.detail.value.includes('agreed')
|
||||
}
|
||||
|
||||
// 发送验证码
|
||||
async function handleSendCode() {
|
||||
if (countdown.value > 0 || isLoading.value) return
|
||||
|
||||
if (!smsPhone.value || smsPhone.value.length !== 11) {
|
||||
showToast('请输入正确的手机号')
|
||||
return
|
||||
}
|
||||
|
||||
if (!agreed.value) {
|
||||
showToast('请先同意用户协议')
|
||||
return
|
||||
}
|
||||
|
||||
isLoading.value = true
|
||||
try {
|
||||
await sendCode({ phone: smsPhone.value })
|
||||
showToast('验证码已发送')
|
||||
countdown.value = 60
|
||||
const timer = setInterval(() => {
|
||||
countdown.value--
|
||||
if (countdown.value <= 0) {
|
||||
clearInterval(timer)
|
||||
}
|
||||
}, 1000)
|
||||
} catch (e) {
|
||||
console.error('发送验证码失败:', e)
|
||||
showToast(e.message || '发送验证码失败')
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 一键登录
|
||||
async function handlePhoneLogin() {
|
||||
if (isLoading.value) return
|
||||
|
||||
if (!agreed.value) {
|
||||
showToast('请先同意用户协议')
|
||||
return
|
||||
}
|
||||
|
||||
isLoading.value = true
|
||||
|
||||
try {
|
||||
// #ifdef APP-PLUS
|
||||
// App环境下使用一键登录
|
||||
await new Promise((resolve, reject) => {
|
||||
uni.login({
|
||||
provider: 'univerify',
|
||||
univerifyStyle: {
|
||||
fullScreen: false,
|
||||
backgroundColor: '#ffffff',
|
||||
icon: {
|
||||
path: '/static/logo.png'
|
||||
},
|
||||
closeIcon: {
|
||||
path: '/static/close.png'
|
||||
},
|
||||
phoneNum: {
|
||||
color: '#333333',
|
||||
fontSize: '18px'
|
||||
},
|
||||
slogan: {
|
||||
color: '#999999',
|
||||
fontSize: '12px'
|
||||
},
|
||||
authButton: {
|
||||
normalColor: '#FF8C42',
|
||||
highlightColor: '#FF7A2C',
|
||||
disabledColor: '#CCCCCC',
|
||||
textColor: '#FFFFFF',
|
||||
title: '本机号码一键登录'
|
||||
},
|
||||
otherLoginButton: {
|
||||
visible: false
|
||||
},
|
||||
protocols: []
|
||||
},
|
||||
success: async (res) => {
|
||||
try {
|
||||
// 调用后端API进行一键登录
|
||||
const result = await oneClickLogin({
|
||||
accessToken: res.authResult.access_token,
|
||||
openid: res.authResult.openid
|
||||
})
|
||||
|
||||
if (result.code === 200 || result.data) {
|
||||
handleLoginSuccess(result)
|
||||
resolve(result)
|
||||
} else {
|
||||
reject(new Error(result.message || '登录失败'))
|
||||
}
|
||||
} catch (e) {
|
||||
reject(e)
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('一键登录失败:', err)
|
||||
reject(new Error(err.errMsg || '一键登录失败'))
|
||||
}
|
||||
})
|
||||
})
|
||||
// #endif
|
||||
|
||||
// #ifndef APP-PLUS
|
||||
// 非App环境(小程序、H5等)提示使用其他方式
|
||||
uni.showToast({
|
||||
title: '请使用短信验证码登录',
|
||||
icon: 'none'
|
||||
})
|
||||
activeCard.value = 'sms'
|
||||
// #endif
|
||||
|
||||
} catch (e) {
|
||||
console.error('一键登录失败:', e)
|
||||
showToast(e.message || '一键登录失败')
|
||||
if (props.onLoginError) {
|
||||
props.onLoginError(e)
|
||||
}
|
||||
emit('loginError', e)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 短信验证码登录
|
||||
async function handleSmsLogin() {
|
||||
if (!canSmsSubmit.value || isLoading.value) return
|
||||
|
||||
isLoading.value = true
|
||||
|
||||
try {
|
||||
const result = await loginWithPhone({
|
||||
phone: smsPhone.value,
|
||||
code: smsCode.value
|
||||
})
|
||||
|
||||
handleLoginSuccess(result)
|
||||
|
||||
} catch (e) {
|
||||
console.error('短信登录失败:', e)
|
||||
showToast(e.message || '登录失败')
|
||||
if (props.onLoginError) {
|
||||
props.onLoginError(e)
|
||||
}
|
||||
emit('loginError', e)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 处理登录成功
|
||||
function handleLoginSuccess(result) {
|
||||
console.log('登录成功:', result)
|
||||
// 保存token和用户信息到本地缓存
|
||||
if (result.data?.token) {
|
||||
uni.setStorageSync('token', result.data.token)
|
||||
}
|
||||
// 直接缓存登录响应数据
|
||||
if (result.data) {
|
||||
uni.setStorageSync('loginMemberInfo', result.data)
|
||||
}
|
||||
|
||||
showToast('登录成功')
|
||||
|
||||
if (props.onLoginSuccess) {
|
||||
props.onLoginSuccess(result)
|
||||
}
|
||||
emit('loginSuccess', result)
|
||||
|
||||
uni.$emit('loginModal:success', result)
|
||||
|
||||
setTimeout(() => {
|
||||
handleClose()
|
||||
}, 1500)
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
function handleClose() {
|
||||
visible.value = false
|
||||
emit('update:modelValue', false)
|
||||
emit('close')
|
||||
|
||||
uni.$emit('loginModal:close')
|
||||
}
|
||||
|
||||
// 点击遮罩层
|
||||
function handleMaskTap() {
|
||||
if (props.maskClosable) {
|
||||
handleClose()
|
||||
}
|
||||
}
|
||||
|
||||
// 对外暴露的方法
|
||||
defineExpose({
|
||||
// 显示弹窗
|
||||
show() {
|
||||
visible.value = true
|
||||
emit('update:modelValue', true)
|
||||
},
|
||||
// 隐藏弹窗
|
||||
hide() {
|
||||
handleClose()
|
||||
},
|
||||
// 切换登录方式
|
||||
switchCard(card) {
|
||||
activeCard.value = card
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.login-modal-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.login-modal {
|
||||
position: relative;
|
||||
width: 90%;
|
||||
max-width: 650rpx;
|
||||
background: #fff;
|
||||
border-radius: 24rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.15);
|
||||
|
||||
&--center {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.login-modal__close {
|
||||
position: absolute;
|
||||
top: 20rpx;
|
||||
right: 20rpx;
|
||||
width: 50rpx;
|
||||
height: 50rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10;
|
||||
|
||||
.icon-close {
|
||||
font-size: 48rpx;
|
||||
color: #999;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.login-modal__content {
|
||||
width: 100%;
|
||||
padding: 40rpx 50rpx 50rpx;
|
||||
}
|
||||
|
||||
.login-modal__header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.logo-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 20rpx;
|
||||
|
||||
.logo-image {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.app-name {
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.modal-subtitle {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
display: none;
|
||||
|
||||
&.active {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.sms-card {
|
||||
display: none;
|
||||
|
||||
&.active {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.card-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.auth-tip {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
background: #F5F7FA;
|
||||
border-radius: 16rpx;
|
||||
border: 2rpx solid transparent;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 24rpx;
|
||||
box-sizing: border-box;
|
||||
transition: all 0.3s;
|
||||
|
||||
&.focused {
|
||||
border-color: #FF8C42;
|
||||
background: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.phone-input {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.code-input {
|
||||
padding-right: 0;
|
||||
|
||||
.phone-input {
|
||||
width: 55%;
|
||||
}
|
||||
}
|
||||
|
||||
.send-code-btn {
|
||||
width: 40%;
|
||||
height: 64rpx;
|
||||
line-height: 64rpx;
|
||||
background: #FF8C42;
|
||||
color: #fff;
|
||||
font-size: 24rpx;
|
||||
border-radius: 8rpx;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
&.disabled {
|
||||
background: #ccc;
|
||||
}
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
border-radius: 44rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
&.primary {
|
||||
background: linear-gradient(135deg, #FF8C42 0%, #FF6B2C 100%);
|
||||
color: #fff;
|
||||
border: none;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
background: #ccc;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&.loading {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
&.switch-btn {
|
||||
background: transparent;
|
||||
color: #FF8C42;
|
||||
border: 2rpx solid #FF8C42;
|
||||
}
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.agreement-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8rpx;
|
||||
margin-top: 24rpx;
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
|
||||
.link {
|
||||
color: #FF8C42;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-toast {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%) scale(0);
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
color: #fff;
|
||||
padding: 24rpx 48rpx;
|
||||
border-radius: 12rpx;
|
||||
font-size: 28rpx;
|
||||
z-index: 99999;
|
||||
opacity: 0;
|
||||
transition: all 0.3s;
|
||||
|
||||
&.show {
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user