1087 lines
29 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="page-login">
<!-- 背景装饰 -->
<view class="bg-decoration"></view>
<view class="content-wrap">
<!-- Logo区域 -->
<view class="logo-section">
<view class="logo-wrapper">
<image class="logo-img" src="/static/logo.png" mode="aspectFit"></image>
</view>
<text class="app-name">柯大鸭</text>
<text class="app-slogan">真的盲盒 · 拒绝套路</text>
</view>
<!-- 登录卡片 -->
<view class="login-card glass-card">
<!-- 切换Tab -->
<view class="tab-bar">
<!-- #ifdef MP-WEIXIN -->
<view
class="tab-item"
:class="{ active: loginMode === 'wechat' }"
@tap="switchMode('wechat')"
>
<text class="tab-text">微信快捷登录</text>
</view>
<!-- #endif -->
<!-- #ifdef MP-TOUTIAO -->
<view
class="tab-item"
:class="{ active: loginMode === 'toutiao' }"
@tap="switchMode('toutiao')"
>
<text class="tab-text">抖音用户信息授权登录</text>
</view>
<!-- #endif -->
<view
class="tab-item"
:class="{ active: loginMode === 'sms' }"
@tap="switchMode('sms')"
>
<text class="tab-text">验证码登录</text>
</view>
<view class="tab-indicator" :class="{ right: loginMode === 'sms' }"></view>
</view>
<!-- 内容区域 -->
<view class="content-area">
<!-- 微信登录 -->
<!-- #ifdef MP-WEIXIN -->
<view v-if="loginMode === 'wechat'" class="login-panel wechat-panel">
<view class="panel-icon-wrap">
<view class="panel-icon wechat-icon">
<text class="icon-emoji">💬</text>
</view>
</view>
<text class="panel-title">一键获取手机号</text>
<text class="panel-desc">授权获取本机手机号安全快速登录</text>
<button
class="btn-primary btn-login"
open-type="getPhoneNumber"
:disabled="loading"
@getphonenumber="onGetPhoneNumber"
>
{{ loading ? '获取中...' : '一键获取手机号' }}
</button>
</view>
<!-- #endif -->
<!-- 抖音登录 -->
<!-- #ifdef MP-TOUTIAO -->
<view v-if="loginMode === 'toutiao'" class="login-panel toutiao-panel">
<view class="panel-icon-wrap">
<view class="panel-icon toutiao-icon">
<text class="icon-emoji">🎵</text>
</view>
</view>
<text class="panel-title">抖音快捷登录</text>
<text class="panel-desc">使用抖音账号快速登录</text>
<!-- 抖音官方授权登录按钮抖音会自动弹出授权提示 -->
<button
class="btn-primary btn-login"
:disabled="loading"
open-type="getUserInfo"
@getuserinfo="onGetUserInfo"
>
{{ loading ? '登录中...' : '抖音用户授权登录' }}
</button>
</view>
<!-- #endif -->
<!-- 短信登录 -->
<view v-else class="login-panel sms-panel">
<text class="panel-title">手机号验证码登录</text>
<text class="panel-desc">输入手机号获取验证码完成登录</text>
<view class="form-group">
<!-- 手机号 -->
<view class="input-field">
<view class="field-prefix">
<text class="prefix-text">+86</text>
</view>
<input
type="number"
v-model="mobile"
placeholder="请输入手机号"
class="field-input"
maxlength="11"
/>
<text v-if="mobile" class="clear-btn" @tap="mobile = ''"></text>
</view>
<!-- 验证码 -->
<view class="input-field">
<view class="field-prefix">
<text class="prefix-icon">🔐</text>
</view>
<input
type="number"
v-model="smsCode"
placeholder="请输入验证码"
class="field-input"
maxlength="6"
/>
<view
class="send-code-btn"
:class="{ disabled: !canSendCode || sendingCode || countdown > 0 }"
@tap="handleSendCode"
>
<text>{{ sendingCode ? '发送中' : (countdown > 0 ? `${countdown}s` : '获取验证码') }}</text>
</view>
</view>
</view>
<button
class="btn-primary btn-login"
:class="{ disabled: !canLogin }"
:disabled="loading || !canLogin"
@tap="handleSmsLogin"
>
{{ loading ? '登录中...' : '登录 / 注册' }}
</button>
<text class="hint-text">未注册的手机号将自动创建账号</text>
</view>
</view>
<!-- 协议区 -->
<view class="agreement-section">
<view class="agreement-row" @tap="toggleAgreement">
<view class="checkbox" :class="{ checked: agreementChecked }">
<text v-if="agreementChecked" class="check-mark"></text>
</view>
<text class="agreement-text">
我已阅读并同意
<text class="link" @tap.stop="toUserAgreement">用户协议</text>
<text class="link" @tap.stop="toPurchaseAgreement">隐私政策</text>
</text>
</view>
</view>
</view>
<!-- 底部信息 -->
<view class="footer">
<text class="copyright">© 2025 柯大鸭潮玩科技</text>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { request } from '../../utils/request'
import { wechatLogin, douyinLogin, bindPhone, bindDouyinPhone, getUserStats, getPointsBalance, sendSmsCode, smsLogin, getUserInfo } from '../../api/appUser'
import { authRequest } from '../../utils/request'
import { vibrateShort } from '@/utils/vibrate.js'
const loading = ref(false)
const agreementChecked = ref(false)
// 登录模式:根据平台设置默认值
// #ifdef MP-WEIXIN
const loginMode = ref('wechat')
// #endif
// #ifdef MP-TOUTIAO
const loginMode = ref('toutiao')
// #endif
// #ifndef MP-WEIXIN || MP-TOUTIAO
const loginMode = ref('sms')
// #endif
// 静默获取 OpenID
async function ensureOpenID() {
const current = uni.getStorageSync('openid')
if (current) return
console.log('[DEBUG] 本地缺少 openid, 尝试静默获取...')
// #ifdef MP-WEIXIN
uni.login({
provider: 'weixin',
success: async (loginRes) => {
try {
const res = await request({
url: '/api/app/common/openid',
method: 'POST',
data: { code: loginRes.code }
})
if (res && res.openid) {
console.log('[DEBUG] 静默获取 openid 成功:', res.openid)
uni.setStorageSync('openid', res.openid)
}
} catch (err) {
console.error('[DEBUG] 静默获取 openid 失败:', err)
}
}
})
// #endif
// #ifdef MP-TOUTIAO
// 抖音小程序使用 tt.login
tt.login({
success: async (loginRes) => {
try {
console.log('[DEBUG] 抖音登录成功code:', loginRes.code)
// 保存 code 用于后续登录
uni.setStorageSync('douyin_login_code', loginRes.code)
// 尝试获取 openid
const res = await request({
url: '/api/app/common/openid',
method: 'POST',
data: { code: loginRes.code, platform: 'douyin' }
})
if (res && res.openid) {
console.log('[DEBUG] 静默获取 openid 成功:', res.openid)
uni.setStorageSync('openid', res.openid)
}
} catch (err) {
console.error('[DEBUG] 静默获取 openid 失败:', err)
}
},
fail: (err) => {
console.error('[DEBUG] 抖音登录失败:', err)
}
})
// #endif
}
onLoad(() => {
ensureOpenID()
})
const fetchExtraDataLoading = ref(false)
// 短信登录
const mobile = ref('')
const smsCode = ref('')
const countdown = ref(0)
const sendingCode = ref(false)
let countdownTimer = null
// 计算属性
const canSendCode = computed(() => {
return mobile.value.length === 11 && /^1[3-9]\d{9}$/.test(mobile.value) && agreementChecked.value
})
const canLogin = computed(() => {
return mobile.value.length === 11 && smsCode.value.length >= 4 && agreementChecked.value
})
onMounted(() => {
try {
const savedMobile = uni.getStorageSync('last_login_mobile')
if (savedMobile) mobile.value = savedMobile
} catch (e) {}
})
onUnmounted(() => {
if (countdownTimer) clearInterval(countdownTimer)
})
function switchMode(mode) {
loginMode.value = mode
}
function toggleAgreement() {
agreementChecked.value = !agreementChecked.value
}
function toUserAgreement() {
uni.navigateTo({ url: '/pages-user/agreement/user' })
}
function toPurchaseAgreement() {
uni.navigateTo({ url: '/pages-user/agreement/purchase' })
}
// 发送验证码
async function handleSendCode() {
if (!agreementChecked.value) {
uni.showToast({ title: '请先同意用户协议', icon: 'none' })
vibrateShort()
return
}
if (countdown.value > 0 || sendingCode.value) return
if (!mobile.value || mobile.value.length !== 11) {
uni.showToast({ title: '请输入正确的手机号', icon: 'none' })
return
}
if (!/^1[3-9]\d{9}$/.test(mobile.value)) {
uni.showToast({ title: '手机号格式不正确', icon: 'none' })
return
}
sendingCode.value = true
try {
await sendSmsCode(mobile.value)
uni.showToast({ title: '验证码已发送', icon: 'success' })
countdown.value = 60
countdownTimer = setInterval(() => {
countdown.value--
if (countdown.value <= 0) {
clearInterval(countdownTimer)
countdownTimer = null
}
}, 1000)
} catch (err) {
uni.showToast({ title: err.message || '发送失败', icon: 'none' })
} finally {
sendingCode.value = false
}
}
// 短信登录
async function handleSmsLogin() {
if (!agreementChecked.value) {
uni.showToast({ title: '请先同意用户协议', icon: 'none' })
vibrateShort()
return
}
if (!canLogin.value) return
loading.value = true
try {
const inviterCode = uni.getStorageSync('inviter_code')
const data = await smsLogin(mobile.value, smsCode.value, inviterCode)
console.log('[DEBUG] 短信登录响应原始数据:', data)
saveUserData(data)
// 短信登录也标记为手机号登录
uni.setStorageSync('login_method', 'sms')
const isNew = data && data.is_new_user
uni.showToast({
title: isNew ? '🎉 注册成功!' : '✨ 登录成功!',
icon: 'none',
duration: 1200
})
// 后台获取数据
if (data && data.user_id) {
fetchExtraData(data.user_id)
}
setTimeout(() => uni.reLaunch({ url: '/pages/mine/index' }), 600)
} catch (err) {
uni.showToast({ title: err.message || '登录失败', icon: 'none' })
} finally {
loading.value = false
}
}
// 微信登录
async function onGetPhoneNumber(e) {
if (!agreementChecked.value) {
uni.showToast({ title: '请先同意用户协议', icon: 'none' })
vibrateShort()
return
}
const phoneCode = e.detail.code
if (!phoneCode) {
uni.showToast({ title: '需要授权手机号', icon: 'none' })
return
}
loading.value = true
try {
// 1. 检查是否已登录且未过期
const token = uni.getStorageSync('token')
const phoneNumber = uni.getStorageSync('phone_number')
console.log('[DEBUG] 微信登录 - 检查登录状态:', {
hasToken: !!token,
hasPhoneNumber: !!phoneNumber,
phoneNumber: phoneNumber || '未找到'
})
let userId = null
// 2. 如果已有 token 和手机号,说明已登录且已绑定手机号
if (token && phoneNumber) {
console.log('[DEBUG] 已登录且已绑定手机号,跳过登录流程')
userId = uni.getStorageSync('user_id')
// 跳转到首页
uni.showToast({ title: '✨ 已登录!', icon: 'none', duration: 1200 })
setTimeout(() => uni.reLaunch({ url: '/pages/mine/index' }), 600)
loading.value = false
return
}
// 3. 如果有 token 但没有手机号,说明已登录但未绑定手机号
if (token && !phoneNumber) {
console.log('[DEBUG] 已登录但未绑定手机号,开始绑定')
userId = uni.getStorageSync('user_id')
try {
const bindRes = await bindPhone(userId, phoneCode)
console.log('[DEBUG] 绑定手机号成功:', bindRes)
// 从绑定响应中获取手机号并缓存
const boundPhone = bindRes.phone || bindRes.phone_number || bindRes.mobile
if (boundPhone) {
uni.setStorageSync('phone_number', boundPhone)
console.log('[DEBUG] 已缓存手机号到 phone_number:', boundPhone)
}
// 更新用户信息
try {
const updatedUserInfo = await getUserProfile() // 使用 api/appUser.js 中的函数
if (updatedUserInfo) {
uni.setStorageSync('user_info', updatedUserInfo)
}
} catch (err) {
console.warn('[DEBUG] 绑定后获取用户信息失败:', err)
}
// 后台获取数据
fetchExtraData(userId)
uni.showToast({ title: '✨ 绑定成功!', icon: 'none', duration: 1200 })
setTimeout(() => uni.reLaunch({ url: '/pages/mine/index' }), 600)
} catch (bindErr) {
console.error('[DEBUG] 绑定手机号失败:', bindErr)
uni.showToast({ title: bindErr.message || '绑定失败', icon: 'none' })
}
loading.value = false
return
}
// 4. 如果没有 token说明未登录或已过期执行完整的登录流程
console.log('[DEBUG] 未登录或已过期,执行微信登录')
uni.login({
provider: 'weixin',
success: async (res) => {
try {
const inviterCode = uni.getStorageSync('inviter_code')
const data = await wechatLogin(res.code, inviterCode)
console.log('[DEBUG] 微信登录成功:', data)
// 保存基本用户信息
saveUserData(data)
userId = data.user_id
// 检查是否需要绑定手机号
const hasPhone = data.mobile || data.phone || data.phone_number
if (!hasPhone) {
// 5. 如果登录返回的数据中没有手机号,调用绑定接口
console.log('[DEBUG] 登录未返回手机号,开始绑定')
try {
const bindRes = await bindPhone(userId, phoneCode)
console.log('[DEBUG] 绑定手机号成功:', bindRes)
// 从绑定响应中获取手机号并缓存
const boundPhone = bindRes.phone || bindRes.phone_number || bindRes.mobile
if (boundPhone) {
uni.setStorageSync('phone_number', boundPhone)
console.log('[DEBUG] 已缓存手机号到 phone_number:', boundPhone)
}
// 更新用户信息
try {
const updatedUserInfo = await getUserProfile()
if (updatedUserInfo) {
Object.assign(data, updatedUserInfo)
uni.setStorageSync('user_info', updatedUserInfo)
}
} catch (err) {
console.warn('[DEBUG] 绑定后获取用户信息失败:', err)
}
} catch (bindErr) {
console.error('[DEBUG] 绑定手机号失败:', bindErr)
}
} else {
// 6. 如果登录已返回手机号,直接缓存
const phoneToCache = data.mobile || data.phone || data.phone_number
uni.setStorageSync('phone_number', phoneToCache)
console.log('[DEBUG] 登录已返回手机号,已缓存:', phoneToCache)
}
// 后台获取数据
fetchExtraData(userId)
uni.showToast({ title: '✨ 登录成功!', icon: 'none', duration: 1200 })
setTimeout(() => uni.reLaunch({ url: '/pages/mine/index' }), 600)
} catch (err) {
console.error('[DEBUG] 微信登录失败:', err)
uni.showToast({ title: err.message || '登录失败', icon: 'none' })
} finally {
loading.value = false
}
},
fail: () => {
loading.value = false
uni.showToast({ title: '微信登录失败', icon: 'none' })
}
})
} catch (err) {
console.error('[DEBUG] 微信登录流程错误:', err)
uni.showToast({ title: err.message || '登录失败', icon: 'none' })
loading.value = false
}
}
// 抖音官方授权登录处理函数(响应 open-type="getUserInfo"
async function onGetUserInfo(e) {
console.log('[DEBUG] 抖音 getUserInfo 回调触发:', e)
// 检查用户是否同意授权
if (!e.detail.userInfo) {
console.log('[DEBUG] 用户拒绝授权')
uni.showToast({ title: '您取消了授权', icon: 'none' })
return
}
if (!agreementChecked.value) {
uni.showToast({ title: '请先同意用户协议', icon: 'none' })
vibrateShort()
return
}
loading.value = true
try {
// 1. 先调用 tt.login 获取登录凭证 code
console.log('[DEBUG] 开始 tt.login 获取登录凭证...')
const loginRes = await new Promise((resolve, reject) => {
tt.login({
success: resolve,
fail: reject
})
})
console.log('[DEBUG] tt.login 成功code:', loginRes.code)
// 2. 获取用户信息(从 e.detail.userInfo 中获取)
const userInfo = e.detail.userInfo
console.log('[DEBUG] 用户信息:', userInfo)
const { nickName, avatarUrl } = userInfo
// 3. 调用后端登录接口
console.log('[DEBUG] 调用后端 douyinLogin 接口...')
const inviterCode = uni.getStorageSync('inviter_code')
const loginData = await douyinLogin(loginRes.code, null, inviterCode)
console.log('[DEBUG] 抖音登录成功:', loginData)
// 保存用户基本信息(昵称和头像)
if (nickName) {
uni.setStorageSync('nickname', nickName)
}
if (avatarUrl) {
uni.setStorageSync('avatar', avatarUrl)
}
// 保存登录数据saveUserData 会自动检查手机号绑定状态)
saveUserData(loginData)
// 后台获取数据(仅在已绑定手机号时)
const hasPhone = loginData.mobile || loginData.phone || loginData.phone_number
if (hasPhone) {
fetchExtraData(loginData.user_id)
uni.showToast({ title: '✨ 登录成功!', icon: 'none', duration: 1200 })
setTimeout(() => uni.reLaunch({ url: '/pages/mine/index' }), 600)
}
// 如果未绑定手机号saveUserData 会自动切换到短信登录tab
} catch (err) {
console.error('[DEBUG] 抖音登录失败:', err)
uni.showToast({
title: err.message || '登录失败,请重试',
icon: 'none',
duration: 2000
})
} finally {
loading.value = false
}
}
// 抖音登录函数(已弃用,保留以防兼容性问题)
async function handleDouyinLogin() {
console.log('[DEBUG] handleDouyinLogin 已弃用,请使用 onGetUserInfo')
}
function saveUserData(data) {
if (!data) return
console.log('[DEBUG] 准备执行 saveUserData, payload:', data)
uni.setStorageSync('user_info', data)
if (data.token) uni.setStorageSync('token', data.token)
if (data.user_id) {
uni.setStorageSync('user_id', data.user_id)
// 标记登录方式:微信手机号登录已经绑定手机号
uni.setStorageSync('login_method', 'wechat_phone')
}
if (data.avatar) uni.setStorageSync('avatar', data.avatar)
if (data.nickname) uni.setStorageSync('nickname', data.nickname)
if (data.invite_code) uni.setStorageSync('invite_code', data.invite_code)
if (data.mobile) uni.setStorageSync('last_login_mobile', data.mobile)
// 检查是否已绑定手机号(检查所有可能的字段名)
const hasPhone = data.mobile || data.phone || data.phone_number
console.log('[DEBUG] 检查手机号绑定状态:', {
mobile: data.mobile,
phone: data.phone,
phone_number: data.phone_number,
hasPhone: hasPhone ? '已绑定' : '未绑定'
})
// 如果有手机号,缓存到 phone_number
if (hasPhone) {
const phoneToCache = data.mobile || data.phone || data.phone_number
uni.setStorageSync('phone_number', phoneToCache)
console.log('[DEBUG] 已缓存手机号到 phone_number:', phoneToCache)
}
// 如果返回了 openid则存储 (短信登录现在也会返回已关联的 openid)
const openid = data.openid || data.open_id
if (openid) {
console.log('[DEBUG] 存储从登录接口获取的 openid:', openid)
uni.setStorageSync('openid', openid)
} else {
console.warn('[DEBUG] 登录接口未返回 openid, 请检查后端或联系管理员')
}
// 如果未绑定手机号切换到短信登录tab进行绑定
if (!hasPhone) {
console.log('[DEBUG] 未检测到手机号切换到短信登录tab')
uni.showModal({
title: '绑定手机号',
content: '登录成功!为了账号安全,请绑定手机号',
showCancel: false,
confirmText: '去绑定',
success: () => {
// 切换到短信登录tab
loginMode.value = 'sms'
}
})
} else {
console.log('[DEBUG] 已检测到手机号,正常登录流程')
}
}
function fetchExtraData(userId) {
Promise.all([
getUserStats(userId).then(s => s && uni.setStorageSync('user_stats', s)).catch(() => {}),
getPointsBalance(userId).then(b => {
const val = b && b.balance !== undefined ? b.balance : b
uni.setStorageSync('points_balance', val)
}).catch(() => {})
])
}
</script>
<style lang="scss" scoped>
.page-login {
min-height: 100vh;
background: $bg-page;
position: relative;
}
.content-wrap {
position: relative;
z-index: 10;
padding: 80rpx 32rpx 40rpx;
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* Logo区域 */
.logo-section {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 48rpx;
}
.logo-wrapper {
width: 160rpx;
height: 160rpx;
background: $bg-card;
border-radius: 40rpx;
display: flex;
align-items: center;
justify-content: center;
box-shadow: $shadow-card;
margin-bottom: 24rpx;
border: 2rpx solid rgba($brand-primary, 0.08);
.logo-img {
width: 120rpx;
height: 120rpx;
}
}
.app-name {
font-size: 44rpx;
font-weight: 800;
color: $text-main;
margin-bottom: 8rpx;
}
.app-slogan {
font-size: 24rpx;
color: $text-sub;
letter-spacing: 2rpx;
}
/* 登录卡片 */
.login-card {
flex: 1;
display: flex;
flex-direction: column;
padding: 0;
overflow: hidden;
}
/* Tab栏 */
.tab-bar {
display: flex;
position: relative;
background: rgba($brand-primary, 0.05);
margin: 24rpx 24rpx 0;
border-radius: $radius-round;
padding: 6rpx;
}
.tab-item {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
height: 76rpx;
position: relative;
z-index: 2;
transition: all $transition-normal $ease-out;
.tab-text {
font-size: 28rpx;
color: $text-sub;
font-weight: 500;
transition: all $transition-normal $ease-out;
}
&.active .tab-text {
color: $brand-primary;
font-weight: 700;
}
}
.tab-indicator {
position: absolute;
top: 6rpx;
left: 6rpx;
width: calc(50% - 6rpx);
height: 76rpx;
background: $bg-card;
border-radius: $radius-round;
box-shadow: $shadow-sm;
transition: transform $transition-normal $ease-out;
z-index: 1;
&.right {
transform: translateX(100%);
}
}
/* 内容区域 */
.content-area {
flex: 1;
padding: 40rpx 32rpx;
}
/* 登录面板 */
.login-panel {
display: flex;
flex-direction: column;
}
/* 微信登录面板 */
.wechat-panel {
align-items: center;
padding-top: 32rpx;
.panel-icon-wrap {
margin-bottom: 32rpx;
}
.panel-icon {
width: 120rpx;
height: 120rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
&.wechat-icon {
background: linear-gradient(135deg, #07c160 0%, #06ad56 100%);
box-shadow: 0 12rpx 32rpx rgba(7, 193, 96, 0.3);
}
.icon-emoji {
font-size: 56rpx;
}
}
.panel-title {
font-size: 36rpx;
font-weight: 700;
color: $text-main;
margin-bottom: 12rpx;
}
.panel-desc {
font-size: 26rpx;
color: $text-sub;
margin-bottom: 48rpx;
}
}
/* 抖音登录面板 */
.toutiao-panel {
align-items: center;
justify-content: center;
padding-top: 48rpx;
.panel-icon-wrap {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 40rpx;
}
.panel-icon {
width: 120rpx;
height: 120rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
&.toutiao-icon {
background: linear-gradient(135deg, #000000 0%, #1a1a1a 100%);
box-shadow: 0 12rpx 32rpx rgba(0, 0, 0, 0.3);
}
.icon-emoji {
font-size: 56rpx;
}
}
.panel-title {
font-size: 36rpx;
font-weight: 700;
color: $text-main;
margin-bottom: 12rpx;
text-align: center;
}
.panel-desc {
font-size: 26rpx;
color: $text-sub;
margin-bottom: 48rpx;
text-align: center;
}
}
/* 短信登录面板 */
.sms-panel {
.panel-title {
font-size: 32rpx;
font-weight: 700;
color: $text-main;
margin-bottom: 8rpx;
}
.panel-desc {
font-size: 24rpx;
color: $text-sub;
margin-bottom: 36rpx;
}
}
.form-group {
display: flex;
flex-direction: column;
gap: 24rpx;
margin-bottom: 36rpx;
}
.input-field {
background: $bg-secondary;
border-radius: $radius-lg;
height: 104rpx;
padding: 0 24rpx;
display: flex;
align-items: center;
border: 2rpx solid transparent;
transition: all $transition-normal $ease-out;
&:focus-within {
background: $bg-card;
border-color: rgba($brand-primary, 0.3);
box-shadow: 0 0 0 6rpx rgba($brand-primary, 0.08);
}
.field-prefix {
margin-right: 16rpx;
padding-right: 16rpx;
border-right: 2rpx solid $border-color-light;
.prefix-text {
font-size: 28rpx;
color: $text-main;
font-weight: 600;
}
.prefix-icon {
font-size: 32rpx;
}
}
.field-input {
flex: 1;
font-size: 30rpx;
color: $text-main;
height: 100%;
}
.clear-btn {
width: 40rpx;
height: 40rpx;
background: $text-tertiary;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 22rpx;
color: $bg-card;
margin-left: 12rpx;
}
}
.send-code-btn {
height: 68rpx;
padding: 0 24rpx;
background: $gradient-brand;
border-radius: $radius-round;
display: flex;
align-items: center;
justify-content: center;
margin-left: 16rpx;
text {
font-size: 24rpx;
color: #fff;
font-weight: 600;
white-space: nowrap;
}
&.disabled {
background: $text-tertiary;
}
}
/* 登录按钮 */
.btn-login {
width: 100%;
height: 100rpx;
font-size: 32rpx;
border: none;
&::after { border: none; }
&.disabled, &[disabled] {
background: $text-tertiary !important;
box-shadow: none !important;
}
}
.hint-text {
display: block;
text-align: center;
font-size: 24rpx;
color: $text-tertiary;
margin-top: 20rpx;
}
/* 协议区 */
.agreement-section {
padding: 24rpx 32rpx 32rpx;
border-top: 2rpx solid $border-color-light;
}
.agreement-row {
display: flex;
align-items: flex-start;
justify-content: center;
}
.checkbox {
width: 36rpx;
height: 36rpx;
border: 2rpx solid $text-tertiary;
border-radius: 50%;
margin-right: 12rpx;
margin-top: 2rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: all $transition-fast $ease-out;
&.checked {
background: $gradient-brand;
border-color: $brand-primary;
}
.check-mark {
color: #fff;
font-size: 22rpx;
font-weight: bold;
}
}
.agreement-text {
font-size: 24rpx;
color: $text-sub;
line-height: 1.6;
.link {
color: $brand-primary;
font-weight: 600;
}
}
/* 底部 */
.footer {
padding: 32rpx 0 20rpx;
text-align: center;
}
.copyright {
font-size: 22rpx;
color: $text-tertiary;
}
</style>