746 lines
18 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 { 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>