完成一键登录和支付功能

This commit is contained in:
future
2026-06-28 14:49:21 +08:00
parent 74876e0923
commit 09e587cb65
94 changed files with 10283 additions and 2665 deletions
+252 -144
View File
@@ -20,48 +20,42 @@
<text class="phone-display">验证码登录</text>
<text class="auth-tip">认证服务由中国移动提供</text>
<view class="form-group">
<view class="input-wrapper" :class="{ focused: isSmsPhoneFocused }">
<input
v-model="smsPhone"
type="number"
placeholder="请输入手机号"
maxlength="11"
class="phone-input"
confirm-type="next"
@focus="isSmsPhoneFocused = true"
@blur="isSmsPhoneFocused = false"
/>
</view>
<view class="form-group phone-form-group">
<input
v-model="smsPhone"
type="number"
placeholder="请输入手机号"
maxlength="11"
class="underline-input"
confirm-type="next"
@focus="isSmsPhoneFocused = true"
@blur="isSmsPhoneFocused = false"
/>
</view>
<view class="form-group">
<view class="input-wrapper code-input" :class="{ focused: isCodeFocused }">
<input
v-model="code"
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 class="form-group code-form-group">
<VerifyCodeInput
ref="codeInputRef"
v-model="code"
:length="4"
:mask="false"
type="underline"
activeColor="#FF8C42"
@complete="handleCodeComplete"
@focus="onInputFocus"
@blur="onInputBlur"
/>
</view>
<button class="login-btn primary" :class="{ disabled: !canSmsSubmit, loading: isLoading }" @tap="handleSmsLogin" :disabled="!canSmsSubmit || isLoading">
<text>{{ isLoading ? '登录中...' : '验证码登录' }}</text>
<button
class="login-btn primary"
:class="{ disabled: countdown > 0 || isLoading }"
@tap="handleSendCode"
:disabled="countdown > 0 || isLoading"
>
<text>{{ countdown > 0 ? `${countdown}秒后重发` : '发送验证码' }}</text>
</button>
<button class="login-btn secondary" @tap="switchToPhone">
<text>其他登录方式</text>
</button>
@@ -84,22 +78,7 @@
<text class="phone-display">本机号码登录</text>
<text class="auth-tip">认证服务由中国移动提供</text>
<view class="form-group phone-login">
<view class="input-wrapper" :class="{ focused: isInputFocused }">
<input
v-model="phone"
type="number"
placeholder="请输入手机号"
maxlength="11"
class="phone-input"
confirm-type="done"
@focus="isInputFocused = true"
@blur="isInputFocused = false"
/>
</view>
</view>
<button class="login-btn primary" :class="{ disabled: !canSubmit, loading: isLoading }" @tap="handlePhoneLogin" :disabled="!canSubmit || isLoading">
<button class="login-btn primary" :class="{ disabled: isLoading, loading: isLoading }" @tap="handlePhoneLogin" :disabled="isLoading">
<text>{{ isLoading ? '登录中...' : '一键登录' }}</text>
</button>
@@ -123,17 +102,14 @@
</template>
<script setup>
import { ref, computed, onUnmounted } from 'vue'
import { setToken } from '@/utils/request.js'
import { ref, onUnmounted } from 'vue'
import { setToken, setRefreshToken } from '@/utils/request.js'
import { loginWithPhone, oneClickLogin, sendCode } from '@/api/main.js'
// 调用真实后端API,不再使用测试数据
import VerifyCodeInput from '@/components/global/VerifyCodeInput.vue'
const activeCard = ref('phone')
const phone = ref('')
const isLoading = ref(false)
const isInputFocused = ref(false)
const agreed = ref(false)
const agreed = ref(true)
const shakeAgreement = ref(false)
const smsPhone = ref('')
@@ -157,13 +133,27 @@ function showToast(msg) {
}, 2000)
}
const canSubmit = computed(() => {
return /^1[3-9]\d{9}$/.test(phone.value) && !isLoading.value
})
const canSmsSubmit = computed(() => {
return /^1[3-9]\d{9}$/.test(smsPhone.value) && /^\d{6}$/.test(code.value) && !isLoading.value
})
/**
* 保存登录信息到本地存储
*/
function saveLoginInfo(loginInfo) {
if (!loginInfo) return
// 保存 token (已经在 request.js 中通过 setToken 保存)
// 保存会员信息
if (loginInfo.memberId || loginInfo.id) {
const memberInfo = {
id: loginInfo.memberId || loginInfo.id,
memberId: loginInfo.memberId || loginInfo.id,
phone: loginInfo.phone || '',
nickname: loginInfo.nickname || '',
avatar: loginInfo.avatar || ''
}
uni.setStorageSync('loginMemberInfo', memberInfo)
console.log('[login] 登录信息已保存:', memberInfo)
}
}
function onAgreementChange(e) {
agreed.value = e.detail.value.includes('agreed')
@@ -175,8 +165,6 @@ function switchToSms() {
function switchToPhone() {
activeCard.value = 'phone'
phone.value = ''
phoneError.value = ''
}
function showAgreement() {
@@ -187,16 +175,20 @@ function showPrivacy() {
uni.showModal({ title: '隐私政策', content: '这里是隐私政策内容...', showCancel: false })
}
function validatePhone() {
if (!phone.value) {
showToast('请输入手机号')
return false
function handleCodeComplete(val) {
if (!isLoading.value) {
setTimeout(() => {
handleSmsLogin()
}, 100)
}
if (!/^1[3-9]\d{9}$/.test(phone.value)) {
showToast('请输入正确的手机号')
return false
}
return true
}
function onInputFocus() {
isCodeFocused.value = true
}
function onInputBlur() {
isCodeFocused.value = false
}
function validateSmsPhone() {
@@ -217,34 +209,146 @@ async function handlePhoneLogin() {
return
}
if (!validatePhone()) return
isLoading.value = true
try {
const res = await oneClickLogin({
accessToken: phone.value,
openid: 'uniapp_phone_login',
nickname: '',
avatar: ''
await new Promise((resolve, reject) => {
uni.preLogin({
provider: 'univerify',
success: resolve,
fail: (err) => {
console.warn('预登录失败,切换到验证码登录:', err)
reject(err)
}
})
})
if (res && res.accessToken) {
setToken(res.accessToken)
uni.setStorageSync('memberInfo', res)
uni.setStorageSync('isLogin', true)
showToast('登录成功')
setTimeout(() => {
uni.switchTab({ url: '/pages/memberInfo/memberInfo' })
}, 1500)
} else {
showToast('登录失败,请重试')
}
uni.login({
provider: 'univerify',
loginBtnText: '本机号码一键登录',
univerifyStyle: {
fullScreen: true,
backgroundColor: '#ffffff',
icon: {
path: '/static/logo.png',
width: '60px',
height: '60px'
},
phoneNum: {
color: '#333333'
},
slogan: {
color: '#999999'
},
authButton: {
normalColor: '#FF8C42',
highlightColor: '#E07A38',
disabledColor: '#FFB88A',
textColor: '#ffffff',
title: '本机号码一键登录',
borderRadius: '24px'
},
otherLoginButton: {
visible: true,
normalColor: '#ffffff',
highlightColor: '#f5f5f5',
textColor: '#666666',
title: '其他登录方式',
borderColor: '#e5e5e5',
borderRadius: '24px'
},
privacyTerms: {
defaultCheckBoxState: true,
textColor: '#999999',
termsColor: '#FF8C42',
prefix: '我已阅读并同意',
suffix: '并使用本机号码登录',
privacyItems: [
{
url: 'https://example.com/agreement',
title: '用户服务协议'
},
{
url: 'https://example.com/privacy',
title: '隐私政策'
}
]
}
},
success: async (loginRes) => {
uni.closeAuthView()
try {
const authResult = loginRes.authResult
if (!authResult) {
console.error('authResult为空:', loginRes)
showToast('一键登录失败,请使用验证码登录')
switchToSms()
isLoading.value = false
return
}
const accessToken = authResult.accessToken || authResult.access_token || authResult.token
const openid = authResult.openid || authResult.openId
console.log('accessToken:', accessToken, 'openid:', openid)
if (!accessToken) {
console.error('accessToken为空,authResult:', authResult)
showToast('一键登录失败,请使用验证码登录')
switchToSms()
isLoading.value = false
return
}
const res = await oneClickLogin({
accessToken: accessToken,
openid: openid,
nickname: '',
avatar: ''
})
if (res && res.accessToken) {
setToken(res.accessToken)
if (res.refreshToken) {
setRefreshToken(res.refreshToken)
}
saveLoginInfo(res)
showToast('登录成功')
setTimeout(() => {
uni.navigateBack({ delta: 1 })
}, 1500)
} else {
showToast('一键登录失败,请使用验证码登录')
switchToSms()
}
} catch (e) {
console.error('一键登录处理失败:', e)
showToast(e?.message || '一键登录失败,请使用验证码登录')
switchToSms()
} finally {
isLoading.value = false
}
},
fail: (err) => {
uni.closeAuthView()
console.error('一键登录失败:', err)
isLoading.value = false
if (err.code === 30008 || err.errMsg && err.errMsg.includes('其他登录方式')) {
switchToSms()
} else {
showToast('一键登录失败,请使用验证码登录')
switchToSms()
}
}
})
} catch (err) {
console.error('一键登录失败:', err)
showToast(err?.message || '登录失败,请重试')
} finally {
console.error('一键登录调用失败:', err)
showToast('一键登录失败,请使用验证码登录')
isLoading.value = false
switchToSms()
}
}
@@ -300,17 +404,13 @@ async function handleSmsLogin() {
triggerAgreementShake()
return
}
if (!validateSmsPhone()) return
if (!code.value) {
showToast('请输入验证码')
if (code.value.length !== 4) {
showToast('请输入4位验证码')
return
}
if (!/^\d{6}$/.test(code.value)) {
showToast('请输入6位验证码')
return
}
isLoading.value = true
try {
@@ -321,8 +421,10 @@ async function handleSmsLogin() {
if (res && res.accessToken) {
setToken(res.accessToken)
uni.setStorageSync('memberInfo', res)
uni.setStorageSync('isLogin', true)
if (res.refreshToken) {
setRefreshToken(res.refreshToken)
}
saveLoginInfo(res)
showToast('登录成功')
setTimeout(() => {
uni.switchTab({ url: '/pages/memberInfo/memberInfo' })
@@ -418,12 +520,12 @@ button::after {
.card-container {
position: relative;
width: 100%;
height: 620rpx;
height: 500rpx;
}
.login-card {
position: absolute;
height: 700rpx;
height: 620rpx;
background: $bg-white;
border-radius: $radius-lg;
padding: 48rpx 40rpx;
@@ -431,13 +533,19 @@ button::after {
will-change: transform, z-index, filter;
}
.card-container {
position: relative;
width: 100%;
height: 560rpx;
}
.sms-card {
z-index: 1;
top: -60rpx;
left: -100rpx;
box-shadow: 0 16rpx 40rpx rgba(0, 0, 0, 0.12);
filter: blur(10rpx);
&.active {
z-index: 10;
top: 0;
@@ -453,7 +561,7 @@ button::after {
left: 0;
box-shadow: 0 24rpx 60rpx rgba(0, 0, 0, 0.18), 0 8rpx 20rpx rgba(0, 0, 0, 0.1);
filter: blur(0);
&:not(.active) {
z-index: 1;
top: -60rpx;
@@ -485,45 +593,45 @@ button::after {
.form-group {
width: 100%;
margin-bottom: 24rpx;
position: relative;
}
.phone-login{
margin-bottom: 150rpx;
}
.input-wrapper {
.phone-form-group {
height: 100rpx;
width: 360rpx;
display: flex;
align-items: center;
background: transparent;
padding: 0 32rpx;
height: 100rpx;
border-bottom: 2rpx solid $border-light;
&.code-input {
padding-right: 16rpx;
}
justify-content: center;
}
.phone-input {
flex: 1;
font-size: 34rpx;
.underline-input {
width: 100%;
height: 100%;
text-align: center;
font-size: 32rpx;
color: $text-dark;
letter-spacing: 2rpx;
background: transparent;
border: none;
border-bottom: 2rpx solid $border-light;
padding: 0 20rpx;
transition: border-color 0.3s ease;
&:focus {
border-bottom-color: $accent-orange;
outline: none;
}
&::placeholder {
color: $text-light;
font-size: 28rpx;
}
}
.send-code-btn {
width: 200rpx;
height: 72rpx;
background: $accent-orange;
color: $text-inverse;
font-size: 26rpx;
border-radius: $radius-sm;
border: none;
&.disabled {
background: $border-light;
color: $text-light;
}
.code-form-group {
height: 100rpx;
display: flex;
align-items: center;
justify-content: center;
}
.login-btn {