943 lines
27 KiB
Vue
943 lines
27 KiB
Vue
<template>
|
||
<view class="login-page">
|
||
<!-- 自定义Toast弹窗 -->
|
||
<view class="custom-toast" :class="{ show: toastShow }">
|
||
<text class="toast-text">{{ toastMessage }}</text>
|
||
</view>
|
||
|
||
<!-- ==================== 微信小程序登录 ==================== -->
|
||
<template v-if="isWechatMiniProgram">
|
||
<view class="login-header" :style="{ paddingTop: statusBarHeightPx }">
|
||
<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="wechat-card">
|
||
<view class="card-body wechat-login-body">
|
||
<text class="phone-display">微信一键登录</text>
|
||
<text class="auth-tip">授权获取您的微信信息</text>
|
||
|
||
<!-- 获取微信头像昵称 -->
|
||
<button
|
||
v-if="canUseGetUserProfile"
|
||
class="login-btn wechat-btn"
|
||
hover-class="wechat-btn-hover"
|
||
@tap="handleGetUserProfile"
|
||
:disabled="isLoading"
|
||
>
|
||
<text>{{ isLoading ? '登录中...' : '微信一键登录' }}</text>
|
||
</button>
|
||
<!-- 低版本兼容 -->
|
||
<button
|
||
v-else
|
||
class="login-btn wechat-btn"
|
||
hover-class="wechat-btn-hover"
|
||
open-type="getUserInfo"
|
||
@getuserinfo="handleGetUserInfo"
|
||
:disabled="isLoading"
|
||
>
|
||
<text>{{ isLoading ? '登录中...' : '微信一键登录' }}</text>
|
||
</button>
|
||
|
||
<view class="agreement-text" :class="{ shake: shakeAgreement }">
|
||
<checkbox-group @change="onAgreementChange">
|
||
<checkbox :checked="agreed" value="agreed" color="#07C160" />
|
||
</checkbox-group>
|
||
<text>登录即表示同意</text>
|
||
<text class="link" @tap="showAgreement">《用户协议》</text>
|
||
<text>和</text>
|
||
<text class="link" @tap="showPrivacy">《隐私政策》</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<!-- ==================== App/其他平台登录(原有) ==================== -->
|
||
<template v-else>
|
||
<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 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 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: 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>
|
||
|
||
<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>
|
||
|
||
<button class="login-btn primary" :class="{ disabled: isLoading, loading: isLoading }" @tap="handlePhoneLogin" :disabled="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>
|
||
</template>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, onMounted, onUnmounted } from 'vue'
|
||
import { setToken, setRefreshToken } from '@/utils/request.js'
|
||
import { loginWithPhone, oneClickLogin, sendCode, login } from '@/api/main.js'
|
||
import VerifyCodeInput from '@/components/global/VerifyCodeInput.vue'
|
||
|
||
// ===== 平台检测 =====
|
||
const isWechatMiniProgram = ref(false)
|
||
const statusBarHeightPx = ref('0px')
|
||
|
||
onMounted(() => {
|
||
// 获取状态栏高度,为刘海屏留出安全空间
|
||
const sysInfo = uni.getSystemInfoSync()
|
||
statusBarHeightPx.value = (sysInfo.statusBarHeight || 0) + 'px'
|
||
|
||
// 检测当前是否为微信小程序环境
|
||
// #ifdef MP-WEIXIN
|
||
isWechatMiniProgram.value = true
|
||
// #endif
|
||
|
||
if (!isWechatMiniProgram.value) {
|
||
// 非微信小程序,保持原有默认卡片
|
||
activeCard.value = 'phone'
|
||
}
|
||
})
|
||
|
||
const activeCard = ref('phone')
|
||
const isLoading = ref(false)
|
||
const agreed = ref(true)
|
||
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)
|
||
}
|
||
|
||
/**
|
||
* 保存登录信息到本地存储
|
||
*/
|
||
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')
|
||
}
|
||
|
||
function switchToSms() {
|
||
activeCard.value = 'sms'
|
||
}
|
||
|
||
function switchToPhone() {
|
||
activeCard.value = 'phone'
|
||
}
|
||
|
||
function showAgreement() {
|
||
uni.showModal({ title: '用户协议', content: '这里是用户协议内容...', showCancel: false })
|
||
}
|
||
|
||
function showPrivacy() {
|
||
uni.showModal({ title: '隐私政策', content: '这里是隐私政策内容...', showCancel: false })
|
||
}
|
||
|
||
function handleCodeComplete(val) {
|
||
if (!isLoading.value) {
|
||
setTimeout(() => {
|
||
handleSmsLogin()
|
||
}, 100)
|
||
}
|
||
}
|
||
|
||
function onInputFocus() {
|
||
isCodeFocused.value = true
|
||
}
|
||
|
||
function onInputBlur() {
|
||
isCodeFocused.value = false
|
||
}
|
||
|
||
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
|
||
}
|
||
|
||
isLoading.value = true
|
||
|
||
try {
|
||
await new Promise((resolve, reject) => {
|
||
uni.preLogin({
|
||
provider: 'univerify',
|
||
success: resolve,
|
||
fail: (err) => {
|
||
console.warn('预登录失败,切换到验证码登录:', err)
|
||
reject(err)
|
||
}
|
||
})
|
||
})
|
||
|
||
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('一键登录失败,请使用验证码登录')
|
||
isLoading.value = false
|
||
switchToSms()
|
||
}
|
||
}
|
||
|
||
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
|
||
|
||
try {
|
||
const res = await sendCode({ phone: smsPhone.value })
|
||
|
||
if (res && res.success) {
|
||
countdown.value = 60
|
||
startCountdown()
|
||
showToast('验证码已发送')
|
||
} else {
|
||
showToast(res?.message || '验证码发送失败')
|
||
}
|
||
} catch (err) {
|
||
console.error('发送验证码失败:', err)
|
||
showToast(err?.message || '验证码发送失败')
|
||
} finally {
|
||
isLoading.value = false
|
||
}
|
||
}
|
||
|
||
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.length !== 4) {
|
||
showToast('请输入4位验证码')
|
||
return
|
||
}
|
||
|
||
isLoading.value = true
|
||
|
||
try {
|
||
const res = await loginWithPhone({
|
||
phone: smsPhone.value,
|
||
code: code.value
|
||
})
|
||
|
||
if (res && res.accessToken) {
|
||
setToken(res.accessToken)
|
||
if (res.refreshToken) {
|
||
setRefreshToken(res.refreshToken)
|
||
}
|
||
saveLoginInfo(res)
|
||
showToast('登录成功')
|
||
setTimeout(() => {
|
||
uni.switchTab({ url: '/pages/memberInfo/memberInfo' })
|
||
}, 1500)
|
||
} else {
|
||
showToast('登录失败,请重试')
|
||
}
|
||
} catch (err) {
|
||
console.error('验证码登录失败:', err)
|
||
showToast(err?.message || '登录失败,请重试')
|
||
} finally {
|
||
isLoading.value = false
|
||
}
|
||
}
|
||
|
||
// ===== 微信小程序登录 =====
|
||
const canUseGetUserProfile = ref(false)
|
||
|
||
// 检查是否支持 getUserProfile(基础库 2.10.4+)
|
||
onMounted(() => {
|
||
// #ifdef MP-WEIXIN
|
||
if (wx.getUserProfile) {
|
||
canUseGetUserProfile.value = true
|
||
}
|
||
// #endif
|
||
})
|
||
|
||
/**
|
||
* 微信小程序登录(新版:getUserProfile)
|
||
*/
|
||
function handleGetUserProfile() {
|
||
if (!agreed.value) {
|
||
triggerAgreementShake()
|
||
return
|
||
}
|
||
|
||
isLoading.value = true
|
||
|
||
// #ifdef MP-WEIXIN
|
||
wx.getUserProfile({
|
||
desc: '用于完善会员资料',
|
||
success: (profileRes) => {
|
||
const userInfo = profileRes.userInfo
|
||
console.log('[微信登录] 获取用户信息成功:', userInfo)
|
||
// 执行微信登录
|
||
performWechatLogin(userInfo)
|
||
},
|
||
fail: (err) => {
|
||
console.error('[微信登录] 获取用户信息失败:', err)
|
||
isLoading.value = false
|
||
// 用户拒绝授权时,仍然尝试静默登录
|
||
performWechatLogin(null)
|
||
}
|
||
})
|
||
// #endif
|
||
}
|
||
|
||
/**
|
||
* 微信小程序登录(旧版:getUserInfo)
|
||
*/
|
||
function handleGetUserInfo(e) {
|
||
if (!agreed.value) {
|
||
triggerAgreementShake()
|
||
return
|
||
}
|
||
|
||
isLoading.value = true
|
||
|
||
const userInfo = e.detail.userInfo
|
||
if (userInfo) {
|
||
console.log('[微信登录] 获取用户信息成功:', userInfo)
|
||
}
|
||
performWechatLogin(userInfo)
|
||
}
|
||
|
||
/**
|
||
* 执行微信登录:调用 uni.login 获取 code,发送到后端
|
||
*/
|
||
function performWechatLogin(userInfo) {
|
||
// #ifdef MP-WEIXIN
|
||
uni.login({
|
||
provider: 'weixin',
|
||
success: async (loginRes) => {
|
||
const code = loginRes.code
|
||
console.log('[微信登录] 获取 code 成功:', code)
|
||
|
||
if (!code) {
|
||
showToast('微信登录失败,请重试')
|
||
isLoading.value = false
|
||
return
|
||
}
|
||
|
||
try {
|
||
const res = await login({
|
||
code: code,
|
||
nickname: userInfo?.nickName || '',
|
||
avatar: userInfo?.avatarUrl || ''
|
||
})
|
||
|
||
console.log('[微信登录] 后端返回:', res)
|
||
|
||
if (res && res.accessToken) {
|
||
setToken(res.accessToken)
|
||
if (res.refreshToken) {
|
||
setRefreshToken(res.refreshToken)
|
||
}
|
||
saveLoginInfo(res)
|
||
showToast('登录成功')
|
||
setTimeout(() => {
|
||
uni.switchTab({ url: '/pages/memberInfo/memberInfo' })
|
||
}, 1500)
|
||
} else {
|
||
showToast('登录失败,请重试')
|
||
}
|
||
} catch (e) {
|
||
console.error('[微信登录] 后端调用失败:', e)
|
||
showToast(e?.message || '登录失败,请重试')
|
||
} finally {
|
||
isLoading.value = false
|
||
}
|
||
},
|
||
fail: (err) => {
|
||
console.error('[微信登录] uni.login 失败:', err)
|
||
showToast('微信登录失败,请重试')
|
||
isLoading.value = false
|
||
}
|
||
})
|
||
// #endif
|
||
}
|
||
|
||
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;
|
||
box-sizing: border-box;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.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: 500rpx;
|
||
}
|
||
|
||
/* ===== 微信小程序登录卡片 ===== */
|
||
.wechat-card {
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
background: $bg-white;
|
||
border-radius: $radius-lg;
|
||
box-shadow: 0 24rpx 60rpx rgba(0, 0, 0, 0.18), 0 8rpx 20rpx rgba(0, 0, 0, 0.1);
|
||
padding: 48rpx 40rpx;
|
||
}
|
||
|
||
.wechat-login-body {
|
||
min-height: 400rpx;
|
||
justify-content: flex-start;
|
||
gap: 24rpx;
|
||
}
|
||
|
||
.wechat-btn {
|
||
width: 100%;
|
||
height: 100rpx;
|
||
border-radius: $radius-md;
|
||
border: none;
|
||
background: #07C160;
|
||
color: #ffffff;
|
||
font-size: 34rpx;
|
||
font-weight: $font-weight-bold;
|
||
box-shadow: 0 8rpx 24rpx rgba(7, 193, 96, 0.3);
|
||
margin-top: 24rpx;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
|
||
&:active {
|
||
transform: scale(0.98);
|
||
opacity: 0.9;
|
||
}
|
||
|
||
&[disabled] {
|
||
background: #A0E0B0;
|
||
box-shadow: none;
|
||
}
|
||
}
|
||
|
||
.wechat-btn-hover {
|
||
opacity: 0.85;
|
||
transform: scale(0.98);
|
||
}
|
||
|
||
.login-card {
|
||
position: absolute;
|
||
height: 620rpx;
|
||
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;
|
||
}
|
||
|
||
.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;
|
||
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;
|
||
position: relative;
|
||
}
|
||
|
||
.phone-form-group {
|
||
height: 100rpx;
|
||
width: 360rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.underline-input {
|
||
width: 100%;
|
||
height: 100%;
|
||
text-align: center;
|
||
font-size: 32rpx;
|
||
color: $text-dark;
|
||
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;
|
||
}
|
||
}
|
||
|
||
.code-form-group {
|
||
height: 100rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.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> |