532 lines
13 KiB
Vue
532 lines
13 KiB
Vue
<template>
|
||
<view class="page-container">
|
||
<!-- 顶部装饰背景 - 漂浮光球 -->
|
||
<view class="bg-decoration"></view>
|
||
|
||
<!-- 装饰球体 -->
|
||
<view class="orb orb-1"></view>
|
||
<view class="orb orb-2"></view>
|
||
|
||
<view class="content-wrap">
|
||
<!-- 品牌区域 -->
|
||
<view class="glass-card hero-card">
|
||
<view class="brand-section">
|
||
<view class="logo-box">
|
||
<image class="logo" src="/static/logo.png" mode="widthFix"></image>
|
||
</view>
|
||
<view class="hero-title">🎁 好友邀请</view>
|
||
<view class="welcome-text">开启欧气之旅 ✨</view>
|
||
</view>
|
||
|
||
<!-- 邀请人信息 -->
|
||
<view class="invite-info" v-if="inviteCode">
|
||
<view class="invite-badge">
|
||
<text class="invite-emoji">👋</text>
|
||
<view class="invite-detail">
|
||
<text class="invite-main">好友正在邀请你加入</text>
|
||
<text class="invite-code">邀请码:{{ inviteCode }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 福利卡片 -->
|
||
<view class="glass-card benefits-card">
|
||
<view class="benefits-header">
|
||
<text class="benefits-title">🎉 新人专属福利</text>
|
||
</view>
|
||
<view class="benefits-list">
|
||
<view class="benefit-item">
|
||
<view class="benefit-icon-wrap">
|
||
<text class="benefit-icon">💎</text>
|
||
</view>
|
||
<view class="benefit-text">
|
||
<text class="benefit-main">注册即送10积分</text>
|
||
<text class="benefit-sub">可用于抽奖抵扣</text>
|
||
</view>
|
||
</view>
|
||
<view class="benefit-item">
|
||
<view class="benefit-icon-wrap">
|
||
<text class="benefit-icon">🎫</text>
|
||
</view>
|
||
<view class="benefit-text">
|
||
<text class="benefit-main">首单专属优惠</text>
|
||
<text class="benefit-sub">限时折扣等你拿</text>
|
||
</view>
|
||
</view>
|
||
<view class="benefit-item">
|
||
<view class="benefit-icon-wrap">
|
||
<text class="benefit-icon">🃏</text>
|
||
</view>
|
||
<view class="benefit-text">
|
||
<text class="benefit-main">新手道具卡</text>
|
||
<text class="benefit-sub">免费体验玩法</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- #ifdef MP-WEIXIN -->
|
||
<view class="action-section">
|
||
<button class="btn login-btn" open-type="getPhoneNumber" :disabled="loading" @getphonenumber="onGetPhoneNumber">
|
||
<text class="btn-text">🚀 微信一键加入</text>
|
||
<view class="btn-shine"></view>
|
||
</button>
|
||
</view>
|
||
<!-- #endif -->
|
||
|
||
<!-- #ifndef MP-WEIXIN -->
|
||
<view class="action-section">
|
||
<button class="btn login-btn" @click="goLogin">
|
||
<text class="btn-text">🚀 立即加入</text>
|
||
</button>
|
||
</view>
|
||
<!-- #endif -->
|
||
|
||
<!-- 协议区 -->
|
||
<view class="agreements">
|
||
<view class="checkbox-area" @click="toggleAgreement">
|
||
<view class="checkbox round" :class="{ checked: agreementChecked }">
|
||
<view class="check-mark" v-if="agreementChecked">✓</view>
|
||
</view>
|
||
</view>
|
||
<view class="agreement-text">
|
||
登录即代表同意 <text class="link" @tap="toUserAgreement">用户协议</text> & <text class="link" @tap="toPurchaseAgreement">隐私政策</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="error" class="error-toast">{{ error }}</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref } from 'vue'
|
||
import { onLoad } from '@dcloudio/uni-app'
|
||
import { wechatLogin, bindPhone, getUserStats, getPointsBalance } from '../../api/appUser'
|
||
|
||
const loading = ref(false)
|
||
const error = ref('')
|
||
const inviteCode = ref('')
|
||
const agreementChecked = ref(false)
|
||
|
||
onLoad((options) => {
|
||
const code = options.invite_code || options.inviteCode || ''
|
||
if (code) {
|
||
inviteCode.value = code
|
||
uni.setStorageSync('inviter_code', code)
|
||
}
|
||
})
|
||
|
||
function toggleAgreement() {
|
||
agreementChecked.value = !agreementChecked.value
|
||
}
|
||
|
||
function toUserAgreement() { uni.navigateTo({ url: '/pages-user/agreement/user' }) }
|
||
function toPurchaseAgreement() { uni.navigateTo({ url: '/pages-user/agreement/purchase' }) }
|
||
|
||
function goLogin() {
|
||
uni.navigateTo({ url: '/pages/login/index' })
|
||
}
|
||
|
||
function onGetPhoneNumber(e) {
|
||
if (!agreementChecked.value) {
|
||
uni.showToast({ title: '请先阅读并同意协议', icon: 'none' })
|
||
return
|
||
}
|
||
|
||
const phoneCode = e.detail.code
|
||
if (!phoneCode) {
|
||
uni.showToast({ title: '未授权手机号', icon: 'none' })
|
||
return
|
||
}
|
||
|
||
loading.value = true
|
||
error.value = ''
|
||
|
||
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 || {}
|
||
|
||
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)
|
||
const openid = data && (data.openid || data.open_id)
|
||
if (openid) uni.setStorageSync('openid', openid)
|
||
|
||
try {
|
||
await new Promise(r => setTimeout(r, 600))
|
||
const bindRes = await bindPhone(user_id, phoneCode, { 'X-Suppress-Auth-Modal': true })
|
||
const phoneNumber = (bindRes && (bindRes.phone || bindRes.phone_number || bindRes.mobile)) || ''
|
||
if (phoneNumber) uni.setStorageSync('phone_number', phoneNumber)
|
||
} catch (bindErr) {
|
||
console.warn('Bind phone failed', bindErr)
|
||
}
|
||
|
||
uni.setStorageSync('phone_bound', true)
|
||
|
||
try {
|
||
const stats = await getUserStats(user_id)
|
||
uni.setStorageSync('user_stats', stats)
|
||
const balance = await getPointsBalance(user_id)
|
||
const b = balance && balance.balance !== undefined ? balance.balance : balance
|
||
uni.setStorageSync('points_balance', b)
|
||
} catch(e) {}
|
||
|
||
uni.showToast({ title: '欢迎加入!', icon: 'success' })
|
||
setTimeout(() => {
|
||
uni.switchTab({ url: '/pages/index/index' })
|
||
}, 500)
|
||
|
||
} catch (err) {
|
||
error.value = err.message || '登录失败'
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
},
|
||
fail: () => {
|
||
error.value = '微信登录失败'
|
||
loading.value = false
|
||
}
|
||
})
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.page-container {
|
||
min-height: 100vh;
|
||
background: $bg-secondary;
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* 装饰光球 - 与登录页保持一致 */
|
||
.orb {
|
||
position: absolute;
|
||
border-radius: 50%;
|
||
filter: blur(80rpx);
|
||
opacity: 0.6;
|
||
pointer-events: none;
|
||
}
|
||
.orb-1 {
|
||
width: 500rpx;
|
||
height: 500rpx;
|
||
background: radial-gradient(circle, rgba($brand-primary, 0.4), transparent 70%);
|
||
top: -100rpx;
|
||
left: -100rpx;
|
||
animation: float 8s ease-in-out infinite;
|
||
}
|
||
.orb-2 {
|
||
width: 600rpx;
|
||
height: 600rpx;
|
||
background: radial-gradient(circle, rgba($accent-gold, 0.3), transparent 70%);
|
||
bottom: -150rpx;
|
||
right: -150rpx;
|
||
animation: float 10s ease-in-out infinite reverse;
|
||
}
|
||
|
||
@keyframes float {
|
||
0%, 100% { transform: translate(0, 0); }
|
||
50% { transform: translate(30rpx, 40rpx); }
|
||
}
|
||
|
||
.content-wrap {
|
||
position: relative;
|
||
z-index: 1;
|
||
padding: 40rpx;
|
||
padding-top: calc(env(safe-area-inset-top) + 40rpx);
|
||
animation: fadeInUp 0.6s ease-out;
|
||
}
|
||
|
||
@keyframes fadeInUp {
|
||
from { opacity: 0; transform: translateY(40rpx); }
|
||
to { opacity: 1; transform: translateY(0); }
|
||
}
|
||
|
||
/* Hero Card */
|
||
.hero-card {
|
||
padding: 60rpx 40rpx;
|
||
margin-bottom: $spacing-lg;
|
||
}
|
||
|
||
.brand-section {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
margin-bottom: 40rpx;
|
||
}
|
||
|
||
.logo-box {
|
||
width: 160rpx;
|
||
height: 160rpx;
|
||
background: $bg-card;
|
||
border-radius: 40rpx;
|
||
padding: 20rpx;
|
||
box-shadow: 0 12rpx 30rpx rgba($brand-primary, 0.2);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-bottom: $spacing-xl;
|
||
animation: pulse 3s infinite;
|
||
}
|
||
|
||
@keyframes pulse {
|
||
0%, 100% { transform: scale(1); box-shadow: 0 12rpx 30rpx rgba($brand-primary, 0.2); }
|
||
50% { transform: scale(1.02); box-shadow: 0 16rpx 40rpx rgba($brand-primary, 0.3); }
|
||
}
|
||
|
||
.logo {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.hero-title {
|
||
font-size: 44rpx;
|
||
font-weight: 900;
|
||
color: $text-main;
|
||
margin-bottom: 12rpx;
|
||
letter-spacing: 2rpx;
|
||
text-shadow: 0 2rpx 4rpx rgba(0,0,0,0.05);
|
||
}
|
||
|
||
.welcome-text {
|
||
font-size: 26rpx;
|
||
color: $text-sub;
|
||
letter-spacing: 4rpx;
|
||
opacity: 0.8;
|
||
}
|
||
|
||
/* 邀请人信息 */
|
||
.invite-info {
|
||
margin-top: 20rpx;
|
||
}
|
||
|
||
.invite-badge {
|
||
display: flex;
|
||
align-items: center;
|
||
background: rgba($brand-primary, 0.08);
|
||
border-radius: $radius-lg;
|
||
padding: 20rpx 24rpx;
|
||
border: 1rpx solid rgba($brand-primary, 0.15);
|
||
}
|
||
|
||
.invite-emoji {
|
||
font-size: 48rpx;
|
||
margin-right: 20rpx;
|
||
}
|
||
|
||
.invite-detail {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.invite-main {
|
||
font-size: 28rpx;
|
||
font-weight: 600;
|
||
color: $text-main;
|
||
margin-bottom: 4rpx;
|
||
}
|
||
|
||
.invite-code {
|
||
font-size: 24rpx;
|
||
color: $text-sub;
|
||
}
|
||
|
||
/* Benefits Card */
|
||
.benefits-card {
|
||
padding: 40rpx;
|
||
margin-bottom: $spacing-lg;
|
||
}
|
||
|
||
.benefits-header {
|
||
text-align: center;
|
||
margin-bottom: 32rpx;
|
||
}
|
||
|
||
.benefits-title {
|
||
font-size: 32rpx;
|
||
font-weight: 700;
|
||
color: $text-main;
|
||
}
|
||
|
||
.benefits-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 20rpx;
|
||
}
|
||
|
||
.benefit-item {
|
||
display: flex;
|
||
align-items: center;
|
||
background: $bg-card;
|
||
border-radius: $radius-lg;
|
||
padding: 24rpx;
|
||
box-shadow: $shadow-sm;
|
||
transition: transform 0.2s;
|
||
|
||
&:active {
|
||
transform: scale(0.98);
|
||
}
|
||
}
|
||
|
||
.benefit-icon-wrap {
|
||
width: 80rpx;
|
||
height: 80rpx;
|
||
background: rgba($brand-primary, 0.1);
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-right: 24rpx;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.benefit-icon {
|
||
font-size: 40rpx;
|
||
}
|
||
|
||
.benefit-text {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.benefit-main {
|
||
font-size: 28rpx;
|
||
font-weight: 600;
|
||
color: $text-main;
|
||
margin-bottom: 6rpx;
|
||
}
|
||
|
||
.benefit-sub {
|
||
font-size: 22rpx;
|
||
color: $text-sub;
|
||
}
|
||
|
||
/* Action Section */
|
||
.action-section {
|
||
margin-bottom: $spacing-lg;
|
||
}
|
||
|
||
.btn {
|
||
height: 96rpx;
|
||
border-radius: $radius-round;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: $font-lg;
|
||
font-weight: 800;
|
||
position: relative;
|
||
overflow: hidden;
|
||
transition: all 0.2s;
|
||
|
||
&:active { transform: scale(0.96); }
|
||
}
|
||
|
||
.login-btn {
|
||
background: $gradient-brand;
|
||
color: $text-inverse;
|
||
box-shadow: 0 10rpx 30rpx rgba($brand-primary, 0.3);
|
||
border: none;
|
||
}
|
||
|
||
.btn-text {
|
||
position: relative;
|
||
z-index: 1;
|
||
}
|
||
|
||
.btn-shine {
|
||
position: absolute;
|
||
top: 0; left: -100%;
|
||
width: 50%; height: 100%;
|
||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
|
||
transform: skewX(-20deg);
|
||
animation: shine 3s infinite;
|
||
}
|
||
|
||
@keyframes shine {
|
||
0% { left: -100%; }
|
||
20% { left: 200%; }
|
||
100% { left: 200%; }
|
||
}
|
||
|
||
/* Agreements */
|
||
.agreements {
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: flex-start;
|
||
padding: 0 20rpx;
|
||
}
|
||
|
||
.checkbox-area {
|
||
padding-right: 12rpx;
|
||
}
|
||
|
||
.checkbox {
|
||
width: 36rpx;
|
||
height: 36rpx;
|
||
border: 3rpx solid $border-color;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: all 0.2s;
|
||
background: rgba(255,255,255,0.5);
|
||
|
||
&.checked {
|
||
background: $brand-primary;
|
||
border-color: $brand-primary;
|
||
box-shadow: 0 4rpx 10rpx rgba($brand-primary, 0.3);
|
||
}
|
||
}
|
||
|
||
.check-mark {
|
||
color: $text-inverse;
|
||
font-size: $font-sm;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.agreement-text {
|
||
font-size: $font-sm;
|
||
color: $text-tertiary;
|
||
line-height: 1.5;
|
||
text-align: left;
|
||
}
|
||
|
||
.link {
|
||
color: $brand-primary;
|
||
text-decoration: none;
|
||
font-weight: 600;
|
||
margin: 0 4rpx;
|
||
}
|
||
|
||
.error-toast {
|
||
position: fixed;
|
||
top: 100rpx;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
background: rgba($uni-color-error, 0.9);
|
||
color: $text-inverse;
|
||
padding: 16rpx 32rpx;
|
||
border-radius: 12rpx;
|
||
font-size: 26rpx;
|
||
z-index: 999;
|
||
box-shadow: 0 8rpx 20rpx rgba(0,0,0,0.2);
|
||
animation: slideDown 0.3s ease-out;
|
||
}
|
||
|
||
@keyframes slideDown {
|
||
from { transform: translate(-50%, -100%); opacity: 0; }
|
||
to { transform: translate(-50%, 0); opacity: 1; }
|
||
}
|
||
</style>
|