704 lines
16 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">
<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 -->
<button
class="btn-primary btn-login"
open-type="getPhoneNumber"
:disabled="loading"
@getphonenumber="onGetPhoneNumber"
>
{{ loading ? '授权中...' : '微信授权登录' }}
</button>
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
<button class="btn-primary btn-login" disabled>
请在微信小程序中使用
</button>
<!-- #endif -->
</view>
<!-- 短信登录 -->
<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 { wechatLogin, bindPhone, getUserStats, getPointsBalance, sendSmsCode, smsLogin } from '../../api/appUser'
const loading = ref(false)
const agreementChecked = ref(false)
// 登录模式
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 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' })
uni.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' })
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.vibrateShort()
return
}
const phoneCode = e.detail.code
if (!phoneCode) {
uni.showToast({ title: '需要授权手机号', icon: 'none' })
return
}
loading.value = true
uni.login({
provider: 'weixin',
success: async (res) => {
try {
const inviterCode = uni.getStorageSync('inviter_code')
const data = await wechatLogin(res.code, inviterCode)
saveUserData(data)
const openid = data && (data.openid || data.open_id)
if (openid) uni.setStorageSync('openid', openid)
// 绑定手机号
const isBound = data.phone || data.phone_number || data.mobile
if (!isBound) {
try {
await bindPhone(data.user_id, phoneCode)
uni.setStorageSync('phone_bound', true)
} catch (e) {}
} else {
uni.setStorageSync('phone_bound', true)
}
// 后台获取数据
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' })
} finally {
loading.value = false
}
},
fail: () => {
loading.value = false
}
})
}
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;
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;
}
}
/* 短信登录面板 */
.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>