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 })
|
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 } })
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user