746 lines
18 KiB
Vue
746 lines
18 KiB
Vue
<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 { onLoad } from '@dcloudio/uni-app'
|
||
import { request } from '../../utils/request'
|
||
import { wechatLogin, bindPhone, getUserStats, getPointsBalance, sendSmsCode, smsLogin } from '../../api/appUser'
|
||
|
||
const loading = ref(false)
|
||
const agreementChecked = ref(false)
|
||
|
||
// 登录模式
|
||
const loginMode = ref('wechat')
|
||
|
||
// 静默获取 OpenID
|
||
async function ensureOpenID() {
|
||
const current = uni.getStorageSync('openid')
|
||
if (current) return
|
||
|
||
console.log('[DEBUG] 本地缺少 openid, 尝试静默获取...')
|
||
uni.login({
|
||
provider: 'weixin',
|
||
success: async (loginRes) => {
|
||
try {
|
||
const res = await request({
|
||
url: '/api/app/common/openid',
|
||
method: 'POST',
|
||
data: { code: loginRes.code }
|
||
})
|
||
if (res && res.openid) {
|
||
console.log('[DEBUG] 静默获取 openid 成功:', res.openid)
|
||
uni.setStorageSync('openid', res.openid)
|
||
}
|
||
} catch (err) {
|
||
console.error('[DEBUG] 静默获取 openid 失败:', err)
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
onLoad(() => {
|
||
ensureOpenID()
|
||
})
|
||
|
||
const fetchExtraDataLoading = ref(false)
|
||
|
||
// 短信登录
|
||
const mobile = ref('')
|
||
const smsCode = ref('')
|
||
const countdown = ref(0)
|
||
const sendingCode = ref(false)
|
||
let countdownTimer = null
|
||
|
||
// 计算属性
|
||
const canSendCode = computed(() => {
|
||
return mobile.value.length === 11 && /^1[3-9]\d{9}$/.test(mobile.value) && agreementChecked.value
|
||
})
|
||
|
||
const canLogin = computed(() => {
|
||
return mobile.value.length === 11 && smsCode.value.length >= 4 && agreementChecked.value
|
||
})
|
||
|
||
onMounted(() => {
|
||
try {
|
||
const savedMobile = uni.getStorageSync('last_login_mobile')
|
||
if (savedMobile) mobile.value = savedMobile
|
||
} catch (e) {}
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
if (countdownTimer) clearInterval(countdownTimer)
|
||
})
|
||
|
||
function switchMode(mode) {
|
||
loginMode.value = mode
|
||
}
|
||
|
||
function toggleAgreement() {
|
||
agreementChecked.value = !agreementChecked.value
|
||
}
|
||
|
||
function toUserAgreement() {
|
||
uni.navigateTo({ url: '/pages-user/agreement/user' })
|
||
}
|
||
|
||
function toPurchaseAgreement() {
|
||
uni.navigateTo({ url: '/pages-user/agreement/purchase' })
|
||
}
|
||
|
||
// 发送验证码
|
||
async function handleSendCode() {
|
||
if (!agreementChecked.value) {
|
||
uni.showToast({ title: '请先同意用户协议', icon: 'none' })
|
||
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)
|
||
|
||
console.log('[DEBUG] 短信登录响应原始数据:', data)
|
||
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 isBound = data.phone || data.phone_number || data.mobile
|
||
if (!isBound) {
|
||
try {
|
||
await bindPhone(data.user_id, phoneCode)
|
||
} catch (e) {}
|
||
}
|
||
|
||
// 后台获取数据
|
||
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
|
||
console.log('[DEBUG] 准备执行 saveUserData, payload:', data)
|
||
uni.setStorageSync('user_info', data)
|
||
if (data.token) uni.setStorageSync('token', data.token)
|
||
if (data.user_id) uni.setStorageSync('user_id', data.user_id)
|
||
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)
|
||
|
||
// 核心修复:无论哪种登录方式,登录成功后都标记手机号已绑定
|
||
uni.setStorageSync('phone_bound', true)
|
||
|
||
// 如果返回了 openid,则存储 (短信登录现在也会返回已关联的 openid)
|
||
const openid = data.openid || data.open_id
|
||
if (openid) {
|
||
console.log('[DEBUG] 存储从登录接口获取的 openid:', openid)
|
||
uni.setStorageSync('openid', openid)
|
||
} else {
|
||
console.warn('[DEBUG] 登录接口未返回 openid, 请检查后端或联系管理员')
|
||
}
|
||
}
|
||
|
||
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>
|