feat: 添加短信登录功能并重构登录页面以支持微信和短信登录方式
This commit is contained in:
parent
3175c6e8ae
commit
73cfd7ef9b
@ -5,6 +5,31 @@ export function wechatLogin(code, invite_code) {
|
||||
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 = {}) {
|
||||
return authRequest({ url: `/api/app/users/${user_id}/inventory`, method: 'GET', data: { page, page_size, ...params } })
|
||||
}
|
||||
|
||||
@ -1,201 +1,327 @@
|
||||
<template>
|
||||
<view class="page-login">
|
||||
<!-- 全局背景装饰 -->
|
||||
<!-- 背景装饰 -->
|
||||
<view class="bg-decoration"></view>
|
||||
|
||||
<view class="content-wrap">
|
||||
<view class="login-card glass-card anim-fade-in-up">
|
||||
<!-- 品牌标识项 -->
|
||||
<view class="brand-section">
|
||||
<view class="logo-wrapper anim-float">
|
||||
<!-- Logo区域 -->
|
||||
<view class="logo-section">
|
||||
<view class="logo-wrapper">
|
||||
<image class="logo-img" src="/static/logo.png" mode="aspectFit"></image>
|
||||
</view>
|
||||
<text class="brand-title">获取手机号</text>
|
||||
<text class="brand-desc">
|
||||
为保障您的权益并提供精准的发货服务,我们需要获取您的手机号码作为登录标识。
|
||||
</text>
|
||||
<text class="app-name">柯大鸭</text>
|
||||
<text class="app-slogan">潮玩盲盒 · 惊喜无限</text>
|
||||
</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 -->
|
||||
<view class="auth-btn-row">
|
||||
<button class="btn-refuse" @tap="handleRefuse">拒绝</button>
|
||||
<button
|
||||
class="btn-allow anim-scale"
|
||||
class="btn-primary btn-login"
|
||||
open-type="getPhoneNumber"
|
||||
:disabled="loading"
|
||||
@getphonenumber="onGetPhoneNumber"
|
||||
>
|
||||
{{ loading ? '处理中...' : '允许' }}
|
||||
<view class="btn-shine"></view>
|
||||
{{ loading ? '授权中...' : '微信授权登录' }}
|
||||
</button>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
|
||||
<!-- 其他登录 (其他环境或手机号验证码) -->
|
||||
<!-- #ifndef MP-WEIXIN -->
|
||||
<view class="form-container">
|
||||
<view class="u-input-wrap">
|
||||
<input type="text" v-model="account" placeholder="请输入手机号/账号" class="u-input" />
|
||||
</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>
|
||||
<button class="btn-primary btn-login" disabled>
|
||||
请在微信小程序中使用
|
||||
</button>
|
||||
<!-- #endif -->
|
||||
|
||||
</view>
|
||||
|
||||
<!-- 协议勾选 -->
|
||||
<view class="agreement-wrap">
|
||||
<view class="checkbox-container" @tap="toggleAgreement">
|
||||
<view class="custom-checkbox" :class="{ 'is-checked': agreementChecked }">
|
||||
<text class="check-icon" v-if="agreementChecked">✓</text>
|
||||
<!-- 短信登录 -->
|
||||
<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>
|
||||
<view class="agreement-content">
|
||||
我已阅读并同意 <text class="link-text" @tap.stop="toUserAgreement">《用户协议》</text> 和 <text class="link-text" @tap.stop="toPurchaseAgreement">《隐私政策》</text>
|
||||
<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>
|
||||
|
||||
<!-- 页脚品牌标识 -->
|
||||
<view class="page-footer">
|
||||
<text class="footer-copy">Copyright © 2025 柯大鸭潮玩科技</text>
|
||||
<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, onMounted } from 'vue'
|
||||
import { wechatLogin, bindPhone, getUserStats, getPointsBalance } from '../../api/appUser'
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { wechatLogin, bindPhone, getUserStats, getPointsBalance, sendSmsCode, smsLogin } from '../../api/appUser'
|
||||
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
const account = ref("")
|
||||
const pwd = ref("")
|
||||
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(() => {
|
||||
try {
|
||||
const saved = uni.getStorageSync('loginInfo')
|
||||
if (saved && saved.account && saved.pwd) {
|
||||
account.value = saved.account
|
||||
pwd.value = saved.pwd
|
||||
}
|
||||
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 handleRefuse() {
|
||||
uni.showToast({ title: '需要授权手机号才能体验完整功能', icon: 'none' })
|
||||
function toUserAgreement() {
|
||||
uni.navigateTo({ url: '/pages-user/agreement/user' })
|
||||
}
|
||||
|
||||
function handleLogin() {
|
||||
if (!agreementChecked.value) {
|
||||
uni.showToast({ title: '请先阅读并同意相关协议', icon: 'none' })
|
||||
return
|
||||
}
|
||||
uni.showToast({ title: '密码登录正在维护中', icon: 'none' })
|
||||
function toPurchaseAgreement() {
|
||||
uni.navigateTo({ url: '/pages-user/agreement/purchase' })
|
||||
}
|
||||
|
||||
function handleTestLogin() {
|
||||
// 发送验证码
|
||||
async function handleSendCode() {
|
||||
if (!agreementChecked.value) {
|
||||
uni.showToast({ title: '请先阅读并同意相关协议', icon: 'none' })
|
||||
uni.showToast({ title: '请先同意用户协议', icon: 'none' })
|
||||
uni.vibrateShort()
|
||||
return
|
||||
}
|
||||
account.value = 'demo'
|
||||
pwd.value = '123456'
|
||||
uni.showLoading({ title: '测试登录中' })
|
||||
setTimeout(() => {
|
||||
uni.hideLoading()
|
||||
uni.setStorageSync('token', 'mock_token_dev')
|
||||
uni.setStorageSync('user_id', 999)
|
||||
uni.reLaunch({ url: '/pages/mine/index' })
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
if (!agreementChecked.value) {
|
||||
uni.showToast({ title: '请先阅读并同意相关协议', icon: 'none' })
|
||||
uni.showToast({ title: '请先同意用户协议', icon: 'none' })
|
||||
uni.vibrateShort()
|
||||
return
|
||||
}
|
||||
|
||||
const phoneCode = e.detail.code
|
||||
if (!phoneCode) {
|
||||
uni.showToast({ title: '需要授权手机号以完成注册', icon: 'none' })
|
||||
uni.showToast({ title: '需要授权手机号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
|
||||
uni.login({
|
||||
provider: 'weixin',
|
||||
success: async (res) => {
|
||||
try {
|
||||
const loginCode = res.code
|
||||
const inviterCode = uni.getStorageSync('inviter_code')
|
||||
const data = await wechatLogin(loginCode, inviterCode)
|
||||
const token = data && data.token
|
||||
const user_id = data && data.user_id
|
||||
const user_info = data || {}
|
||||
const data = await wechatLogin(res.code, inviterCode)
|
||||
|
||||
// 保存用户信息及 Token
|
||||
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)
|
||||
saveUserData(data)
|
||||
|
||||
// 保存 openid 用于支付等场景
|
||||
const openid = data && (data.openid || data.open_id)
|
||||
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) {
|
||||
try {
|
||||
await bindPhone(user_id, phoneCode)
|
||||
await bindPhone(data.user_id, phoneCode)
|
||||
uni.setStorageSync('phone_bound', true)
|
||||
} catch (e) {
|
||||
console.warn('Bind phone failed', e)
|
||||
}
|
||||
} catch (e) {}
|
||||
} else {
|
||||
uni.setStorageSync('phone_bound', true)
|
||||
}
|
||||
|
||||
// 性能优化:后台静默获取非关键数据,不阻塞页面跳转
|
||||
Promise.all([
|
||||
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)
|
||||
// 后台获取数据
|
||||
fetchExtraData(data.user_id)
|
||||
|
||||
uni.showToast({ title: '✨ 登录成功!', icon: 'none', duration: 1200 })
|
||||
setTimeout(() => uni.reLaunch({ url: '/pages/mine/index' }), 600)
|
||||
} catch (err) {
|
||||
uni.showToast({ title: err.message || '登录异常', icon: 'none' })
|
||||
uni.showToast({ title: err.message || '登录失败', icon: 'none' })
|
||||
} finally {
|
||||
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>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-login {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40rpx;
|
||||
box-sizing: border-box;
|
||||
background: $bg-page;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.content-wrap {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
padding: 80rpx 32rpx 40rpx;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
padding: 80rpx 40rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 品牌区 */
|
||||
.brand-section {
|
||||
/* Logo区域 */
|
||||
.logo-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 80rpx;
|
||||
margin-bottom: 48rpx;
|
||||
}
|
||||
|
||||
.logo-wrapper {
|
||||
width: 180rpx;
|
||||
height: 180rpx;
|
||||
background: #fff;
|
||||
border-radius: 46rpx;
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
background: $bg-card;
|
||||
border-radius: 40rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: $shadow-warm;
|
||||
margin-bottom: 40rpx;
|
||||
padding: 24rpx;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
border: 4rpx solid rgba($brand-primary, 0.05);
|
||||
box-shadow: $shadow-card;
|
||||
margin-bottom: 24rpx;
|
||||
border: 2rpx solid rgba($brand-primary, 0.08);
|
||||
|
||||
.logo-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.brand-info {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.brand-title {
|
||||
font-size: 36rpx;
|
||||
.app-name {
|
||||
font-size: 44rpx;
|
||||
font-weight: 800;
|
||||
color: $text-main;
|
||||
margin-bottom: 24rpx;
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.brand-desc {
|
||||
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;
|
||||
.app-slogan {
|
||||
font-size: 24rpx;
|
||||
color: $text-sub;
|
||||
text-decoration: underline;
|
||||
opacity: 0.5;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/* 短信登录面板 */
|
||||
.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 {
|
||||
margin-top: 40rpx;
|
||||
.agreement-section {
|
||||
padding: 24rpx 32rpx 32rpx;
|
||||
border-top: 2rpx solid $border-color-light;
|
||||
}
|
||||
|
||||
.checkbox-container {
|
||||
.agreement-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.custom-checkbox {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
.checkbox {
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
border: 2rpx solid $text-tertiary;
|
||||
border-radius: 50%;
|
||||
margin-right: 16rpx;
|
||||
margin-top: 4rpx;
|
||||
margin-right: 12rpx;
|
||||
margin-top: 2rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s;
|
||||
flex-shrink: 0;
|
||||
transition: all $transition-fast $ease-out;
|
||||
|
||||
&.is-checked {
|
||||
background: $brand-primary;
|
||||
&.checked {
|
||||
background: $gradient-brand;
|
||||
border-color: $brand-primary;
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
.check-mark {
|
||||
color: #fff;
|
||||
font-size: 20rpx;
|
||||
font-size: 22rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.agreement-content {
|
||||
.agreement-text {
|
||||
font-size: 24rpx;
|
||||
color: $text-sub;
|
||||
line-height: 1.5;
|
||||
text-align: left;
|
||||
max-width: 500rpx;
|
||||
line-height: 1.6;
|
||||
|
||||
.link-text {
|
||||
.link {
|
||||
color: $brand-primary;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
/* 页脚 */
|
||||
.page-footer {
|
||||
margin-top: 100rpx;
|
||||
/* 底部 */
|
||||
.footer {
|
||||
padding: 32rpx 0 20rpx;
|
||||
text-align: center;
|
||||
|
||||
.footer-copy {
|
||||
font-size: 20rpx;
|
||||
color: rgba($text-tertiary, 0.6);
|
||||
letter-spacing: 1rpx;
|
||||
}
|
||||
}
|
||||
|
||||
/* 动画增强 */
|
||||
.anim-fade-in-up {
|
||||
animation: fadeInUp 0.8s $ease-out both;
|
||||
}
|
||||
|
||||
.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;
|
||||
.copyright {
|
||||
font-size: 22rpx;
|
||||
color: $text-tertiary;
|
||||
}
|
||||
</style>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user