feat: 添加短信登录功能并重构登录页面以支持微信和短信登录方式

This commit is contained in:
邹方成 2025-12-28 11:36:07 +08:00
parent 3175c6e8ae
commit 73cfd7ef9b
2 changed files with 572 additions and 309 deletions

View File

@ -5,6 +5,31 @@ export function wechatLogin(code, invite_code) {
return request({ url: '/api/app/users/weixin/login', method: 'POST', data }) return request({ url: '/api/app/users/weixin/login', method: 'POST', data })
} }
// ============================================
// 短信登录 API
// ============================================
/**
* 发送短信验证码
* @param {string} mobile - 手机号
*/
export function sendSmsCode(mobile) {
return request({ url: '/api/app/sms/send-code', method: 'POST', data: { mobile } })
}
/**
* 短信验证码登录
* @param {string} mobile - 手机号
* @param {string} code - 验证码
* @param {string} invite_code - 可选邀请码
*/
export function smsLogin(mobile, code, invite_code) {
const data = { mobile, code }
if (invite_code) data.invite_code = invite_code
return request({ url: '/api/app/sms/login', method: 'POST', data })
}
export function getInventory(user_id, page = 1, page_size = 20, params = {}) { export function getInventory(user_id, page = 1, page_size = 20, params = {}) {
return authRequest({ url: `/api/app/users/${user_id}/inventory`, method: 'GET', data: { page, page_size, ...params } }) return authRequest({ url: `/api/app/users/${user_id}/inventory`, method: 'GET', data: { page, page_size, ...params } })
} }

View File

@ -1,201 +1,327 @@
<template> <template>
<view class="page-login"> <view class="page-login">
<!-- 全局背景装饰 --> <!-- 背景装饰 -->
<view class="bg-decoration"></view> <view class="bg-decoration"></view>
<view class="content-wrap"> <view class="content-wrap">
<view class="login-card glass-card anim-fade-in-up"> <!-- Logo区域 -->
<!-- 品牌标识项 --> <view class="logo-section">
<view class="brand-section"> <view class="logo-wrapper">
<view class="logo-wrapper anim-float">
<image class="logo-img" src="/static/logo.png" mode="aspectFit"></image> <image class="logo-img" src="/static/logo.png" mode="aspectFit"></image>
</view> </view>
<text class="brand-title">获取手机号</text> <text class="app-name">柯大鸭</text>
<text class="brand-desc"> <text class="app-slogan">潮玩盲盒 · 惊喜无限</text>
为保障您的权益并提供精准的发货服务我们需要获取您的手机号码作为登录标识
</text>
</view> </view>
<!-- 登录方式区域 --> <!-- 登录卡片 -->
<view class="auth-section"> <view class="login-card glass-card">
<!-- 微信一键登录 (微信小程序环境) --> <!-- 切换Tab -->
<view class="tab-bar">
<view
class="tab-item"
:class="{ active: loginMode === 'wechat' }"
@tap="switchMode('wechat')"
>
<text class="tab-text">微信快捷登录</text>
</view>
<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">
<!-- 微信登录 -->
<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>
<!-- #ifdef MP-WEIXIN --> <!-- #ifdef MP-WEIXIN -->
<view class="auth-btn-row">
<button class="btn-refuse" @tap="handleRefuse">拒绝</button>
<button <button
class="btn-allow anim-scale" class="btn-primary btn-login"
open-type="getPhoneNumber" open-type="getPhoneNumber"
:disabled="loading" :disabled="loading"
@getphonenumber="onGetPhoneNumber" @getphonenumber="onGetPhoneNumber"
> >
{{ loading ? '处理中...' : '允许' }} {{ loading ? '授权中...' : '微信授权登录' }}
<view class="btn-shine"></view>
</button> </button>
</view>
<!-- #endif --> <!-- #endif -->
<!-- 其他登录 (其他环境或手机号验证码) -->
<!-- #ifndef MP-WEIXIN --> <!-- #ifndef MP-WEIXIN -->
<view class="form-container"> <button class="btn-primary btn-login" disabled>
<view class="u-input-wrap"> 请在微信小程序中使用
<input type="text" v-model="account" placeholder="请输入手机号/账号" class="u-input" /> </button>
</view>
<view class="u-input-wrap">
<input type="password" v-model="pwd" placeholder="请输入密码" class="u-input" />
</view>
<button class="btn-primary-large" @tap="handleLogin">立即登录</button>
</view>
<!-- #endif --> <!-- #endif -->
</view> </view>
<!-- 协议勾选 --> <!-- 短信登录 -->
<view class="agreement-wrap"> <view v-else class="login-panel sms-panel">
<view class="checkbox-container" @tap="toggleAgreement"> <text class="panel-title">手机号验证码登录</text>
<view class="custom-checkbox" :class="{ 'is-checked': agreementChecked }"> <text class="panel-desc">输入手机号获取验证码完成登录</text>
<text class="check-icon" v-if="agreementChecked"></text>
<view class="form-group">
<!-- 手机号 -->
<view class="input-field">
<view class="field-prefix">
<text class="prefix-text">+86</text>
</view> </view>
<view class="agreement-content"> <input
我已阅读并同意 <text class="link-text" @tap.stop="toUserAgreement">用户协议</text> <text class="link-text" @tap.stop="toPurchaseAgreement">隐私政策</text> type="number"
v-model="mobile"
placeholder="请输入手机号"
class="field-input"
maxlength="11"
/>
<text v-if="mobile" class="clear-btn" @tap="mobile = ''"></text>
</view> </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> </view>
</view> </view>
<!-- 页脚品牌标识 --> <button
<view class="page-footer"> class="btn-primary btn-login"
<text class="footer-copy">Copyright © 2025 柯大鸭潮玩科技</text> :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> </view>
</view> </view>
</template> </template>
<script setup> <script setup>
import { ref, onMounted } from 'vue' import { ref, computed, onMounted, onUnmounted } from 'vue'
import { wechatLogin, bindPhone, getUserStats, getPointsBalance } from '../../api/appUser' import { wechatLogin, bindPhone, getUserStats, getPointsBalance, sendSmsCode, smsLogin } from '../../api/appUser'
const loading = ref(false) const loading = ref(false)
const error = ref('')
const account = ref("")
const pwd = ref("")
const agreementChecked = ref(false) const agreementChecked = ref(false)
const process = { env: { NODE_ENV: 'development' } } // Polyfill for template if needed
//
const loginMode = ref('wechat')
//
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(() => { onMounted(() => {
try { try {
const saved = uni.getStorageSync('loginInfo') const savedMobile = uni.getStorageSync('last_login_mobile')
if (saved && saved.account && saved.pwd) { if (savedMobile) mobile.value = savedMobile
account.value = saved.account
pwd.value = saved.pwd
}
} catch (e) {} } catch (e) {}
}) })
onUnmounted(() => {
if (countdownTimer) clearInterval(countdownTimer)
})
function switchMode(mode) {
loginMode.value = mode
}
function toggleAgreement() { function toggleAgreement() {
agreementChecked.value = !agreementChecked.value agreementChecked.value = !agreementChecked.value
} }
function handleRefuse() { function toUserAgreement() {
uni.showToast({ title: '需要授权手机号才能体验完整功能', icon: 'none' }) uni.navigateTo({ url: '/pages-user/agreement/user' })
} }
function handleLogin() { function toPurchaseAgreement() {
if (!agreementChecked.value) { uni.navigateTo({ url: '/pages-user/agreement/purchase' })
uni.showToast({ title: '请先阅读并同意相关协议', icon: 'none' })
return
}
uni.showToast({ title: '密码登录正在维护中', icon: 'none' })
} }
function handleTestLogin() { //
async function handleSendCode() {
if (!agreementChecked.value) { if (!agreementChecked.value) {
uni.showToast({ title: '请先阅读并同意相关协议', icon: 'none' }) uni.showToast({ title: '请先同意用户协议', icon: 'none' })
uni.vibrateShort()
return return
} }
account.value = 'demo'
pwd.value = '123456' if (countdown.value > 0 || sendingCode.value) return
uni.showLoading({ title: '测试登录中' })
setTimeout(() => { if (!mobile.value || mobile.value.length !== 11) {
uni.hideLoading() uni.showToast({ title: '请输入正确的手机号', icon: 'none' })
uni.setStorageSync('token', 'mock_token_dev') return
uni.setStorageSync('user_id', 999) }
uni.reLaunch({ url: '/pages/mine/index' })
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) }, 1000)
} catch (err) {
uni.showToast({ title: err.message || '发送失败', icon: 'none' })
} finally {
sendingCode.value = false
}
} }
function toUserAgreement() { uni.navigateTo({ url: '/pages-user/agreement/user' }) } //
function toPurchaseAgreement() { uni.navigateTo({ url: '/pages-user/agreement/purchase' }) } async function handleSmsLogin() {
if (!agreementChecked.value) {
uni.showToast({ title: '请先同意用户协议', icon: 'none' })
uni.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)
saveUserData(data)
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
}
}
//
function onGetPhoneNumber(e) { function onGetPhoneNumber(e) {
if (!agreementChecked.value) { if (!agreementChecked.value) {
uni.showToast({ title: '请先阅读并同意相关协议', icon: 'none' }) uni.showToast({ title: '请先同意用户协议', icon: 'none' })
uni.vibrateShort() uni.vibrateShort()
return return
} }
const phoneCode = e.detail.code const phoneCode = e.detail.code
if (!phoneCode) { if (!phoneCode) {
uni.showToast({ title: '需要授权手机号以完成注册', icon: 'none' }) uni.showToast({ title: '需要授权手机号', icon: 'none' })
return return
} }
loading.value = true loading.value = true
uni.login({ uni.login({
provider: 'weixin', provider: 'weixin',
success: async (res) => { success: async (res) => {
try { try {
const loginCode = res.code
const inviterCode = uni.getStorageSync('inviter_code') const inviterCode = uni.getStorageSync('inviter_code')
const data = await wechatLogin(loginCode, inviterCode) const data = await wechatLogin(res.code, inviterCode)
const token = data && data.token
const user_id = data && data.user_id
const user_info = data || {}
// Token saveUserData(data)
uni.setStorageSync('user_info', user_info)
if (token) uni.setStorageSync('token', token)
if (user_id) uni.setStorageSync('user_id', user_id)
if (user_info.avatar) uni.setStorageSync('avatar', user_info.avatar)
if (user_info.nickname) uni.setStorageSync('nickname', user_info.nickname)
if (user_info.invite_code) uni.setStorageSync('invite_code', user_info.invite_code)
// openid
const openid = data && (data.openid || data.open_id) const openid = data && (data.openid || data.open_id)
if (openid) uni.setStorageSync('openid', openid) if (openid) uni.setStorageSync('openid', openid)
// //
const isBound = user_info.phone || user_info.phone_number || user_info.mobile const isBound = data.phone || data.phone_number || data.mobile
if (!isBound) { if (!isBound) {
try { try {
await bindPhone(user_id, phoneCode) await bindPhone(data.user_id, phoneCode)
uni.setStorageSync('phone_bound', true) uni.setStorageSync('phone_bound', true)
} catch (e) { } catch (e) {}
console.warn('Bind phone failed', e)
}
} else { } else {
uni.setStorageSync('phone_bound', true) uni.setStorageSync('phone_bound', true)
} }
// //
Promise.all([ fetchExtraData(data.user_id)
getUserStats(user_id).then(stats => {
if (stats) uni.setStorageSync('user_stats', stats)
}).catch(()=>{}),
getPointsBalance(user_id).then(balance => {
const b = balance && balance.balance !== undefined ? balance.balance : balance
uni.setStorageSync('points_balance', b)
}).catch(()=>{})
])
uni.showToast({ title: '欧气加持,登录成功!', icon: 'none', duration: 1000 })
//
setTimeout(() => {
uni.reLaunch({ url: '/pages/mine/index' })
}, 500)
uni.showToast({ title: '✨ 登录成功!', icon: 'none', duration: 1200 })
setTimeout(() => uni.reLaunch({ url: '/pages/mine/index' }), 600)
} catch (err) { } catch (err) {
uni.showToast({ title: err.message || '登录异常', icon: 'none' }) uni.showToast({ title: err.message || '登录失败', icon: 'none' })
} finally { } finally {
loading.value = false loading.value = false
} }
@ -205,261 +331,373 @@ function onGetPhoneNumber(e) {
} }
}) })
} }
function saveUserData(data) {
if (!data) return
uni.setStorageSync('user_info', data)
if (data.token) uni.setStorageSync('token', data.token)
if (data.user_id) uni.setStorageSync('user_id', data.user_id)
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)
}
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> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.page-login { .page-login {
min-height: 100vh; min-height: 100vh;
display: flex; background: $bg-page;
align-items: center; position: relative;
justify-content: center;
padding: 40rpx;
box-sizing: border-box;
} }
.content-wrap { .content-wrap {
width: 100%;
position: relative; position: relative;
z-index: 10; z-index: 10;
padding: 80rpx 32rpx 40rpx;
min-height: 100vh;
display: flex;
flex-direction: column;
} }
.login-card { /* Logo区域 */
padding: 80rpx 40rpx; .logo-section {
text-align: center;
}
/* 品牌区 */
.brand-section {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
margin-bottom: 80rpx; margin-bottom: 48rpx;
} }
.logo-wrapper { .logo-wrapper {
width: 180rpx; width: 160rpx;
height: 180rpx; height: 160rpx;
background: #fff; background: $bg-card;
border-radius: 46rpx; border-radius: 40rpx;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
box-shadow: $shadow-warm; box-shadow: $shadow-card;
margin-bottom: 40rpx; margin-bottom: 24rpx;
padding: 24rpx; border: 2rpx solid rgba($brand-primary, 0.08);
box-sizing: border-box;
overflow: hidden;
border: 4rpx solid rgba($brand-primary, 0.05);
.logo-img { .logo-img {
width: 100%; width: 120rpx;
height: 100%; height: 120rpx;
} }
} }
.brand-info { .app-name {
text-align: center; font-size: 44rpx;
}
.brand-title {
font-size: 36rpx;
font-weight: 800; font-weight: 800;
color: $text-main; color: $text-main;
margin-bottom: 24rpx; margin-bottom: 8rpx;
display: block;
} }
.brand-desc { .app-slogan {
font-size: 26rpx;
color: $text-sub;
line-height: 1.6;
text-align: center;
padding: 0 20rpx;
}
/* 登录操作区 */
.auth-section {
margin-top: 100rpx;
margin-bottom: 60rpx;
}
.auth-btn-row {
display: flex;
gap: 30rpx;
margin-bottom: 0;
}
.btn-refuse {
flex: 1;
height: 100rpx;
border-radius: 50rpx;
background: #f1f3f5;
color: $text-sub;
font-size: 32rpx;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
border: none;
&::after { border: none; }
}
.btn-allow {
flex: 1.2;
height: 100rpx;
border-radius: 50rpx;
background: $gradient-brand;
color: #fff;
font-size: 32rpx;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
box-shadow: $shadow-warm;
position: relative;
overflow: hidden;
border: none;
&::after { border: none; }
}
.dev-login {
margin-top: 40rpx;
font-size: 24rpx; font-size: 24rpx;
color: $text-sub; color: $text-sub;
text-decoration: underline; letter-spacing: 2rpx;
opacity: 0.5; }
/* 登录卡片 */
.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;
}
}
/* 短信登录面板 */
.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-wrap { .agreement-section {
margin-top: 40rpx; padding: 24rpx 32rpx 32rpx;
border-top: 2rpx solid $border-color-light;
} }
.checkbox-container { .agreement-row {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
justify-content: center; justify-content: center;
} }
.custom-checkbox { .checkbox {
width: 32rpx; width: 36rpx;
height: 32rpx; height: 36rpx;
border: 2rpx solid $text-tertiary; border: 2rpx solid $text-tertiary;
border-radius: 50%; border-radius: 50%;
margin-right: 16rpx; margin-right: 12rpx;
margin-top: 4rpx; margin-top: 2rpx;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
transition: all 0.2s;
flex-shrink: 0; flex-shrink: 0;
transition: all $transition-fast $ease-out;
&.is-checked { &.checked {
background: $brand-primary; background: $gradient-brand;
border-color: $brand-primary; border-color: $brand-primary;
} }
.check-icon { .check-mark {
color: #fff; color: #fff;
font-size: 20rpx; font-size: 22rpx;
font-weight: bold; font-weight: bold;
} }
} }
.agreement-content { .agreement-text {
font-size: 24rpx; font-size: 24rpx;
color: $text-sub; color: $text-sub;
line-height: 1.5; line-height: 1.6;
text-align: left;
max-width: 500rpx;
.link-text { .link {
color: $brand-primary; color: $brand-primary;
font-weight: 600; font-weight: 600;
} }
} }
/* 页脚 */ /* 底部 */
.page-footer { .footer {
margin-top: 100rpx; padding: 32rpx 0 20rpx;
text-align: center; text-align: center;
.footer-copy {
font-size: 20rpx;
color: rgba($text-tertiary, 0.6);
letter-spacing: 1rpx;
}
} }
/* 动画增强 */ .copyright {
.anim-fade-in-up { font-size: 22rpx;
animation: fadeInUp 0.8s $ease-out both; color: $text-tertiary;
}
.anim-float {
animation: float 4s ease-in-out infinite;
}
.anim-scale {
transition: transform 0.2s $ease-out;
&:active {
transform: scale(0.96);
}
}
.btn-shine {
position: absolute;
top: 0; left: -100%;
width: 50%; height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
transform: skewX(-25deg);
animation: shine 4s infinite;
}
@keyframes shine {
0% { left: -150%; }
30% { left: 150%; }
100% { left: 150%; }
}
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-15rpx); }
}
/* 其他表单 */
.form-container {
display: flex;
flex-direction: column;
gap: 30rpx;
}
.u-input-wrap {
background: #f8f8fa;
border-radius: 50rpx;
height: 90rpx;
padding: 0 40rpx;
display: flex;
align-items: center;
border: 1px solid transparent;
&:focus-within {
border-color: rgba($brand-primary, 0.3);
background: #fff;
}
}
.u-input {
flex: 1;
font-size: 28rpx;
}
.btn-primary-large {
background: $gradient-brand;
color: #fff;
height: 90rpx;
border-radius: 45rpx;
border: none;
font-weight: 700;
box-shadow: $shadow-warm;
} }
</style> </style>