feat: 移除注册页,新增邀请落地页,优化分享流程、积分展示及活动加载,并添加分享图片。

This commit is contained in:
邹方成 2025-12-27 01:54:08 +08:00
parent 3dde150cde
commit e19ec06d74
12 changed files with 744 additions and 387 deletions

View File

@ -4,35 +4,30 @@
<view class="cabinet-panel" @tap.stop> <view class="cabinet-panel" @tap.stop>
<view class="cabinet-header"> <view class="cabinet-header">
<text class="cabinet-title">我的盒柜</text> <text class="cabinet-title">我的盒柜</text>
<view class="cabinet-actions">
<text class="view-all" @tap="goFullCabinet">查看全部</text>
<text class="cabinet-close" @tap="close">×</text> <text class="cabinet-close" @tap="close">×</text>
</view> </view>
</view>
<scroll-view scroll-y class="cabinet-content">
<view v-if="loading" class="cabinet-loading"> <view v-if="loading" class="cabinet-loading">
<text class="loading-icon">📦</text>
<text class="loading-text">加载中...</text> <text class="loading-text">加载中...</text>
</view> </view>
<view v-else-if="items.length === 0" class="cabinet-empty"> <view v-else-if="items.length === 0" class="cabinet-empty">
<text class="empty-icon">🎁</text> <text class="empty-text">暂无物品参与活动获取奖品</text>
<text class="empty-text">暂无物品</text>
<text class="empty-hint">参与活动获得奖品后会显示在这里</text>
</view> </view>
<view v-else class="cabinet-grid"> <scroll-view v-else scroll-x class="cabinet-scroll">
<view v-for="item in displayItems" :key="item.id" class="cabinet-item"> <view class="thumb-list">
<image class="item-image" :src="item.image" mode="aspectFill" /> <view v-for="item in displayItems" :key="item.id" class="thumb-item">
<view class="item-info"> <image class="thumb-img" :src="item.image" mode="aspectFill" />
<text class="item-name">{{ item.name }}</text> <text class="thumb-count">x{{ item.count }}</text>
<text class="item-count">x{{ item.count }}</text>
</view> </view>
<view v-if="hasMore" class="thumb-more" @tap="goFullCabinet">
<text>+{{ items.length - maxDisplay }}</text>
</view> </view>
</view> </view>
<view v-if="hasMore" class="load-more" @tap="goFullCabinet">
<text>查看全部 {{ total }} 件物品</text>
<text class="arrow"></text>
</view>
</scroll-view> </scroll-view>
</view> </view>
</view> </view>
@ -40,16 +35,11 @@
<script setup> <script setup>
import { ref, computed, watch } from 'vue' import { ref, computed, watch } from 'vue'
import { getInventory } from '@/api/appUser'
const props = defineProps({ const props = defineProps({
visible: { visible: { type: Boolean, default: false },
type: Boolean, activityId: { type: [String, Number], default: '' }
default: false
},
activityId: {
type: [String, Number],
default: ''
}
}) })
const emit = defineEmits(['update:visible']) const emit = defineEmits(['update:visible'])
@ -57,203 +47,189 @@ const emit = defineEmits(['update:visible'])
const loading = ref(false) const loading = ref(false)
const items = ref([]) const items = ref([])
const total = ref(0) const total = ref(0)
const maxDisplay = 6 // 6 const maxDisplay = 8
const displayItems = computed(() => items.value.slice(0, maxDisplay)) const displayItems = computed(() => items.value.slice(0, maxDisplay))
const hasMore = computed(() => items.value.length > maxDisplay || total.value > items.value.length) const hasMore = computed(() => items.value.length > maxDisplay)
function close() { function close() { emit('update:visible', false) }
emit('update:visible', false)
}
function goFullCabinet() { function goFullCabinet() {
close() close()
uni.switchTab({ url: '/pages/cabinet/index' }) uni.switchTab({ url: '/pages/cabinet/index' })
} }
// API function cleanUrl(u) {
async function loadItems() { if (!u) return '/static/logo.png'
if (!props.activityId) return let s = String(u).trim()
if (s.startsWith('[') && s.endsWith(']')) {
try { const arr = JSON.parse(s); if (Array.isArray(arr) && arr.length > 0) s = arr[0] } catch (e) {}
}
s = s.replace(/[`'"]/g, '').trim()
const m = s.match(/https?:\/\/[^\s]+/)
if (m && m[0]) return m[0]
return s || '/static/logo.png'
}
async function loadItems() {
loading.value = true loading.value = true
try { try {
// TODO: API const userId = uni.getStorageSync('user_id')
// const res = await getUserCabinetItems(props.activityId) if (!userId) { items.value = []; total.value = 0; return }
// items.value = res.items || [] const res = await getInventory(userId, 1, 50)
// total.value = res.total || 0 let list = []
let rawTotal = 0
if (res && Array.isArray(res.list)) { list = res.list; rawTotal = res.total || 0 }
else if (res && Array.isArray(res.data)) { list = res.data; rawTotal = res.total || 0 }
else if (Array.isArray(res)) { list = res; rawTotal = res.length }
// const filtered = list.filter(item => Number(item.status) === 1 && !item.has_shipment)
await new Promise(r => setTimeout(r, 300)) const aggregated = new Map()
filtered.forEach(item => {
const key = String(item.product_id || item.id)
if (aggregated.has(key)) {
aggregated.get(key).count += 1
} else {
aggregated.set(key, {
id: key,
name: (item.product_name || item.name || '').trim() || '未知物品',
image: cleanUrl(item.product_images || item.image),
count: 1
})
}
})
items.value = Array.from(aggregated.values())
total.value = rawTotal
} catch (e) {
console.error('[CabinetPreviewPopup] 加载失败', e)
items.value = [] items.value = []
total.value = 0 total.value = 0
} catch (e) {
console.error('加载盒柜失败', e)
} finally { } finally {
loading.value = false loading.value = false
} }
} }
watch(() => props.visible, (v) => { watch(() => props.visible, (v) => { if (v) loadItems() })
if (v) loadItems()
})
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.cabinet-overlay { .cabinet-overlay {
position: fixed; position: fixed;
left: 0; left: 0; right: 0; top: 0; bottom: 0;
right: 0;
top: 0;
bottom: 0;
z-index: 9000; z-index: 9000;
} }
.cabinet-mask { .cabinet-mask {
position: absolute; position: absolute;
left: 0; left: 0; right: 0; top: 0; bottom: 0;
right: 0; background: rgba(0, 0, 0, 0.5);
top: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(10rpx);
} }
.cabinet-panel { .cabinet-panel {
position: absolute; position: absolute;
left: $spacing-lg; left: 24rpx; right: 24rpx;
right: $spacing-lg;
bottom: calc(env(safe-area-inset-bottom) + 24rpx); bottom: calc(env(safe-area-inset-bottom) + 24rpx);
max-height: 65vh; background: rgba(255, 255, 255, 0.95);
background: rgba($bg-card, 0.95); border-radius: 24rpx;
border-radius: $radius-xl; box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.12);
box-shadow: $shadow-card;
border: 1rpx solid rgba(255, 255, 255, 0.5);
overflow: hidden; overflow: hidden;
animation: slideUp 0.25s ease-out; animation: slideUp 0.2s ease-out;
} }
.cabinet-header { .cabinet-header {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: $spacing-lg; padding: 20rpx 24rpx;
border-bottom: 1rpx solid rgba(0, 0, 0, 0.06); border-bottom: 1rpx solid rgba(0, 0, 0, 0.06);
} }
.cabinet-title { .cabinet-title {
font-size: $font-lg; font-size: 28rpx;
font-weight: 800; font-weight: 700;
color: $text-main; color: #333;
}
.cabinet-actions {
display: flex;
align-items: center;
gap: 16rpx;
}
.view-all {
font-size: 24rpx;
color: #FF6B35;
font-weight: 600;
} }
.cabinet-close { .cabinet-close {
font-size: 48rpx; font-size: 40rpx;
line-height: 1; line-height: 1;
color: $text-tertiary; color: #999;
padding: 0 10rpx; padding: 0 8rpx;
} }
.cabinet-content { .cabinet-loading, .cabinet-empty {
max-height: 50vh; padding: 32rpx 24rpx;
padding: $spacing-lg;
}
.cabinet-loading,
.cabinet-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60rpx $spacing-lg;
}
.loading-icon,
.empty-icon {
font-size: 64rpx;
margin-bottom: $spacing-md;
}
.loading-text,
.empty-text {
font-size: $font-md;
color: $text-sub;
font-weight: 600;
}
.empty-hint {
font-size: $font-xs;
color: $text-tertiary;
margin-top: $spacing-xs;
}
.cabinet-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: $spacing-md;
}
.cabinet-item {
display: flex;
flex-direction: column;
align-items: center;
}
.item-image {
width: 100%;
aspect-ratio: 1 / 1;
border-radius: $radius-md;
background: $bg-secondary;
margin-bottom: $spacing-xs;
}
.item-info {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
}
.item-name {
font-size: $font-xs;
color: $text-main;
font-weight: 600;
@include text-ellipsis(1);
text-align: center; text-align: center;
width: 100%;
} }
.item-count { .loading-text, .empty-text {
font-size: 22rpx; font-size: 24rpx;
color: $brand-primary; color: #999;
font-weight: 700;
} }
.load-more { .cabinet-scroll {
white-space: nowrap;
padding: 20rpx 24rpx;
}
.thumb-list {
display: inline-flex;
gap: 16rpx;
}
.thumb-item {
position: relative;
flex-shrink: 0;
}
.thumb-img {
width: 100rpx;
height: 100rpx;
border-radius: 12rpx;
background: #f5f5f5;
}
.thumb-count {
position: absolute;
right: 4rpx;
bottom: 4rpx;
background: rgba(0, 0, 0, 0.6);
color: #fff;
font-size: 20rpx;
padding: 2rpx 8rpx;
border-radius: 8rpx;
font-weight: 600;
}
.thumb-more {
width: 100rpx;
height: 100rpx;
border-radius: 12rpx;
background: #f0f0f0;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 8rpx; font-size: 24rpx;
padding: $spacing-lg 0; color: #666;
margin-top: $spacing-md;
color: $brand-primary;
font-size: $font-sm;
font-weight: 600; font-weight: 600;
border-top: 1rpx solid rgba(0, 0, 0, 0.04); flex-shrink: 0;
}
.arrow {
font-size: 28rpx;
font-weight: 700;
} }
@keyframes slideUp { @keyframes slideUp {
from { from { transform: translateY(20rpx); opacity: 0; }
transform: translateY(40rpx); to { transform: translateY(0); opacity: 1; }
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
} }
</style> </style>

View File

@ -81,13 +81,17 @@ const groupedResults = computed(() => {
const arr = Array.isArray(props.results) ? props.results : [] const arr = Array.isArray(props.results) ? props.results : []
arr.forEach(item => { arr.forEach(item => {
const key = item.title || item.name || '神秘奖品' // 使reward_idkey
const rewardId = item.reward_id || item.rewardId || item.id
const key = rewardId != null ? `rid_${rewardId}` : (item.title || item.name || '神秘奖品')
if (map.has(key)) { if (map.has(key)) {
map.get(key).quantity++ map.get(key).quantity++
} else { } else {
map.set(key, { map.set(key, {
title: key, title: item.title || item.name || '神秘奖品',
image: item.image || item.img || item.pic || '', image: item.image || item.img || item.pic || '',
reward_id: rewardId,
quantity: 1 quantity: 1
}) })
} }

View File

@ -54,6 +54,12 @@
"navigationBarTitleText": "我的道具卡" "navigationBarTitleText": "我的道具卡"
} }
}, },
{
"path": "pages/invite/landing",
"style": {
"navigationBarTitleText": "好友邀请"
}
},
{ {
"path": "pages/invites/index", "path": "pages/invites/index",
"style": { "style": {
@ -158,12 +164,6 @@
"navigationBarBackgroundColor": "#000000", "navigationBarBackgroundColor": "#000000",
"navigationBarTextStyle": "white" "navigationBarTextStyle": "white"
} }
},
{
"path": "pages/register/register",
"style": {
"navigationBarTitleText": ""
}
} }
], ],
"tabBar": { "tabBar": {

View File

@ -207,8 +207,8 @@ function openPayment(count) {
return return
} }
paymentVisible.value = true paymentVisible.value = true
fetchPropCards() //
fetchCoupons() Promise.all([fetchPropCards(), fetchCoupons()])
} }
async function onPaymentConfirm(data) { async function onPaymentConfirm(data) {
@ -291,6 +291,7 @@ function mapResultsToFlipItems(resultRes, poolRewards) {
const fromName = !fromId && rewardName ? poolArr.find(x => x?.title === rewardName) : null const fromName = !fromId && rewardName ? poolArr.find(x => x?.title === rewardName) : null
const it = fromId || fromName || null const it = fromId || fromName || null
return { return {
reward_id: rewardId, // reward_id
title: rewardName || it?.title || '奖励', title: rewardName || it?.title || '奖励',
image: d.image || it?.image || d.img || d.pic || d.product_image || '' image: d.image || it?.image || d.img || d.pic || d.product_image || ''
} }
@ -352,7 +353,19 @@ async function onMachineDraw(count) {
}) })
}) })
const resultRes = await getLotteryResult(orderNo) //
let resultRes = await getLotteryResult(orderNo)
let pollCount = 0
const maxPolls = 15 // 15230
while (resultRes?.status === 'paid_waiting' &&
resultRes?.completed < resultRes?.count &&
pollCount < maxPolls) {
await new Promise(r => setTimeout(r, resultRes?.nextPollMs || 2000))
resultRes = await getLotteryResult(orderNo)
pollCount++
}
const items = mapResultsToFlipItems(resultRes, currentIssueRewards.value) const items = mapResultsToFlipItems(resultRes, currentIssueRewards.value)
drawResults.value = items drawResults.value = items
@ -367,16 +380,17 @@ async function onMachineDraw(count) {
// ============ ============ // ============ ============
onLoad(async (opts) => { onLoad(async (opts) => {
const id = opts?.id || '' const id = opts?.id || ''
if (id) { if (!id) return
activityId.value = id activityId.value = id
await fetchDetail() //
await Promise.all([fetchDetail(), fetchIssues()])
setNavigationTitle('无限赏') setNavigationTitle('无限赏')
await fetchIssues() //
await fetchRewardsForIssues(issues.value) await fetchRewardsForIssues(issues.value)
//
if (currentIssueId.value) { if (currentIssueId.value) {
fetchWinRecords(id, currentIssueId.value) fetchWinRecords(id, currentIssueId.value)
} }
}
}) })
// //

View File

@ -298,16 +298,17 @@ function onPaymentSuccess(payload) {
onLoad(async (opts) => { onLoad(async (opts) => {
startNowTicker() startNowTicker()
const id = opts?.id || '' const id = opts?.id || ''
if (id) { if (!id) return
activityId.value = id activityId.value = id
await fetchDetail() //
await Promise.all([fetchDetail(), fetchIssues()])
setNavigationTitle('一番赏') setNavigationTitle('一番赏')
await fetchIssues() //
await fetchRewardsForIssues(issues.value) await fetchRewardsForIssues(issues.value)
//
if (currentIssueId.value) { if (currentIssueId.value) {
fetchWinRecords(id, currentIssueId.value) fetchWinRecords(id, currentIssueId.value)
} }
}
}) })
onUnload(() => { onUnload(() => {

531
pages/invite/landing.vue Normal file
View File

@ -0,0 +1,531 @@
<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/agreement/user' }) }
function toPurchaseAgreement() { uni.navigateTo({ url: '/pages/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>

View File

@ -61,10 +61,6 @@
<button class="btn test-login-btn" @click="handleTestLogin"> <button class="btn test-login-btn" @click="handleTestLogin">
<text class="btn-text">测试账号登录 (Dev)</text> <text class="btn-text">测试账号登录 (Dev)</text>
</button> </button>
<view class="register-link">
<text class="register-text" @click="goToRegister">没有账号<text class="highlight">立即注册</text></text>
</view>
</view> </view>
<!-- #endif --> <!-- #endif -->
@ -126,8 +122,6 @@ function toggleAgreement() {
agreementChecked.value = !agreementChecked.value agreementChecked.value = !agreementChecked.value
} }
function goToRegister() { uni.navigateTo({ url: '/pages/register/register' }) }
function handleLogin() { function handleLogin() {
if (!agreementChecked.value) { if (!agreementChecked.value) {
uni.showToast({ title: '请先阅读并同意协议', icon: 'none' }) uni.showToast({ title: '请先阅读并同意协议', icon: 'none' })

View File

@ -31,7 +31,7 @@
<!-- 数据统计栏 --> <!-- 数据统计栏 -->
<view class="stats-row"> <view class="stats-row">
<view class="stat-item" @click="toPointsPage"> <view class="stat-item" @click="toPointsPage">
<text class="stat-num">{{ pointsBalance || 0 }}</text> <text class="stat-num">{{ formatPoints(pointsBalance) }}</text>
<text class="stat-label">积分</text> <text class="stat-label">积分</text>
</view> </view>
<view class="stat-divider"></view> <view class="stat-divider"></view>
@ -190,7 +190,7 @@
</view> </view>
<view class="point-right"> <view class="point-right">
<text class="point-amount" :class="{ 'positive': Number(item.points) > 0, 'negative': Number(item.points) < 0 }"> <text class="point-amount" :class="{ 'positive': Number(item.points) > 0, 'negative': Number(item.points) < 0 }">
{{ Number(item.points) > 0 ? '+' : '' }}{{ item.points }} {{ Number(item.points) > 0 ? '+' : '' }}{{ formatPoints(item.points) }}
</text> </text>
</view> </view>
</view> </view>
@ -516,7 +516,7 @@ export default {
try { uni.setStorageSync('inviter_code', v) } catch (_) {} try { uni.setStorageSync('inviter_code', v) } catch (_) {}
const token = uni.getStorageSync('token') const token = uni.getStorageSync('token')
if (!token) { if (!token) {
uni.navigateTo({ url: `/pages/register/register?invite_code=${encodeURIComponent(v)}` }) uni.navigateTo({ url: '/pages/login/index' })
} }
} }
}, },
@ -526,17 +526,17 @@ export default {
onShareAppMessage() { onShareAppMessage() {
const inviteCode = uni.getStorageSync('invite_code') || (uni.getStorageSync('user_info') || {}).invite_code || '' const inviteCode = uni.getStorageSync('invite_code') || (uni.getStorageSync('user_info') || {}).invite_code || ''
return { return {
title: '给你一个宝藏应用,快来', title: '🎁 好友邀请你一起玩,快来领福利',
path: inviteCode ? `/pages/register/register?invite_code=${inviteCode}` : '/pages/register/register', path: inviteCode ? `/pages/invite/landing?invite_code=${inviteCode}` : '/pages/invite/landing',
imageUrl: '/static/logo.png' imageUrl: '/static/share_invite.png'
} }
}, },
onShareTimeline() { onShareTimeline() {
const inviteCode = uni.getStorageSync('invite_code') || (uni.getStorageSync('user_info') || {}).invite_code || '' const inviteCode = uni.getStorageSync('invite_code') || (uni.getStorageSync('user_info') || {}).invite_code || ''
return { return {
title: '给你一个宝藏应用,快来', title: '🎁 好友邀请你一起玩,快来领福利',
query: inviteCode ? `invite_code=${inviteCode}` : '', query: inviteCode ? `invite_code=${inviteCode}` : '',
imageUrl: '/static/logo.png' imageUrl: '/static/share_invite.png'
} }
}, },
methods: { methods: {
@ -544,6 +544,12 @@ export default {
const v = this.inviteCode || uni.getStorageSync('invite_code') || (uni.getStorageSync('user_info') || {}).invite_code || '' const v = this.inviteCode || uni.getStorageSync('invite_code') || (uni.getStorageSync('user_info') || {}).invite_code || ''
return String(v || '').trim() return String(v || '').trim()
}, },
formatPoints(v) {
const n = Number(v) || 0
if (n === 0) return '0'
const f = n / 100
return Number.isInteger(f) ? String(f) : f.toFixed(2).replace(/\.?0+$/, '')
},
copyInviteCode() { copyInviteCode() {
const code = this.getInviteCode() const code = this.getInviteCode()
if (!code) { if (!code) {
@ -559,7 +565,7 @@ export default {
}, },
getInviteSharePath() { getInviteSharePath() {
const code = this.getInviteCode() const code = this.getInviteCode()
return code ? `/pages/register/register?invite_code=${encodeURIComponent(code)}` : '/pages/register/register' return code ? `/pages/invite/landing?invite_code=${encodeURIComponent(code)}` : '/pages/invite/landing'
}, },
normalizePointsBalance(v) { normalizePointsBalance(v) {
if (v && typeof v === 'object') { if (v && typeof v === 'object') {

View File

@ -33,7 +33,7 @@
<view class="record-main"> <view class="record-main">
<view class="record-title">{{ getActionText(item.action) || item.title || item.reason || '积分变更' }}</view> <view class="record-title">{{ getActionText(item.action) || item.title || item.reason || '积分变更' }}</view>
<view class="record-amount" :class="{ inc: (item.points || 0) > 0, dec: (item.points || 0) < 0 }"> <view class="record-amount" :class="{ inc: (item.points || 0) > 0, dec: (item.points || 0) < 0 }">
{{ (item.points ?? 0) > 0 ? '+' : '' }}{{ item.points ?? 0 }} {{ (item.points ?? 0) > 0 ? '+' : '' }}{{ formatPoints(item.points) }}
</view> </view>
</view> </view>
<view class="record-footer"> <view class="record-footer">
@ -65,6 +65,12 @@ const error = ref('')
const page = ref(1) const page = ref(1)
const pageSize = ref(20) const pageSize = ref(20)
const hasMore = ref(true) const hasMore = ref(true)
function formatPoints(v) {
const n = Number(v) || 0
if (n === 0) return '0'
const f = n / 100
return Number.isInteger(f) ? String(f) : f.toFixed(2).replace(/\.?0+$/, '')
}
function formatTime(t) { function formatTime(t) {
if (!t) return '' if (!t) return ''

View File

@ -1,172 +0,0 @@
<template>
<view class="container">
<image class="logo" src="/static/logo.png" mode="widthFix"></image>
<view class="title">注册新账号</view>
<view class="form">
<view class="input-row">
<text class="label">邀请码</text>
<input type="text" v-model="inviteCode" class="input-field" placeholder="请输入邀请码" />
</view>
<view class="input-row">
<text class="label">账号</text>
<input type="text" v-model="account" class="input-field" placeholder="请输入账号" />
</view>
<view class="input-row">
<text class="label">密码</text>
<input type="password" v-model="password" class="input-field" placeholder="请输入密码" />
</view>
<view class="input-row">
<text class="label">确认密码</text>
<input type="password" v-model="confirmPassword" class="input-field" placeholder="请再次输入密码" />
</view>
<button class="btn submit-btn" :disabled="loading" @click="onRegister">注册</button>
</view>
<view class="login-link">
<text @tap="goLogin">已有账号去登录</text>
</view>
<view v-if="error" class="error">{{ error }}</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
const account = ref('')
const password = ref('')
const confirmPassword = ref('')
const inviteCode = ref('')
const loading = ref(false)
const error = ref('')
onLoad((opts) => {
const code = (opts && (opts.invite_code || opts.inviteCode)) || ''
const v = String(code || uni.getStorageSync('inviter_code') || '').trim()
if (v) {
inviteCode.value = v
try { uni.setStorageSync('inviter_code', v) } catch (_) {}
}
})
function goLogin() {
uni.navigateBack()
}
function onRegister() {
if (!account.value || !password.value) {
uni.showToast({ title: '请填写完整', icon: 'none' })
return
}
if (password.value !== confirmPassword.value) {
uni.showToast({ title: '两次密码不一致', icon: 'none' })
return
}
// TODO: API
uni.showToast({ title: '注册功能开发中', icon: 'none' })
}
</script>
<style scoped>
/* ============================================
柯大鸭潮玩 - 注册页面
============================================ */
.container {
min-height: 100vh;
padding: 60rpx 40rpx;
display: flex;
flex-direction: column;
align-items: center;
background: linear-gradient(180deg, #FFF8F3 0%, #FFE8D1 50%, #FFDAB9 100%);
}
.logo {
width: 160rpx;
height: 160rpx;
margin-top: 80rpx;
margin-bottom: 32rpx;
border-radius: 32rpx;
box-shadow: 0 12rpx 36rpx rgba(255, 107, 53, 0.2);
}
.title {
font-size: 40rpx;
font-weight: 700;
color: #1F2937;
margin-bottom: 48rpx;
}
.form {
width: 100%;
max-width: 600rpx;
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(20px);
border-radius: 32rpx;
padding: 40rpx;
box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.08);
}
.input-row {
display: flex;
align-items: center;
margin-bottom: 24rpx;
background: #F9FAFB;
border-radius: 16rpx;
padding: 8rpx 24rpx;
border: 2rpx solid #E5E7EB;
}
.label {
width: 140rpx;
font-size: 28rpx;
font-weight: 500;
color: #6B7280;
}
.input-field {
flex: 1;
height: 80rpx;
border: none;
background: transparent;
font-size: 28rpx;
color: #1F2937;
}
.submit-btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
margin-top: 32rpx;
background: linear-gradient(135deg, #FF9F43, #FF6B35) !important;
color: #FFFFFF !important;
border: none;
border-radius: 44rpx;
font-size: 32rpx;
font-weight: 600;
box-shadow: 0 8rpx 24rpx rgba(255, 107, 53, 0.35);
}
.submit-btn:active {
transform: scale(0.97);
}
.login-link {
margin-top: 32rpx;
font-size: 26rpx;
color: #FF9F43;
font-weight: 500;
}
.error {
margin-top: 24rpx;
color: #EF4444;
font-size: 26rpx;
text-align: center;
}
</style>

BIN
static/share_invite.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 518 KiB

View File

@ -22,7 +22,6 @@ function handleAuthExpired() {
export function request({ url, method = 'GET', data = {}, header = {} }) { export function request({ url, method = 'GET', data = {}, header = {} }) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const finalHeader = { ...buildDefaultHeaders(), ...header } const finalHeader = { ...buildDefaultHeaders(), ...header }
try { console.log('HTTP request', method, url, 'data', data, 'headers', finalHeader) } catch (e) { }
uni.request({ uni.request({
url: BASE_URL + url, url: BASE_URL + url,
method, method,
@ -31,7 +30,6 @@ export function request({ url, method = 'GET', data = {}, header = {} }) {
timeout: 15000, timeout: 15000,
success: (res) => { success: (res) => {
const code = res.statusCode const code = res.statusCode
try { console.log('HTTP response', method, url, 'status', code, 'body', res.data) } catch (e) { }
if (code >= 200 && code < 300) { if (code >= 200 && code < 300) {
const body = res.data const body = res.data
resolve(body && body.data !== undefined ? body.data : body) resolve(body && body.data !== undefined ? body.data : body)
@ -47,7 +45,6 @@ export function request({ url, method = 'GET', data = {}, header = {} }) {
} }
}, },
fail: (err) => { fail: (err) => {
try { console.error('HTTP fail', method, url, 'err', err) } catch (e) { }
reject(err) reject(err)
} }
}) })