532 lines
13 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-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>