bindbox-mini/pages/mine/index.vue

746 lines
33 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="wrap">
<!-- 头部区域 -->
<view class="header-section">
<view class="user-info">
<image class="avatar" :src="avatar || '/static/logo.png'" mode="aspectFill"></image>
<view class="user-meta">
<view class="name-row">
<text class="nickname">{{ nickname || '未登录' }}</text>
<view class="level-badge" v-if="nickname">
<image class="level-icon" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNGRjZCMDAiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48cGF0aCBkPSJNNiAzSDE4TDIxIDlMOSAyMSAzIDlMNiAzWiIgLz48L3N2Zz4=" mode="aspectFit"></image>
<text class="level-text">Lv1 青铜</text>
</view>
</view>
<view class="progress-container" v-if="nickname">
<view class="progress-bar">
<view class="progress-fill" style="width: 20%;"></view>
</view>
<text class="progress-text">100/5000 升级Lv2</text>
</view>
<view class="userid" v-else>ID: {{ userId || '-' }}</view>
</view>
<view class="join-btn" @click="handleJoin" v-if="!nickname">立即登录</view>
<view class="join-btn" @click="signCheckin" v-else>立即签到</view>
</view>
<!-- 数据统计栏 (Modified: Points / Coupons / Item Cards) -->
<view class="stats-row">
<view class="stat-item" @click="showPointsPopup">
<text class="stat-num">{{ pointsBalance || 0 }}</text>
<text class="stat-label">积分</text>
</view>
<view class="stat-item" @click="showCouponsPopup">
<text class="stat-num">{{ stats.coupon_count || 0 }}</text>
<text class="stat-label">优惠券</text>
</view>
<view class="stat-item" @click="showItemCardsPopup">
<text class="stat-num">{{ stats.item_card_count || 0 }}</text>
<text class="stat-label">道具卡</text>
</view>
</view>
</view>
<!-- 邀请Banner (Relocated & Re-designed) -->
<view class="invite-banner">
<view class="invite-info">
<view class="invite-tag">好礼相送</view>
<view class="invite-title">邀请好友送好礼</view>
<view class="invite-desc">全新版本等你来玩,奖励拿到手软</view>
</view>
<view class="invite-action">
<text class="invite-btn-text">立即邀请</text>
<image class="invite-arrow" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiMxQTFBMUEiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48cGF0aCBkPSc5IDE4bDYtNi02LTYnIC8+PC9zdmc+" mode="aspectFit"></image>
</view>
<image class="invite-bg-icon" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI2NCIgaGVpZ2h0PSI2NCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiMxQTFBMUEiIHN0cm9rZS13aWR0aD0iMSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48cGF0aCBkPSJNMjEgMTF2OGEyIDIgMCAwIDEtMiAySDVhMiAyIDAgMCAxLTItMnYtOGw5LTYgOSA2eiIgLz48L3N2Zz4=" mode="aspectFit"></image>
</view>
<!-- 我的订单 -->
<view class="card-section">
<view class="section-title">我的订单</view>
<view class="grid-row">
<view class="grid-item" @click="toOrders('pending')">
<image class="grid-icon-img" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0OCIgaGVpZ2h0PSI0OCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNGRjZCMDAiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik0yMSA4YTIgMiAwIDAgMC0xLTEuNzNMNTIuMjdhMiAyIDAgMCAwLTIgMGwtOCA0LjczQTIgMiAwIDAgMCA0IDhoMTZ6TTQgOGwyIDEyaDEybDItMTJNNCA4bDgtNCA4IDQiIC8+PC9zdmc+" mode="aspectFit"></image>
<text class="grid-label">盒柜</text>
</view>
<view class="grid-item" @click="toOrders('pending')">
<image class="grid-icon-img" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0OCIgaGVpZ2h0PSI0OCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNGRjZCMDAiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxyZWN0IHg9IjEiIHk9IjQiIHdpZHRoPSIyMiIgaGVpZ2h0PSIxNiIgcng9IjIiIHJ5PSIyIj48L3JlY3Q+PGxpbmUgeDE9IjEiIHkxPSIxMCIgeDI9IjIzIiB5Mj0iMTAiPjwvbGluZT48L3N2Zz4=" mode="aspectFit"></image>
<text class="grid-label">待付款</text>
</view>
<view class="grid-item" @click="toOrders('pending')">
<image class="grid-icon-img" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0OCIgaGVpZ2h0PSI0OCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNGRjZCMDAiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik0xMiAyMmg1LjlhMiAyIDAgMCAwIDEuOS0xLjQybDMuMTMtMTEuMDlhMiAyIDAgMCAwLTEuOS0yLjUxaC00LjE2IiAvPjxwYXRoIGQ9Ik0xOC40MiA5aC02LjMyIiAvPjxwYXRoIGQ9Ik0xNSA2VjNhMSAxIDAgMCAwLTEtMUg2YTEgMSAwIDAgMC0xIDF2MTQiIC8+PHBhdGggZD0iTTkgMTNoNyIgLz48L3N2Zz4=" mode="aspectFit"></image>
<text class="grid-label">待发货</text>
</view>
<view class="grid-item" @click="toOrders('completed')">
<image class="grid-icon-img" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0OCIgaGVpZ2h0PSI0OCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNGRjZCMDAiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxyZWN0IHg9IjEiIHk9IjMiIHdpZHRoPSIxNSIgaGVpZ2h0PSIxMyIgcng9IjIiIHJ5PSIyIj48L3JlY3Q+PHBvbHlsaW5lIHBvaW50cz0iMTYgOCAyMCA4IDIwIDIxIDIgMjEgMiAxNiA2IDE2IiAvPjwvc3ZnPg==" mode="aspectFit"></image>
<text class="grid-label">已发货</text>
</view>
</view>
</view>
<!-- 功能菜单 -->
<view class="card-section">
<view class="grid-menu">
<view class="menu-item" @click="showCouponsPopup">
<image class="menu-icon-img" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0OCIgaGVpZ2h0PSI0OCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiMzMzMiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik0yMSA2SDNhMiAyIDAgMCAwLTIgMnY4YTIgMiAwIDAgMCAyIDJoMThhMiAyIDAgMCAwIDItMnYtOGEyIDIgMCAwIDAtMi0yWiIgLz48cGF0aCBkPSJNNiAxMnYtMiIgLz48cGF0aCBkPSJNNiAxNnYtMiIgLz48cGF0aCBkPSJNMTYgNnYxMiIgLz48L3N2Zz4=" mode="aspectFit"></image>
<text class="menu-label">优惠券</text>
</view>
<view class="menu-item" @click="showItemCardsPopup">
<image class="menu-icon-img" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0OCIgaGVpZ2h0PSI0OCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiMzMzMiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxyZWN0IHg9IjMiIHk9IjMiIHdpZHRoPSIxOCIgaGVpZ2h0PSIxOCIgcng9IjIiIHJ5PSIyIj48L3JlY3Q+PGNpcmNsZSBjeD0iMTIiIGN5PSIxMiIgcj0iNCIgLz48cGF0aCBkPSJNMjEgMTVsLTUuNDMtMy4yM2EyIDIgMCAwIDAtMS44NCAwbC0yLjMxIDEuMzciIC8+PC9zdmc+" mode="aspectFit"></image>
<text class="menu-label">道具卡</text>
</view>
<view class="menu-item">
<image class="menu-icon-img" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0OCIgaGVpZ2h0PSI0OCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiMzMzMiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik02IDJMNCA2djEyYTIgMiAwIDAgMCAyIDJoMTJhMiAyIDAgMCAwIDItMlY2bC0yLTRINlpNOSA2djRCMyA1IDAgMCAwIDE1IDEweiIgLz48L3N2Zz4=" mode="aspectFit"></image>
<text class="menu-label">赏包</text>
</view>
<view class="menu-item" @click="toAddresses"> <!-- 此处暂用 invite_log 占位 -->
<image class="menu-icon-img" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0OCIgaGVpZ2h0PSI0OCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiMzMzMiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik0xNiA0aDJhMiAyIDAgMCAxIDIgMnYxNGEyIDIgMCAwIDEtMiAySDZ4MiAyIDAgMCAxLTItMnYtMTQiIC8+PHJlY3QgeD0iOCIgeT0iMiIgd2lkdGg9IjgiIGhlaWdodD0iNCIgcng9IjEiIHJ5PSIyIj48L3JlY3Q+PC9zdmc+" mode="aspectFit"></image>
<text class="menu-label">邀请记录</text>
</view>
<view class="menu-item" @click="toAddresses">
<image class="menu-icon-img" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0OCIgaGVpZ2h0PSI0OCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiMzMzMiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik0yMSAxMGMwIDctOSAxMy05IDEzcy05LTYtOS0xM2E5IDkgMCAwIDEgMTggMHoiIC8+PGNpcmNsZSBjeD0iMTIiIGN5PSIxMCIgcj0iMyIgLz48L3N2Zz4=" mode="aspectFit"></image>
<text class="menu-label">收货地址</text>
</view>
<view class="menu-item" @click="toHelp">
<image class="menu-icon-img" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0OCIgaGVpZ2h0PSI0OCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiMzMzMiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxjaXJjbGUgY3g9IjEyIiBjeT0iMTIiIHI9IjEwIiAvPjxwYXRoIGQ9Ik05LjA5IDlhMyAzIDAgMCAxIDUuODMgMS43OGMwIC4zOC0uNDIuNzktLjYzIDEuMDMtLjU5LjcxLTEuMTggMS40My0xLjE4IDIuMTkiIC8+PGxpbmUgeDE9IjEyIiB5MT0iMTciIHgyPSIxMiIgeTI9“MTciPjwvbGluZT48L3N2Zz4=" mode="aspectFit"></image>
<text class="menu-label">帮助中心</text>
</view>
</view>
</view>
<!-- 弹窗组件保持原有逻辑 -->
<!-- 积分明细弹窗 -->
<view class="popup-mask" v-if="pointsVisible" @tap="closePointsPopup">
<view class="popup-content" @tap.stop>
<view class="popup-header">
<text class="popup-title">积分明细</text>
<text class="close-btn" @tap="closePointsPopup">×</text>
</view>
<scroll-view scroll-y class="points-list" @scrolltolower="loadMorePoints">
<view v-if="pointsLoading && pointsList.length === 0" class="status-text">加载中...</view>
<view v-else-if="!pointsList || pointsList.length === 0" class="status-text">暂无积分记录</view>
<view v-for="(item, index) in pointsList" :key="index" class="point-item">
<view class="point-left">
<text class="point-desc">{{ getActionText(item.action) }}</text>
<text class="point-time">{{ formatDate(item.created_at) }}</text>
</view>
<view class="point-right">
<text class="point-amount" :class="{ 'positive': Number(item.points) > 0, 'negative': Number(item.points) < 0 }">
{{ Number(item.points) > 0 ? '+' : '' }}{{ item.points }}
</text>
</view>
</view>
<view v-if="pointsLoading && pointsList.length > 0" class="loading-more">加载更多...</view>
<view v-if="!pointsHasMore && pointsList.length > 0" class="no-more">没有更多了</view>
</scroll-view>
</view>
</view>
<!-- 优惠券弹窗 -->
<view class="popup-mask" v-if="couponsVisible" @tap="closeCouponsPopup">
<view class="popup-content" @tap.stop>
<view class="popup-header">
<text class="popup-title">我的优惠券</text>
<text class="close-btn" @tap="closeCouponsPopup">×</text>
</view>
<view class="popup-tabs">
<view class="popup-tab" :class="{ active: couponsTab === 0 }" @tap="switchCouponsTab(0)">未使用</view>
<view class="popup-tab" :class="{ active: couponsTab === 1 }" @tap="switchCouponsTab(1)">已使用</view>
<view class="popup-tab" :class="{ active: couponsTab === 2 }" @tap="switchCouponsTab(2)">去兑换</view>
</view>
<view v-if="couponsTab === 2" class="redeem-container">
<input class="redeem-input" v-model="redeemCode" placeholder="请输入兑换码" />
<button class="redeem-btn" @tap="handleRedeem">立即兑换</button>
</view>
<scroll-view v-else scroll-y class="points-list" @scrolltolower="loadMoreCoupons">
<view v-if="couponsLoading && couponsList.length === 0" class="status-text">加载中...</view>
<view v-else-if="!couponsList || couponsList.length === 0" class="status-text">暂无优惠券</view>
<view v-for="(item, index) in couponsList" :key="index" class="coupon-item">
<view class="coupon-left">
<text class="coupon-name">{{ item.name || '优惠券' }}</text>
<text class="coupon-desc">{{ item.rules || item.description || '' }}</text>
<text class="coupon-time">有效期至:{{ formatDate(item.valid_end || item.end_time) }}</text>
</view>
<view class="coupon-right">
<view v-if="couponsTab === 0 && (item.remaining !== undefined && item.remaining !== null)" class="coupon-amount-wrapper">
<text class="symbol">¥</text>
<text class="amount-value">{{ (Number(item.remaining || 0) / 100).toFixed(2) }}</text>
</view>
<text class="coupon-status">{{ couponsTab === 0 ? '未使用' : '已使用/过期' }}</text>
</view>
</view>
<view v-if="couponsLoading && couponsList.length > 0" class="loading-more">加载更多...</view>
<view v-if="!couponsHasMore && couponsList.length > 0" class="no-more">没有更多了</view>
</scroll-view>
</view>
</view>
<!-- 道具卡弹窗 -->
<view class="popup-mask" v-if="itemCardsVisible" @tap="closeItemCardsPopup">
<view class="popup-content" @tap.stop>
<view class="popup-header">
<text class="popup-title">我的道具卡</text>
<text class="close-btn" @tap="closeItemCardsPopup">×</text>
</view>
<scroll-view scroll-y class="points-list">
<view v-if="itemCardsLoading && itemCardsList.length === 0" class="status-text">加载中...</view>
<view v-else-if="!itemCardsList || itemCardsList.length === 0" class="status-text">暂无道具卡</view>
<view v-for="(item, index) in itemCardsList" :key="index" class="coupon-item">
<view class="coupon-left">
<text class="coupon-name">{{ item.name || item.title || '道具卡' }}</text>
<text class="coupon-desc">{{ item.description || item.rules || '' }}</text>
<text class="coupon-time" v-if="item.valid_end || item.end_time">有效期至:{{ formatDate(item.valid_end || item.end_time) }}</text>
</view>
<view class="coupon-right">
<text class="coupon-status">数量:{{ item.remaining ?? item.count ?? 1 }}</text>
</view>
</view>
</scroll-view>
</view>
</view>
<!-- 任务中心弹窗 -->
<view class="popup-mask" v-if="tasksVisible" @tap="closeTasksPopup">
<view class="popup-content" @tap.stop>
<view class="popup-header">
<text class="popup-title">任务中心</text>
<text class="close-btn" @tap="closeTasksPopup">×</text>
</view>
<scroll-view scroll-y class="points-list" @scrolltolower="loadMoreTasks">
<view v-if="tasksLoading && tasksList.length === 0" class="status-text">加载中...</view>
<view v-else-if="!tasksList || tasksList.length === 0" class="status-text">暂无任务</view>
<view v-for="(task, idx) in tasksList" :key="task.id || idx" class="task-item">
<view class="task-left">
<text class="task-name">{{ task.name || '任务' }}</text>
<text class="task-desc">{{ task.description || '' }}</text>
<view class="reward-list" v-if="Array.isArray(task.rewards) && task.rewards.length">
<text class="reward-label">奖励</text>
<view class="reward-tags">
<view class="reward-item" v-for="(rw, ri) in task.rewards" :key="ri">
<text class="reward-type">{{ rw.reward_type || '未知' }}</text>
<text class="reward-qty">×{{ rw.quantity || 0 }}</text>
</view>
</view>
</view>
</view>
<view class="task-right">
<text class="task-status">{{ formatStatus(task.status) }}</text>
</view>
</view>
</scroll-view>
</view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { onShow, onLoad, onShareAppMessage } from '@dcloudio/uni-app'
import { getUserStats, getPointsBalance, getUserPoints, getUserCoupons, redeemCoupon, getItemCards, getTasks } from '../../api/appUser'
const avatar = ref(uni.getStorageSync('avatar') || '')
const nickname = ref(uni.getStorageSync('nickname') || '')
const userId = ref(uni.getStorageSync('user_id') || '')
const phoneNumber = ref(uni.getStorageSync('phone_number') || '')
const inviteCode = ref(uni.getStorageSync('invite_code') || (uni.getStorageSync('user_info') || {}).invite_code || '')
const pointsBalance = ref(uni.getStorageSync('points_balance') || 0)
const stats = ref(uni.getStorageSync('user_stats') || {})
const loading = ref(false)
const error = ref('')
// 积分弹窗相关状态
const pointsVisible = ref(false)
const pointsList = ref([])
const pointsLoading = ref(false)
const pointsPage = ref(1)
const pointsPageSize = ref(20)
const pointsHasMore = ref(true)
// 优惠券弹窗相关状态
const couponsVisible = ref(false)
const couponsTab = ref(0) // 0: 未使用, 1: 已使用, 2: 兑换
const couponsList = ref([])
const couponsLoading = ref(false)
const couponsPage = ref(1)
const couponsHasMore = ref(true)
const redeemCode = ref('')
// 道具卡弹窗相关状态
const itemCardsVisible = ref(false)
const itemCardsList = ref([])
const itemCardsLoading = ref(false)
const tasksVisible = ref(false)
const tasksList = ref([])
const tasksLoading = ref(false)
const tasksPage = ref(1)
const tasksPageSize = ref(20)
const tasksHasMore = ref(true)
async function refresh() {
const token = uni.getStorageSync('token')
// 允许未登录状态浏览基本页面结构,用户信息显示“未登录”
if (!token) {
nickname.value = ''
avatar.value = ''
return
}
const user_id = uni.getStorageSync('user_id')
loading.value = true
try {
const s = await getUserStats(user_id)
stats.value = s || {}
uni.setStorageSync('user_stats', stats.value)
const b = await getPointsBalance(user_id)
const balance = b && b.balance !== undefined ? b.balance : b
pointsBalance.value = balance || 0
uni.setStorageSync('points_balance', pointsBalance.value)
} catch (e) {
console.error(e)
} finally {
loading.value = false
}
}
function handleJoin() {
const token = uni.getStorageSync('token')
if (!token) {
uni.navigateTo({ url: '/pages/login/index' })
} else {
uni.showToast({ title: '已登录', icon: 'none' })
}
}
function signCheckin() {
uni.showToast({ title: '签到功能开发中', icon: 'none' })
}
function toOrders(status) {
const s = status === 'completed' ? 'completed' : 'pending' // 简单映射
uni.navigateTo({ url: `/pages/orders/index?status=${s}` })
}
function toAddresses() {
uni.navigateTo({ url: '/pages/address/index' })
}
function toHelp() {
uni.navigateTo({ url: '/pages/help/index' })
}
// 积分弹窗逻辑
function showPointsPopup() {
pointsVisible.value = true
if (pointsList.value.length === 0) {
pointsPage.value = 1
pointsHasMore.value = true
loadPoints()
}
}
function closePointsPopup() { pointsVisible.value = false }
async function loadPoints() {
if (pointsLoading.value) return
pointsLoading.value = true
const user_id = uni.getStorageSync('user_id')
try {
const res = await getUserPoints(user_id, pointsPage.value, pointsPageSize.value)
let list = []
let total = 0
if (res && Array.isArray(res.list)) { list = res.list; total = res.total || 0 }
else if (res && Array.isArray(res.data)) { list = res.data; total = res.total || 0 }
else if (Array.isArray(res)) { list = res; total = res.length }
if (pointsPage.value === 1) pointsList.value = list
else pointsList.value = [...pointsList.value, ...list]
if (list.length < pointsPageSize.value || (pointsPage.value * pointsPageSize.value >= total && total > 0)) pointsHasMore.value = false
else pointsPage.value += 1
if (list.length === 0 && pointsPage.value === 1) pointsHasMore.value = false
} catch (e) {
uni.showToast({ title: '加载失败', icon: 'none' })
} finally {
pointsLoading.value = false
}
}
function loadMorePoints() { if (pointsHasMore.value && !pointsLoading.value) loadPoints() }
// 优惠券弹窗逻辑
function showCouponsPopup() {
couponsVisible.value = true
couponsTab.value = 0
redeemCode.value = ''
couponsList.value = []
couponsPage.value = 1
couponsHasMore.value = true
loadCoupons()
}
function closeCouponsPopup() { couponsVisible.value = false }
function switchCouponsTab(index) {
if (couponsTab.value === index) return
couponsTab.value = index
if (index !== 2) {
couponsList.value = []
couponsPage.value = 1
couponsHasMore.value = true
loadCoupons()
}
}
async function loadCoupons() {
if (couponsLoading.value) return
couponsLoading.value = true
const user_id = uni.getStorageSync('user_id')
try {
const status = couponsTab.value === 0 ? 0 : 1
const res = await getUserCoupons(user_id, status, couponsPage.value, 20)
let list = []
let total = 0
if (res && Array.isArray(res.list)) { list = res.list; total = res.total || 0 }
else if (res && Array.isArray(res.data)) { list = res.data; total = res.total || 0 }
else if (Array.isArray(res)) { list = res; total = res.length }
if (couponsPage.value === 1) couponsList.value = list
else couponsList.value = [...couponsList.value, ...list]
if (list.length < 20 || (total > 0 && couponsList.value.length >= total)) couponsHasMore.value = false
else couponsPage.value += 1
if (list.length === 0 && couponsPage.value === 1) couponsHasMore.value = false
} catch (e) {
uni.showToast({ title: '加载失败', icon: 'none' })
} finally {
couponsLoading.value = false
}
}
function loadMoreCoupons() { if (couponsHasMore.value && !couponsLoading.value && couponsTab.value !== 2) loadCoupons() }
async function handleRedeem() {
if (!redeemCode.value) { uni.showToast({ title: '请输入兑换码', icon: 'none' }); return }
const user_id = uni.getStorageSync('user_id')
try {
await redeemCoupon(user_id, redeemCode.value)
uni.showToast({ title: '兑换成功', icon: 'success' })
redeemCode.value = ''
refresh()
} catch (e) {
uni.showToast({ title: e.message || '兑换失败', icon: 'none' })
}
}
// 道具卡弹窗逻辑
function showItemCardsPopup() {
itemCardsVisible.value = true
loadItemCards()
}
function closeItemCardsPopup() { itemCardsVisible.value = false }
async function loadItemCards() {
if (itemCardsLoading.value) return
itemCardsLoading.value = true
const user_id = uni.getStorageSync('user_id')
try {
const res = await getItemCards(user_id)
let list = []
if (res && Array.isArray(res.list)) list = res.list
else if (res && Array.isArray(res.data)) list = res.data
else if (Array.isArray(res)) list = res
itemCardsList.value = list
} catch (e) {
uni.showToast({ title: '加载失败', icon: 'none' })
} finally {
itemCardsLoading.value = false
}
}
function showTasksPopup() {
tasksVisible.value = true
if (tasksList.value.length === 0) {
tasksPage.value = 1
tasksHasMore.value = true
loadTasks()
}
}
function closeTasksPopup() { tasksVisible.value = false }
async function loadTasks() {
if (tasksLoading.value) return
tasksLoading.value = true
try {
const res = await getTasks(tasksPage.value, tasksPageSize.value)
let list = []
let total = 0
if (res && Array.isArray(res.list)) { list = res.list; total = res.total || 0 }
else if (res && Array.isArray(res.data)) { list = res.data; total = res.total || 0 }
else if (Array.isArray(res)) { list = res; total = res.length }
if (tasksPage.value === 1) tasksList.value = list
else tasksList.value = [...tasksList.value, ...list]
if (list.length < tasksPageSize.value || (tasksPage.value * tasksPageSize.value >= total && total > 0)) tasksHasMore.value = false
else tasksPage.value += 1
} catch (e) {
uni.showToast({ title: '加载失败', icon: 'none' })
} finally {
tasksLoading.value = false
}
}
function loadMoreTasks() { if (tasksHasMore.value && !tasksLoading.value) loadTasks() }
function formatDate(dateStr) {
if (!dateStr) return ''
const date = new Date(dateStr)
if (isNaN(date.getTime())) return dateStr
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
}
function getActionText(action) {
const map = { 'manual_add': '商品兑换积分', 'manual_sub': '系统扣除', 'register': '注册奖励', 'lottery_cost': '抽奖消耗', 'checkin': '签到奖励' }
return map[action] || action || '积分变动'
}
function formatStatus(s) {
const n = Number(s)
if (n === 1) return '进行中'
if (n === 2) return '已结束'
if (n === 0) return '未开始'
return '未知'
}
onShow(() => {
avatar.value = uni.getStorageSync('avatar') || avatar.value
nickname.value = uni.getStorageSync('nickname') || nickname.value
userId.value = uni.getStorageSync('user_id') || userId.value
refresh()
})
onLoad(() => { refresh() })
onShareAppMessage(() => {
return { title: '邀请你一起来加入', path: `/pages/index/index?invite_code=${inviteCode.value}`, imageUrl: '/static/logo.png' }
})
</script>
<style scoped>
/* Page Wrap - Creamy Background */
.wrap {
min-height: 100vh;
background: #F8F5F2; /* Cream background close to reference */
padding-bottom: 40rpx;
}
/* Header Section */
.header-section {
padding: 40rpx 32rpx 32rpx;
background: #F8F5F2;
}
.user-info {
display: flex;
align-items: center;
margin-bottom: 40rpx;
}
.avatar {
width: 120rpx;
height: 120rpx;
border-radius: 50%;
border: 4rpx solid #FFFFFF;
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.1);
background: #eee;
}
.user-meta {
flex: 1;
margin-left: 24rpx;
display: flex;
flex-direction: column;
}
.name-row {
display: flex;
align-items: center;
margin-bottom: 12rpx;
}
.nickname {
font-size: 36rpx;
font-weight: 800;
color: #1A1A1A;
margin-right: 16rpx;
}
.level-badge {
display: flex;
align-items: center;
background: #FFF0E5;
padding: 4rpx 12rpx;
border-radius: 999rpx;
}
.level-icon { width: 24rpx; height: 24rpx; margin-right: 4rpx; }
.level-text { font-size: 22rpx; color: #FF6B00; font-weight: 700; }
.progress-container {
width: 100%;
}
.progress-bar {
height: 8rpx;
background: #EAEAEA;
border-radius: 4rpx;
overflow: hidden;
margin-bottom: 8rpx;
}
.progress-fill {
background: #FFD700;
height: 100%;
border-radius: 4rpx;
}
.progress-text {
font-size: 22rpx;
color: #999;
}
.userid {
font-size: 24rpx;
color: #999;
}
.join-btn {
background: #FFD700;
color: #1A1A1A;
font-size: 26rpx;
font-weight: 700;
padding: 12rpx 32rpx;
border-radius: 999rpx;
border: 2rpx solid #1A1A1A;
box-shadow: 2rpx 2rpx 0 #1A1A1A;
}
.join-btn:active { transform: translate(1rpx, 1rpx); box-shadow: none; }
/* Stats Row */
.stats-row {
display: flex;
justify-content: space-between;
padding: 0 32rpx; /* Increased padding */
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
}
.stat-num {
font-size: 36rpx;
font-weight: 800;
color: #1A1A1A;
margin-bottom: 8rpx;
}
.stat-label {
font-size: 24rpx;
color: #888;
}
/* VIP Banner (Removed) */
/* Invite Banner */
.invite-banner {
margin: 0 32rpx 32rpx;
background: linear-gradient(135deg, #FFEFD5 0%, #FFF5E6 50%, #FFFFFF 100%);
border-radius: 24rpx;
padding: 32rpx;
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
overflow: hidden;
box-shadow: 0 8rpx 24rpx rgba(0,0,0,0.05);
}
.invite-info { z-index: 2; flex: 1; }
.invite-tag { background: #FF6B00; color: #fff; font-size: 20rpx; padding: 4rpx 12rpx; border-radius: 8rpx; display: inline-block; margin-bottom: 8rpx; }
.invite-title { font-size: 32rpx; font-weight: 800; color: #1A1A1A; margin-bottom: 8rpx; }
.invite-desc { font-size: 24rpx; color: #666; }
.invite-action { display: flex; align-items: center; background: #FFD700; padding: 12rpx 24rpx; border-radius: 999rpx; z-index: 2; box-shadow: 0 4rpx 12rpx rgba(255, 215, 0, 0.4); }
.invite-btn-text { font-size: 24rpx; font-weight: 700; color: #1A1A1A; margin-right: 4rpx; }
.invite-arrow { width: 24rpx; height: 24rpx; }
.invite-bg-icon { position: absolute; right: 0; bottom: -10rpx; width: 140rpx; height: 140rpx; opacity: 0.1; transform: rotate(-15deg); pointer-events: none; }
/* Card Section */
.card-section {
background: #FFFFFF;
margin: 0 32rpx 32rpx;
border-radius: 24rpx;
padding: 32rpx 24rpx;
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.03);
}
.section-title {
font-size: 30rpx;
font-weight: 700;
color: #1A1A1A;
margin-bottom: 32rpx;
}
/* Grid Layout */
.grid-row, .grid-menu {
display: flex;
flex-wrap: wrap;
}
.grid-menu {
margin-top: -16rpx; /* Adjust for spacing */
}
.grid-item {
width: 25%;
display: flex;
flex-direction: column;
align-items: center;
}
.menu-item {
width: 25%;
display: flex;
flex-direction: column;
align-items: center;
margin-top: 32rpx;
}
.grid-icon-img, .menu-icon-img {
width: 48rpx;
height: 48rpx;
margin-bottom: 16rpx;
/* background-color: #FFF0E5; */
/* border-radius: 12rpx; */
/* padding: 8rpx; */
/* This can be uncommented for background circles */
}
.grid-label, .menu-label {
font-size: 24rpx;
color: #333;
}
/* Popup Styles (simplified/inherited) */
.popup-mask { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.6); z-index: 999; display: flex; align-items: center; justify-content: center; }
.popup-content { width: 600rpx; max-height: 80vh; background: #FFF; border-radius: 24rpx; padding: 32rpx; display: flex; flex-direction: column; }
.popup-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24rpx; }
.popup-title { font-size: 32rpx; font-weight: 700; }
.close-btn { font-size: 40rpx; padding: 0 10rpx; color: #999; }
.points-list { flex: 1; height: 0; min-height: 400rpx; overflow-y: scroll; }
.status-text { text-align: center; color: #999; padding: 40rpx 0; }
.point-item, .coupon-item { display: flex; justify-content: space-between; padding: 24rpx 0; border-bottom: 1rpx solid #eee; }
.coupon-item { align-items: flex-start; }
.coupon-left { flex: 1; display: flex; flex-direction: column; }
.point-desc, .coupon-name { font-size: 28rpx; color: #333; margin-bottom: 8rpx; }
.task-name { font-size: 30rpx; font-weight: 700; }
.point-time, .coupon-time, .coupon-desc, .task-desc { font-size: 22rpx; color: #999; }
.point-amount { font-size: 32rpx; font-weight: 700; color: #333; }
.positive { color: #ff6b00; }
.coupon-right, .task-right { display: flex; flex-direction: column; align-items: flex-end; justify-content: center; }
.coupon-amount-wrapper { display: flex; align-items: baseline; color: #ff4d4f; }
.symbol { font-size: 24rpx; }
.amount-value { font-size: 40rpx; font-weight: 700; }
.coupon-status, .task-status { font-size: 22rpx; color: #666; margin-top: 8rpx; background: #eee; padding: 4rpx 12rpx; border-radius: 8rpx;}
.popup-tabs { display: flex; margin-bottom: 24rpx; border-bottom: 1rpx solid #eee; }
.popup-tab { flex: 1; text-align: center; padding: 16rpx 0; font-size: 28rpx; color: #666; position: relative; }
.popup-tab.active { color: #ff6b00; font-weight: 700; }
.popup-tab.active::after { content: ''; position: absolute; bottom: 0; left: 50%; transform: translateX(-50%); width: 40rpx; height: 4rpx; background: #ff6b00; border-radius: 2rpx; }
.redeem-container { display: flex; margin-top: 24rpx; }
.redeem-input { flex: 1; background: #f5f5f5; height: 80rpx; border-radius: 12rpx; padding: 0 24rpx; font-size: 28rpx; }
.redeem-btn { margin-left: 16rpx; background: #ff6b00; color: #fff; font-size: 28rpx; height: 80rpx; line-height: 80rpx; padding: 0 32rpx; border-radius: 12rpx; }
.reward-list { margin-top: 8rpx; display: flex; align-items: center; }
.reward-label { font-size: 20rpx; color: #ff6b00; margin-right: 8rpx; background: #FFF0E5; padding: 2rpx 8rpx; border-radius: 6rpx; }
.reward-tags { display: flex; flex-wrap: wrap; gap: 8rpx; }
.reward-item { font-size: 20rpx; color: #666; background: #f0f0f0; padding: 2rpx 8rpx; border-radius: 6rpx; }
</style>