整合api请求、添加购买会员卡页面、登陆页面
This commit is contained in:
@@ -0,0 +1,569 @@
|
||||
<template>
|
||||
<view class="login-page">
|
||||
<!-- 自定义Toast弹窗 -->
|
||||
<view class="custom-toast" :class="{ show: toastShow }">
|
||||
<text class="toast-text">{{ toastMessage }}</text>
|
||||
</view>
|
||||
|
||||
<view class="login-header">
|
||||
<view class="logo-wrapper">
|
||||
<image class="logo-image" src="/static/logo.png" mode="aspectFit" />
|
||||
<text class="app-name">活氧舱欢迎您</text>
|
||||
<text class="app-slogan">享受运动,拥抱健康</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card-container">
|
||||
<!-- 手机号+验证码登录卡片 -->
|
||||
<view class="login-card sms-card" :class="{ active: activeCard === 'sms' }">
|
||||
<view class="card-body">
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
<button class="login-btn primary" :class="{ disabled: !canSmsSubmit, loading: isLoading }" @tap="handleSmsLogin" :disabled="!canSmsSubmit || isLoading">
|
||||
<text>{{ isLoading ? '登录中...' : '验证码登录' }}</text>
|
||||
</button>
|
||||
|
||||
<button class="login-btn secondary" @tap="switchToPhone">
|
||||
<text>其他登录方式</text>
|
||||
</button>
|
||||
|
||||
<view class="agreement-text" :class="{ shake: shakeAgreement }">
|
||||
<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>
|
||||
|
||||
<!-- 手机号一键登录卡片 -->
|
||||
<view class="login-card phone-card" :class="{ active: activeCard === 'phone' }">
|
||||
<view class="card-body">
|
||||
<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">
|
||||
<text>{{ isLoading ? '登录中...' : '一键登录' }}</text>
|
||||
</button>
|
||||
|
||||
<button class="login-btn secondary" @tap="switchToSms">
|
||||
<text>其他登录方式</text>
|
||||
</button>
|
||||
|
||||
<view class="agreement-text" :class="{ shake: shakeAgreement }">
|
||||
<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>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onUnmounted } from 'vue'
|
||||
import { setToken } from '@/utils/request.js'
|
||||
|
||||
// 测试数据
|
||||
const TEST_TOKEN = 'test_token_123456'
|
||||
const TEST_MEMBER_INFO = {
|
||||
memberId: 10001,
|
||||
memberNo: 'M001',
|
||||
phone: '13800138000',
|
||||
nickname: '测试用户',
|
||||
avatar: '/static/logo.png',
|
||||
accessToken: TEST_TOKEN,
|
||||
isNewUser: false
|
||||
}
|
||||
|
||||
const activeCard = ref('phone')
|
||||
const phone = ref('')
|
||||
const isLoading = ref(false)
|
||||
const isInputFocused = ref(false)
|
||||
const agreed = ref(false)
|
||||
const shakeAgreement = ref(false)
|
||||
|
||||
const smsPhone = ref('')
|
||||
const isSmsPhoneFocused = ref(false)
|
||||
const code = ref('')
|
||||
const isCodeFocused = ref(false)
|
||||
const countdown = ref(0)
|
||||
let timer = null
|
||||
|
||||
// 自定义Toast
|
||||
const toastShow = ref(false)
|
||||
const toastMessage = ref('')
|
||||
let toastTimer = null
|
||||
|
||||
function showToast(msg) {
|
||||
if (toastTimer) clearTimeout(toastTimer)
|
||||
toastMessage.value = msg
|
||||
toastShow.value = true
|
||||
toastTimer = setTimeout(() => {
|
||||
toastShow.value = false
|
||||
}, 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 onAgreementChange(e) {
|
||||
agreed.value = e.detail.value.includes('agreed')
|
||||
}
|
||||
|
||||
function switchToSms() {
|
||||
activeCard.value = 'sms'
|
||||
}
|
||||
|
||||
function switchToPhone() {
|
||||
activeCard.value = 'phone'
|
||||
phone.value = ''
|
||||
phoneError.value = ''
|
||||
}
|
||||
|
||||
function showAgreement() {
|
||||
uni.showModal({ title: '用户协议', content: '这里是用户协议内容...', showCancel: false })
|
||||
}
|
||||
|
||||
function showPrivacy() {
|
||||
uni.showModal({ title: '隐私政策', content: '这里是隐私政策内容...', showCancel: false })
|
||||
}
|
||||
|
||||
function validatePhone() {
|
||||
if (!phone.value) {
|
||||
showToast('请输入手机号')
|
||||
return false
|
||||
}
|
||||
if (!/^1[3-9]\d{9}$/.test(phone.value)) {
|
||||
showToast('请输入正确的手机号')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function validateSmsPhone() {
|
||||
if (!smsPhone.value) {
|
||||
showToast('请输入手机号')
|
||||
return false
|
||||
}
|
||||
if (!/^1[3-9]\d{9}$/.test(smsPhone.value)) {
|
||||
showToast('请输入正确的手机号')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
async function handlePhoneLogin() {
|
||||
if (!agreed.value) {
|
||||
triggerAgreementShake()
|
||||
return
|
||||
}
|
||||
|
||||
if (!validatePhone()) return
|
||||
|
||||
isLoading.value = true
|
||||
|
||||
setTimeout(() => {
|
||||
setToken(TEST_TOKEN)
|
||||
uni.setStorageSync('memberInfo', TEST_MEMBER_INFO)
|
||||
uni.setStorageSync('isLogin', true)
|
||||
showToast('登录成功')
|
||||
setTimeout(() => {
|
||||
uni.switchTab({ url: '/pages/memberInfo/memberInfo' })
|
||||
}, 1500)
|
||||
isLoading.value = false
|
||||
}, 500)
|
||||
}
|
||||
|
||||
function triggerAgreementShake() {
|
||||
shakeAgreement.value = true
|
||||
showToast('请先同意用户协议和隐私政策')
|
||||
setTimeout(() => {
|
||||
shakeAgreement.value = false
|
||||
}, 500)
|
||||
}
|
||||
|
||||
async function handleSendCode() {
|
||||
if (countdown.value > 0) {
|
||||
showToast('验证码在有效期内,请勿重复发送')
|
||||
return
|
||||
}
|
||||
|
||||
if (!validateSmsPhone()) return
|
||||
|
||||
isLoading.value = true
|
||||
|
||||
setTimeout(() => {
|
||||
countdown.value = 300
|
||||
startCountdown()
|
||||
showToast('验证码已发送')
|
||||
isLoading.value = false
|
||||
}, 300)
|
||||
}
|
||||
|
||||
function startCountdown() {
|
||||
if (timer) clearInterval(timer)
|
||||
timer = setInterval(() => {
|
||||
countdown.value--
|
||||
if (countdown.value <= 0) {
|
||||
clearInterval(timer)
|
||||
timer = null
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
async function handleSmsLogin() {
|
||||
if (!agreed.value) {
|
||||
triggerAgreementShake()
|
||||
return
|
||||
}
|
||||
|
||||
if (!validateSmsPhone()) return
|
||||
if (!code.value) {
|
||||
showToast('请输入验证码')
|
||||
return
|
||||
}
|
||||
if (!/^\d{6}$/.test(code.value)) {
|
||||
showToast('请输入6位验证码')
|
||||
return
|
||||
}
|
||||
|
||||
isLoading.value = true
|
||||
|
||||
setTimeout(() => {
|
||||
setToken(TEST_TOKEN)
|
||||
uni.setStorageSync('memberInfo', TEST_MEMBER_INFO)
|
||||
showToast('登录成功')
|
||||
setTimeout(() => {
|
||||
uni.switchTab({ url: '/pages/memberInfo/memberInfo' })
|
||||
}, 1500)
|
||||
isLoading.value = false
|
||||
}, 500)
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timer) clearInterval(timer)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// 自定义Toast弹窗样式
|
||||
.custom-toast {
|
||||
position: fixed;
|
||||
top: -100rpx;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
border-radius: $radius-lg;
|
||||
padding: 24rpx 48rpx;
|
||||
z-index: 9999;
|
||||
opacity: 0;
|
||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
&.show {
|
||||
top: 120rpx;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.toast-text {
|
||||
color: $text-inverse;
|
||||
font-size: 28rpx;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
button::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.login-page {
|
||||
min-height: 100vh;
|
||||
background: $bg-gradient-primary;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 48rpx;
|
||||
}
|
||||
|
||||
.login-header {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-bottom: 80rpx;
|
||||
}
|
||||
|
||||
.logo-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.logo-image {
|
||||
width: 140rpx;
|
||||
height: 140rpx;
|
||||
border-radius: $radius-lg;
|
||||
box-shadow: $shadow-lg;
|
||||
}
|
||||
|
||||
.app-name {
|
||||
margin-top: 24rpx;
|
||||
font-size: 42rpx;
|
||||
font-weight: $font-weight-extrabold;
|
||||
color: $primary-dark;
|
||||
letter-spacing: 4rpx;
|
||||
}
|
||||
|
||||
.app-slogan {
|
||||
margin-top: 12rpx;
|
||||
font-size: 26rpx;
|
||||
color: $text-light;
|
||||
}
|
||||
|
||||
.card-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 620rpx;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
position: absolute;
|
||||
height: 700rpx;
|
||||
background: $bg-white;
|
||||
border-radius: $radius-lg;
|
||||
padding: 48rpx 40rpx;
|
||||
transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
will-change: transform, z-index, filter;
|
||||
}
|
||||
|
||||
.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;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
.phone-card {
|
||||
z-index: 10;
|
||||
top: 0;
|
||||
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;
|
||||
left: -100rpx;
|
||||
box-shadow: 0 16rpx 40rpx rgba(0, 0, 0, 0.12);
|
||||
filter: blur(10rpx);
|
||||
}
|
||||
}
|
||||
|
||||
.card-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.phone-display {
|
||||
font-size: 48rpx;
|
||||
font-weight: $font-weight-bold;
|
||||
color: $text-dark;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.auth-tip {
|
||||
font-size: 24rpx;
|
||||
color: $text-light;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
width: 100%;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.phone-login{
|
||||
margin-bottom: 150rpx;
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: transparent;
|
||||
padding: 0 32rpx;
|
||||
height: 100rpx;
|
||||
border-bottom: 2rpx solid $border-light;
|
||||
|
||||
&.code-input {
|
||||
padding-right: 16rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.phone-input {
|
||||
flex: 1;
|
||||
font-size: 34rpx;
|
||||
color: $text-dark;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
width: 100%;
|
||||
height: 100rpx;
|
||||
border-radius: $radius-md;
|
||||
border: none;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&.primary {
|
||||
background: $gradient-orange;
|
||||
color: $text-inverse;
|
||||
font-size: 34rpx;
|
||||
font-weight: $font-weight-bold;
|
||||
box-shadow: $shadow-orange-glow;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
background: $border-light;
|
||||
color: $text-light;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&.loading {
|
||||
background: $accent-orange;
|
||||
}
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
background: #F8F9FA;
|
||||
color: $text-dark;
|
||||
font-size: 30rpx;
|
||||
font-weight: $font-weight-medium;
|
||||
margin-top: 24rpx;
|
||||
|
||||
&:active {
|
||||
background: #E9ECEF;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.agreement-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
font-size: 24rpx;
|
||||
color: $text-light;
|
||||
margin-top: 36rpx;
|
||||
|
||||
.link {
|
||||
color: $accent-orange;
|
||||
margin: 0 6rpx;
|
||||
}
|
||||
|
||||
&.shake {
|
||||
animation: shake 0.5s ease-in-out;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
10%, 30%, 50%, 70%, 90% { transform: translateX(-8rpx); }
|
||||
20%, 40%, 60%, 80% { transform: translateX(8rpx); }
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user