bindbox-mini/pages/mine/index.vue

1809 lines
67 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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="header-section">
<view class="user-info-card glass-card">
<view class="user-main">
<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="title">
<image class="level-icon" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNGRjZCMDAiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48cGF0aCBkPSJNNiAzSDE4TDIxIDlMOSAyMSAzIDlMNiAzWiIgLz48L3N2Zz4=" mode="aspectFit"></image>
<text class="level-text">Lv1 {{ title }}</text>
</view>
</view>
<view class="userid" v-if="userId">ID: {{ userId }}</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>
<!-- 数据统计栏 -->
<view class="stats-row">
<view class="stat-item" @click="toPointsPage">
<text class="stat-num">{{ formatPoints(pointsBalance) }}</text>
<text class="stat-label">积分</text>
</view>
<view class="stat-divider"></view>
<view class="stat-item" @click="toCouponsPage">
<text class="stat-num">{{ stats.coupon_count || 0 }}</text>
<text class="stat-label">优惠券</text>
</view>
<view class="stat-divider"></view>
<view class="stat-item" @click="toItemCardsPage">
<text class="stat-num">{{ stats.item_card_count || 0 }}</text>
<text class="stat-label">道具卡</text>
</view>
</view>
</view>
</view>
<!-- #ifdef MP-WEIXIN -->
<view class="invite-banner glass-card">
<view class="invite-content">
<view class="invite-text-group">
<view class="invite-title-row">
<text class="invite-tag">好礼相送</text>
<text class="invite-title">邀请好友送好礼</text>
</view>
<text class="invite-desc" @tap="copyInviteCode">我的邀请码{{ getInviteCode() || '-' }}点击复制</text>
</view>
<button class="invite-action-btn invite-share-btn" open-type="share">
<image class="invite-share-icon" src="/static/logo.png" mode="aspectFit"></image>
<text>立即邀请</text>
<text class="invite-arrow"></text>
</button>
</view>
<image class="invite-bg-icon" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI2NCIgaGVpZ2h0PSI2NCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNGRjZCMDAiIHN0cm9rZS13aWR0aD0iMSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48cGF0aCBkPSJNMjEgMTF2OGEyIDIgMCAwIDEtMiAySDVhMiAyIDAgMCAwIDEtMiAyVjdsOS02IDkgNnoiIC8+PC9zdmc+" mode="aspectFit"></image>
</view>
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
<view class="invite-banner glass-card" @click="handleInvite">
<view class="invite-content">
<view class="invite-text-group">
<view class="invite-title-row">
<text class="invite-tag">好礼相送</text>
<text class="invite-title">邀请好友送好礼</text>
</view>
<text class="invite-desc" @click.stop="copyInviteCode">我的邀请码{{ getInviteCode() || '-' }}点击复制</text>
</view>
<view class="invite-action-btn">
<text>立即邀请</text>
<text class="invite-arrow"></text>
</view>
</view>
<image class="invite-bg-icon" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI2NCIgaGVpZ2h0PSI2NCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNGRjZCMDAiIHN0cm9rZS13aWR0aD0iMSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48cGF0aCBkPSJNMjEgMTF2OGEyIDIgMCAwIDEtMiAySDVhMiAyIDAgMCAwIDEtMiAyVjdsOS02IDkgNnoiIC8+PC9zdmc+" mode="aspectFit"></image>
</view>
<!-- #endif -->
<!-- 我的订单 -->
<view class="section-card glass-card">
<view class="section-header">
<text class="section-title">我的订单</text>
<text class="section-more" @click="toOrders('all')">全部订单 </text>
</view>
<view class="grid-row">
<!-- 1. 待付款 -->
<view class="grid-item" @click="toOrders('pending')">
<view class="icon-wrapper">
<image class="grid-icon-img" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0OCIgaGVpZ2h0PSI0OCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNGRjZCMDAiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxyZWN0IHg9IjEiIHk9IjQiIHdpZHRoPSIyMiIgaGVpZ2h0PSIxNiIgcng9IjIiIHJ5PSIyIj48L3JlY3Q+PGxpbmUgeDE9IjEiIHkxPSIxMCIgeDI9IjIzIiB5Mj0iMTAiPjwvbGluZT48L3N2Zz4=" mode="aspectFit"></image>
</view>
<text class="grid-label">待付款</text>
</view>
<!-- 2. 待发货 (Jump to Cabinet Tab 1) -->
<view class="grid-item" @click="toCabinetTab(1)">
<view class="icon-wrapper">
<image class="grid-icon-img" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0OCIgaGVpZ2h0PSI0OCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNGRjZCMDAiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik0xMiAyMmg1LjlhMiAyIDAgMCAwIDEuOS0xLjQybDMuMTMtMTEuMDlhMiAyIDAgMCAwLTEuOS0yLjUxaC00LjE2IiAvPjxwYXRoIGQ9Ik0xOC40MiA5aC02LjMyIiAvPjxwYXRoIGQ9Ik0xNSA2VjNhMSAxIDAgMCAwLTEtMUg2YTEgMSAwIDAgMC0xIDF2MTQiIC8+PHBhdGggZD0iTTkgMTNoNyIgLz48L3N2Zz4=" mode="aspectFit"></image>
</view>
<text class="grid-label">待发货</text>
</view>
<!-- 3. 已发货 (Jump to Cabinet Tab 1) -->
<view class="grid-item" @click="toCabinetTab(1)">
<view class="icon-wrapper">
<image class="grid-icon-img" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0OCIgaGVpZ2h0PSI0OCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNGRjZCMDAiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxyZWN0IHg9IjEiIHk9IjMiIHdpZHRoPSIxNSIgaGVpZ2h0PSIxMyIgcng9IjIiIHJ5PSIyIj48L3JlY3Q+PHBvbHlsaW5lIHBvaW50cz0iMTYgOCAyMCA4IDIwIDIxIDIgMjEgMiAxNiA2IDE2IiAvPjwvc3ZnPg==" mode="aspectFit"></image>
</view>
<text class="grid-label">已发货</text>
</view>
<!-- 4. 全部订单 (Was Box Cabinet) -->
<view class="grid-item" @click="toOrders('completed')">
<view class="icon-wrapper">
<image class="grid-icon-img" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0OCIgaGVpZ2h0PSI0OCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNGRjZCMDAiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik0yMSA4YTIgMiAwIDAgMC0xLTEuNzNsLTctNGEyIDIgMCAwIDAtMiAwbC03IDRBMiAyIDAgMCAwIDMgOHY4YTIgMiAwIDAgMCAxIDEuNzNsNyA0YTIgMiAwIDAgMCAyIDBsNy00QTIgMiAwIDAgMCAyMSAxNlY4eiIvPjxwYXRoIGQ9Ik0zLjI3IDYuOTZMMTIgMTIuMDFsMTAgLTUuMDUiLz48cGF0aCBkPSJNMTIgMjIuMDhWMTIiLz48L3N2Zz4=" mode="aspectFit"></image>
</view>
<text class="grid-label">全部订单</text>
</view>
</view>
</view>
<!-- 功能菜单 -->
<view class="section-card glass-card">
<view class="section-header">
<text class="section-title">常用功能</text>
</view>
<view class="grid-menu">
<view class="menu-item" @click="toCouponsPage">
<view class="menu-icon-box">
<image class="menu-icon-img" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0OCIgaGVpZ2h0PSI0OCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiMzMzMiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik0yMSA2SDNhMiAyIDAgMCAwLTIgMnY4YTIgMiAwIDAgMCAyIDJoMThhMiAyIDAgMCAwIDItMnYtOGEyIDIgMCAwIDAtMi0yWiIgLz48cGF0aCBkPSJNNiAxMnYtMiIgLz48cGF0aCBkPSJNNiAxNnYtMiIgLz48cGF0aCBkPSJNMTYgNnYxMiIgLz48L3N2Zz4=" mode="aspectFit"></image>
</view>
<text class="menu-label">优惠券</text>
</view>
<view class="menu-item" @click="toItemCardsPage">
<view class="menu-icon-box">
<image class="menu-icon-img" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0OCIgaGVpZ2h0PSI0OCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiMzMzMiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxyZWN0IHg9IjMiIHk9IjMiIHdpZHRoPSIxOCIgaGVpZ2h0PSIxOCIgcng9IjIiIHJ5PSIyIj48L3JlY3Q+PGNpcmNsZSBjeD0iMTIiIGN5PSIxMiIgcj0iNCIgLz48cGF0aCBkPSJNMjEgMTVsLTUuNDMtMy4yM2EyIDIgMCAwIDAtMS44NCAwbC0yLjMxIDEuMzciIC8+PC9zdmc+" mode="aspectFit"></image>
</view>
<text class="menu-label">道具卡</text>
</view>
<view class="menu-item" @click="toTasksPage">
<view class="menu-icon-box">
<image class="menu-icon-img" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0OCIgaGVpZ2h0PSI0OCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiMzMzMiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik05IDExbDMgM0wyMiA0Ii8+PHBhdGggZD0iTTIxIDEydjdhMiAyIDAgMCAxLTIgMkg1YTIgMiAwIDAgMS0yLTJWNWEyIDIgMCAwIDEgMi0yaDExIi8+PC9zdmc+" mode="aspectFit"></image>
</view>
<text class="menu-label">任务中心</text>
</view>
<view class="menu-item" @click="toInvitesPage">
<view class="menu-icon-box">
<image class="menu-icon-img" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0OCIgaGVpZ2h0PSI0OCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiMzMzMiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik0xNyAyMXYtMmE0IDQgMCAwIDAtNC00SDdhNCA0IDAgMCAwLTQgNHYyIj48L3BhdGg+PGNpcmNsZSBjeD0iMTAiIGN5PSI3IiByPSI0Ij48L2NpcmNsZT48cGF0aCBkPSJNMjMgMjF2LTJhNCA0IDAgMCAwLTMtMy4yNyI+PC9wYXRoPjxwYXRoIGQ9Ik0xNiAzLjEzYTQgNCAwIDAgMSAwIDcuNzUiPjwvcGF0aD48L3N2Zz4=" mode="aspectFit"></image>
</view>
<text class="menu-label">邀请记录</text>
</view>
<view class="menu-item" @click="toAddresses">
<view class="menu-icon-box">
<image class="menu-icon-img" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0OCIgaGVpZ2h0PSI0OCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiMzMzMiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik0yMSAxMGMwIDctOSAxMy05IDEzcy05LTYtOS0xM2E5IDkgMCAwIDEgMTggMHoiIC8+PGNpcmNsZSBjeD0iMTIiIGN5PSIxMCIgcj0iMyIgLz48L3N2Zz4=" mode="aspectFit"></image>
</view>
<text class="menu-label">收货地址</text>
</view>
<view class="menu-item" @click="toHelp">
<view class="menu-icon-box">
<image class="menu-icon-img" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0OCIgaGVpZ2h0PSI0OCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiMzMzMiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxjaXJjbGUgY3g9IjEyIiBjeT0iMTIiIHI9IjEwIi8+PHBhdGggZD0iTTkuMDkgOWEzIDMgMCAwIDEgNS44MyAxYzAgMi0zIDMtMyAzIi8+PHBhdGggZD0iTTEyIDE3aC4wMSIvPjwvc3ZnPg==" mode="aspectFit"></image>
</view>
<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 ? '+' : '' }}{{ formatPoints(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 coupon-popup" @tap.stop>
<view class="popup-header">
<text class="popup-title">我的优惠券</text>
<text class="close-btn" @tap="closeCouponsPopup">×</text>
</view>
<view class="popup-tabs coupon-tabs-3">
<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 class="popup-tab" :class="{ active: couponsTab === 3 }" @tap="switchCouponsTab(3)">已过期</view>
</view>
<scroll-view scroll-y class="coupon-scroll" @scrolltolower="loadMoreCoupons">
<view v-if="couponsLoading && couponsList.length === 0" class="status-text">加载中...</view>
<view v-else-if="!couponsList || couponsList.length === 0" class="empty-state">
<text class="empty-icon">🎟</text>
<text class="empty-text">{{ getCouponEmptyText() }}</text>
</view>
<view v-for="(item, index) in couponsList" :key="item.id || index" class="coupon-ticket-v2" :class="getCouponCardClass(item)">
<!-- 左侧金额区域 -->
<view class="coupon-left-v2">
<view class="coupon-remaining">
<text class="coupon-symbol">¥</text>
<text class="coupon-amount-num">{{ formatCouponValue(item.remaining ?? item.amount ?? 0) }}</text>
</view>
<text class="coupon-label">{{ couponsTab === 1 ? '可用' : (couponsTab === 2 ? '已用' : '过期') }}</text>
</view>
<!-- 中间分割线 -->
<view class="coupon-divider-v2">
<view class="divider-notch top"></view>
<view class="divider-dash"></view>
<view class="divider-notch bottom"></view>
</view>
<!-- 右侧信息区域 -->
<view class="coupon-right-v2">
<view class="coupon-header-row">
<text class="coupon-name-v2">{{ item.name || '优惠券' }}</text>
<view class="coupon-original" v-if="item.amount && item.remaining !== undefined && item.remaining !== item.amount">
<text>原值 ¥{{ formatCouponValue(item.amount) }}</text>
</view>
</view>
<text class="coupon-rules">{{ item.rules || '全场通用' }}</text>
<!-- 使用进度条 (仅当有使用记录时显示) -->
<view class="coupon-progress-wrap" v-if="item.amount && item.remaining !== undefined && item.remaining < item.amount">
<view class="coupon-progress-bar">
<view class="coupon-progress-fill" :style="{ width: getCouponUsedPercent(item) + '%' }"></view>
</view>
<text class="coupon-progress-text">已用 {{ formatCouponValue(item.amount - item.remaining) }} ({{ getCouponUsedPercent(item) }}%)</text>
</view>
<view class="coupon-footer-row">
<view class="coupon-footer-left">
<text class="coupon-expire-v2">{{ formatCouponExpiry(item) }}</text>
<!-- 使用时间 (已使用状态) -->
<text class="coupon-used-time" v-if="couponsTab === 2 && item.used_at">使用时间{{ formatDateTime(item.used_at) }}</text>
</view>
<view class="coupon-action-v2" v-if="couponsTab === 1">
<view class="use-btn-v2">去使用</view>
</view>
<view class="coupon-status-v2" v-else>
<text class="status-tag" :class="couponsTab === 2 ? 'used' : 'expired'">{{ couponsTab === 2 ? '已使用' : '已过期' }}</text>
</view>
</view>
</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 item-cards-popup" @tap.stop>
<view class="popup-header">
<text class="popup-title">我的道具卡</text>
<text class="close-btn" @tap="closeItemCardsPopup">×</text>
</view>
<view class="popup-tabs">
<view class="popup-tab" :class="{ active: itemCardsTab === 0 }" @tap="switchItemCardsTab(0)">未使用</view>
<view class="popup-tab" :class="{ active: itemCardsTab === 1 }" @tap="switchItemCardsTab(1)">已使用</view>
</view>
<scroll-view scroll-y class="item-cards-scroll">
<view v-if="itemCardsLoading && itemCardsList.length === 0" class="status-text">加载中...</view>
<view v-else-if="!itemCardsList || itemCardsList.length === 0" class="empty-state">
<text class="empty-icon">🃏</text>
<text class="empty-text">{{ itemCardsTab === 0 ? '暂无可用道具卡' : '暂无使用记录' }}</text>
</view>
<view class="item-cards-grid">
<view v-for="(item, index) in itemCardsList" :key="index" class="item-card" :class="{ 'used': itemCardsTab === 1 }">
<view class="card-icon-wrap">
<text class="card-icon">{{ getCardIcon(item.type || item.name) }}</text>
</view>
<view class="card-info">
<text class="card-name">{{ item.name || item.title || '道具卡' }}</text>
<text class="card-desc">{{ item.description || item.rules || '可在抽奖时使用' }}</text>
<text class="card-use-time" v-if="itemCardsTab === 1 && item.used_at">使用时间{{ formatDateTimeSeconds(item.used_at) }}</text>
</view>
<view class="card-count-badge" v-if="itemCardsTab === 0">
<text class="count-num">×{{ item.remaining ?? item.count ?? 1 }}</text>
</view>
<view class="card-used-badge" v-else>
<text class="used-text">已使用</text>
</view>
</view>
</view>
</scroll-view>
</view>
</view>
<!-- 任务中心弹窗 -->
<view class="popup-mask" v-if="tasksVisible" @tap="closeTasksPopup">
<view class="popup-content task-center-popup" @tap.stop>
<view class="popup-header">
<text class="popup-title">任务中心</text>
<text class="close-btn" @tap="closeTasksPopup">×</text>
</view>
<view class="task-hero">
<view class="hero-top">
<view class="hero-left">
<text class="hero-title">总完成率</text>
<view class="hero-percent-row">
<text class="hero-percent">{{ getOverallProgress() }}</text>
<text class="hero-percent-unit">%</text>
</view>
<view class="hero-progress">
<view class="hero-progress-fill" :style="{ width: getOverallProgress() + '%' }"></view>
</view>
</view>
<view class="hero-right">
<view class="hero-chip done">
<text class="chip-num">{{ tasksStats.done }}</text>
<text class="chip-label">已完成</text>
</view>
<view class="hero-chip ongoing">
<text class="chip-num">{{ tasksStats.ongoing }}</text>
<text class="chip-label">进行中</text>
</view>
<view class="hero-chip waiting">
<text class="chip-num">{{ tasksStats.waiting }}</text>
<text class="chip-label">未开始</text>
</view>
</view>
</view>
</view>
<scroll-view scroll-y class="task-list-scroll">
<view v-if="tasksLoading" class="task-skeleton-list">
<view v-for="i in 3" :key="i" class="task-skeleton">
<view class="sk-left">
<view class="sk-icon"></view>
<view class="sk-text">
<view class="sk-line sk-line-title"></view>
<view class="sk-line sk-line-desc"></view>
</view>
</view>
<view class="sk-btn"></view>
</view>
</view>
<view v-else-if="!tasksList.length" class="empty-state">
<text class="empty-icon">📝</text>
<text class="empty-text">暂无任务</text>
</view>
<view v-for="(task, index) in tasksList" :key="index" class="task-card" :class="getTaskClass(task)">
<view class="task-card-left">
<view class="task-icon-bubble" :class="getTaskClass(task)">
<text class="task-icon">{{ getTaskIcon(task.type || task.title || task.name) }}</text>
</view>
<view class="task-text">
<view class="task-title-row">
<text class="task-name">{{ task.title }}</text>
<text class="task-status-tag" :class="getTaskClass(task)">{{ getTaskStatusText(task) }}</text>
</view>
<text class="task-desc">{{ task.description }}</text>
<view class="task-meta">
<view class="task-reward-chip" v-if="task.reward">
<text class="reward-icon">🏆</text>
<text class="reward-text">{{ task.reward }}</text>
</view>
<view class="task-progress-wrap">
<view class="task-progress-bar">
<view class="task-progress-fill" :class="getTaskClass(task)" :style="{ width: getTaskProgress(task).percent + '%' }"></view>
</view>
<text class="task-progress-text">{{ getTaskProgressText(task) }}</text>
</view>
</view>
</view>
</view>
<view class="task-card-right">
<view class="task-action-btn" :class="getTaskClass(task)" @tap="handleTask(task)">
{{ getTaskBtnText(task) }}
</view>
</view>
</view>
</scroll-view>
</view>
</view>
<!-- 邀请记录弹窗 -->
<view class="popup-mask" v-if="invitesVisible" @tap="closeInvitesPopup">
<view class="popup-content" @tap.stop>
<view class="popup-header">
<text class="popup-title">我的邀请</text>
<text class="close-btn" @tap="closeInvitesPopup">×</text>
</view>
<view class="invite-summary">
<view class="summary-item">
<text class="summary-num">{{ invitesList.length }}</text>
<text class="summary-label">邀请人数</text>
</view>
<view class="summary-item">
<text class="summary-num">{{ getInviteRewardsTotal() }}</text>
<text class="summary-label">累计奖励</text>
</view>
</view>
<scroll-view scroll-y class="invite-list-scroll">
<view v-if="invitesLoading" class="status-text">加载中...</view>
<view v-else-if="!invitesList.length" class="empty-state">
<text class="empty-icon">👥</text>
<text class="empty-text">暂无邀请记录</text>
</view>
<view v-for="(item, index) in invitesList" :key="index" class="invite-item">
<image class="invite-avatar" :src="item.avatar || '/static/logo.png'" mode="aspectFill"></image>
<view class="invite-info">
<text class="invite-name">{{ item.nickname || '用户' + item.id }}</text>
<text class="invite-time">{{ formatDate(item.created_at) }}</text>
</view>
<view class="invite-status">已邀请</view>
</view>
</scroll-view>
</view>
</view>
</view>
</template>
<script>
import {
getUserInfo, getUserStats, getPointsBalance, getUserPoints, getUserCoupons, getItemCards,
getUserTasks, getTaskProgress, getInviteRecords
} from '../../api/appUser.js'
export default {
data() {
return {
userId: '',
nickname: '',
avatar: '',
title: '', // 用户头衔
inviteCode: '',
pointsBalance: 0,
stats: {
coupon_count: 0,
item_card_count: 0,
pending_payment: 0,
pending_shipment: 0,
shipped: 0
},
// Points Popup
pointsVisible: false,
pointsList: [],
pointsPage: 1,
pointsLoading: false,
pointsHasMore: true,
// Coupons Popup
couponsVisible: false,
couponsList: [],
couponsTab: 1, // 1=unused, 2=used, 3=expired
couponsPage: 1,
couponsLoading: false,
couponsHasMore: true,
// Item Cards Popup
itemCardsVisible: false,
itemCardsList: [],
itemCardsTab: 0, // 0=unused, 1=used
itemCardsLoading: false,
// Tasks Popup
tasksVisible: false,
tasksList: [],
tasksLoading: false,
tasksStats: { done: 0, ongoing: 0, waiting: 0 },
tasksProgressMap: {},
// Invites Popup
invitesVisible: false,
invitesList: [],
invitesLoading: false
}
},
onLoad(opts) {
const code = (opts && (opts.invite_code || opts.inviteCode)) || ''
const v = String(code || '').trim()
if (v) {
try { uni.setStorageSync('inviter_code', v) } catch (_) {}
const token = uni.getStorageSync('token')
if (!token) {
uni.navigateTo({ url: '/pages/login/index' })
}
}
},
onShow() {
this.loadUserInfo()
},
onShareAppMessage() {
const inviteCode = uni.getStorageSync('invite_code') || (uni.getStorageSync('user_info') || {}).invite_code || ''
return {
title: '🎁 好友邀请你一起玩,快来领福利!',
path: inviteCode ? `/pages-user/invite/landing?invite_code=${inviteCode}` : '/pages-user/invite/landing',
imageUrl: '/static/share_invite.png'
}
},
onShareTimeline() {
const inviteCode = uni.getStorageSync('invite_code') || (uni.getStorageSync('user_info') || {}).invite_code || ''
return {
title: '🎁 好友邀请你一起玩,快来领福利!',
query: inviteCode ? `invite_code=${inviteCode}` : '',
imageUrl: '/static/share_invite.png'
}
},
methods: {
getInviteCode() {
const v = this.inviteCode || uni.getStorageSync('invite_code') || (uni.getStorageSync('user_info') || {}).invite_code || ''
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() {
const code = this.getInviteCode()
if (!code) {
uni.showToast({ title: '暂无邀请码', icon: 'none' })
return
}
uni.setClipboardData({
data: code,
success: () => {
uni.showToast({ title: '邀请码已复制', icon: 'none' })
}
})
},
getInviteSharePath() {
const code = this.getInviteCode()
return code ? `/pages-user/invite/landing?invite_code=${encodeURIComponent(code)}` : '/pages-user/invite/landing'
},
normalizePointsBalance(v) {
if (v && typeof v === 'object') {
const nested = v.balance ?? v.points_balance ?? v.value
const n1 = Number(nested)
return Number.isFinite(n1) ? n1 : 0
}
const n2 = Number(v)
return Number.isFinite(n2) ? n2 : 0
},
async loadUserInfo() {
const token = uni.getStorageSync('token')
if (!token) {
this.resetUser()
return
}
try {
// 先尝试从缓存获取基础信息
const cachedUser = uni.getStorageSync('user_info')
const cachedUserId = uni.getStorageSync('user_id')
if (cachedUser) {
this.userId = cachedUser.id || cachedUserId
this.nickname = cachedUser.nickname
this.avatar = cachedUser.avatar
this.inviteCode = cachedUser.invite_code
this.title = cachedUser.title || ''
this.pointsBalance = this.normalizePointsBalance(cachedUser.points_balance)
} else if (cachedUserId) {
this.userId = cachedUserId
}
if (this.userId) {
try {
const balanceRes = await getPointsBalance(this.userId)
if (balanceRes !== undefined) {
this.pointsBalance = this.normalizePointsBalance(balanceRes)
if (cachedUser) {
cachedUser.points_balance = this.pointsBalance
uni.setStorageSync('user_info', cachedUser)
}
}
} catch (e) {}
const s = await getUserStats(this.userId)
if (s) this.stats = { ...this.stats, ...s }
} else {
// 如果没有 userId尝试调用 getUserInfo (即使可能失败)
const res = await getUserInfo()
if (res) {
this.userId = res.id
this.nickname = res.nickname
this.avatar = res.avatar
this.title = res.title || res.level_name || ''
this.inviteCode = res.invite_code
this.pointsBalance = this.normalizePointsBalance(res.points_balance)
uni.setStorageSync('user_info', res)
uni.setStorageSync('user_id', res.id)
// Load stats
const s = await getUserStats(res.id)
if(s) this.stats = { ...this.stats, ...s }
}
}
} catch (e) {
console.error(e)
// If 401, maybe clear token
}
},
resetUser() {
this.userId = ''
this.nickname = ''
this.avatar = ''
this.title = ''
this.pointsBalance = 0
this.stats = { coupon_count: 0, item_card_count: 0 }
},
handleJoin() {
uni.navigateTo({ url: '/pages/login/index' })
},
toOrders(status) {
uni.navigateTo({ url: `/pages-user/orders/index?status=${status}` })
},
toCabinetTab(tabIndex) {
uni.setStorageSync('cabinet_target_tab', tabIndex)
uni.switchTab({ url: '/pages/cabinet/index' })
},
toAddresses() {
uni.navigateTo({ url: '/pages-user/address/index' })
},
toPointsPage() {
uni.navigateTo({ url: '/pages-user/points/index' })
},
toCouponsPage() {
uni.navigateTo({ url: '/pages-user/coupons/index' })
},
toItemCardsPage() {
uni.navigateTo({ url: '/pages-user/item-cards/index' })
},
toInvitesPage() {
uni.navigateTo({ url: '/pages-user/invites/index' })
},
toTasksPage() {
uni.navigateTo({ url: '/pages-user/tasks/index' })
},
toHelp() {
uni.showActionSheet({
itemList: ['购买协议', '用户协议'],
success: (res) => {
const idx = Number(res && res.tapIndex)
if (idx === 0) {
uni.navigateTo({ url: '/pages-user/agreement/purchase' })
return
}
if (idx === 1) {
uni.navigateTo({ url: '/pages-user/agreement/user' })
return
}
}
})
},
handleInvite() {
const code = this.getInviteCode()
const path = this.getInviteSharePath()
try { if (code) uni.setStorageSync('inviter_code', code) } catch (_) {}
// #ifdef MP-WEIXIN
try { uni.showShareMenu({ withShareTicket: true }) } catch (_) {}
uni.showModal({
title: '邀请好友',
content: '请点击「立即邀请」进行分享。',
showCancel: true,
cancelText: '复制链接',
confirmText: '知道了',
success: (res) => {
if (res.cancel) {
const data = path
uni.setClipboardData({ data })
}
}
})
return
// #endif
// #ifdef APP-PLUS
if (typeof uni.share === 'function') {
const summary = code ? `给你一个宝藏应用,快来!邀请码:${code}` : '给你一个宝藏应用,快来!'
uni.share({
provider: 'weixin',
scene: 'WXSceneSession',
type: 1,
summary,
fail: () => {
uni.setClipboardData({ data: path })
}
})
return
}
// #endif
// #ifdef H5
const url = `${location.origin}${location.pathname}#${path}`
const text = '给你一个宝藏应用,快来!'
if (typeof navigator !== 'undefined' && navigator.share) {
navigator.share({ title: text, text, url }).catch(() => {
uni.setClipboardData({ data: url })
})
} else {
uni.setClipboardData({ data: url })
}
// #endif
// #ifndef MP-WEIXIN
// #ifndef APP-PLUS
// #ifndef H5
uni.setClipboardData({ data: path })
// #endif
// #endif
// #endif
},
// --- Points Logic ---
async showPointsPopup() {
this.pointsVisible = true
this.pointsList = []
this.pointsPage = 1
this.pointsHasMore = true
await this.loadMorePoints()
},
closePointsPopup() { this.pointsVisible = false },
async loadMorePoints() {
if (this.pointsLoading || !this.pointsHasMore) return
this.pointsLoading = true
try {
const res = await getUserPoints(this.userId, this.pointsPage, 10)
const list = res.list || res.data || []
if (list.length < 10) this.pointsHasMore = false
this.pointsList = [...this.pointsList, ...list]
this.pointsPage++
} catch (e) {
this.pointsHasMore = false
} finally {
this.pointsLoading = false
}
},
getActionText(action) {
const map = {
'login': '每日登录',
'register': '注册赠送',
'invite': '邀请好友',
'consume': '消费抵扣',
'refund': '退款返还',
'activity': '活动奖励'
}
return map[action] || '积分变动'
},
// --- Coupons Logic ---
async showCouponsPopup() {
this.couponsVisible = true
this.couponsTab = 1
this.couponsList = []
this.couponsPage = 1
this.couponsHasMore = true
await this.loadMoreCoupons()
},
closeCouponsPopup() { this.couponsVisible = false },
switchCouponsTab(tab) {
if (this.couponsTab === tab) return
this.couponsTab = tab
this.couponsList = []
this.couponsPage = 1
this.couponsHasMore = true
this.loadMoreCoupons()
},
async loadMoreCoupons() {
if (this.couponsLoading || !this.couponsHasMore) return
this.couponsLoading = true
try {
// status: 0=unused, 1=used, 2=expired
const statusMap = { 1: 0, 2: 1, 3: 2 }
const res = await getUserCoupons(this.userId, statusMap[this.couponsTab], this.couponsPage)
const list = res.list || res.data || []
if (list.length < 10) this.couponsHasMore = false
this.couponsList = [...this.couponsList, ...list]
this.couponsPage++
} catch (e) {
this.couponsHasMore = false
} finally {
this.couponsLoading = false
}
},
getCouponEmptyText() {
if (this.couponsTab === 1) return '暂无可用优惠券'
if (this.couponsTab === 2) return '暂无使用记录'
return '暂无过期优惠券'
},
getCouponCardClass(item) {
if (this.couponsTab === 2) return 'coupon-used'
if (this.couponsTab === 3) return 'coupon-expired'
return ''
},
formatCouponValue(val) {
return (Number(val) / 100).toFixed(0)
},
formatCouponExpiry(item) {
if (!item.end_time) return '长期有效'
return `有效期至 ${this.formatDate(item.end_time)}`
},
getCouponUsedPercent(item) {
if (!item.amount || !item.remaining) return 0
const used = item.amount - item.remaining
return Math.floor((used / item.amount) * 100)
},
// --- Item Cards Logic ---
async showItemCardsPopup() {
this.itemCardsVisible = true
this.itemCardsTab = 0
this.itemCardsList = []
this.itemCardsLoading = true
await this.loadItemCards()
},
closeItemCardsPopup() { this.itemCardsVisible = false },
switchItemCardsTab(t) {
if (this.itemCardsTab === t) return
this.itemCardsTab = t
this.loadItemCards()
},
async loadItemCards() {
this.itemCardsLoading = true
this.itemCardsList = []
try {
// Pass status: 0(tab)=>1(unused), 1(tab)=>2(used)
const status = this.itemCardsTab === 0 ? 1 : 2
const res = await getItemCards(this.userId, status)
// Robustly get the list (support res.list or res.data)
let list = Array.isArray(res) ? res : (res.list || res.data || [])
this.itemCardsList = list.map(item => {
return {
...item,
// Ensure count exists, default to 1 if undefined
count: item.count ?? item.remaining ?? 1
}
})
// For unused tab, filter out items with 0 count if any
if (this.itemCardsTab === 0) {
this.itemCardsList = this.itemCardsList.filter(i => i.count > 0)
}
} catch (e) {
console.error('loadItemCards error:', e)
} finally {
this.itemCardsLoading = false
}
},
getCardIcon(type) {
if ((type || '').includes('透视')) return '👁️'
if ((type || '').includes('提示')) return '💡'
if ((type || '').includes('重置')) return '🔄'
return '🃏'
},
// --- Tasks Logic ---
async showTasksPopup() {
this.tasksVisible = true
this.tasksLoading = true
try {
const res = await getUserTasks(1, 20)
this.tasksList = Array.isArray(res) ? res : (res.list || [])
} catch (e) {
this.tasksList = []
} finally {
this.recalcTasksStats()
this.loadTasksProgress()
this.tasksLoading = false
}
},
closeTasksPopup() { this.tasksVisible = false },
recalcTasksStats() {
const list = Array.isArray(this.tasksList) ? this.tasksList : []
let done = 0
let ongoing = 0
let waiting = 0
for (let i = 0; i < list.length; i++) {
const s = Number(list[i]?.status)
if (s === 2) done += 1
else if (s === 1) ongoing += 1
else waiting += 1
}
this.tasksStats = { done, ongoing, waiting }
},
getOverallProgress() {
const total = Array.isArray(this.tasksList) ? this.tasksList.length : 0
if (!total) return 0
return Math.floor((this.tasksStats.done / total) * 100)
},
getTaskClass(task) {
const s = Number(task && task.status)
if (s === 2) return 'task-done'
if (s === 1) return 'task-ongoing'
return 'task-waiting'
},
getTaskIcon(type) {
const s = String(type || '').trim()
if (!s) return '📌'
if (s.includes('邀请') || s.includes('invite')) return '👥'
if (s.includes('登录') || s.includes('login')) return '📅'
if (s.includes('分享') || s.includes('share')) return '📣'
if (s.includes('消费') || s.includes('consume')) return '🛒'
if (s.includes('绑定') || s.includes('phone')) return '📱'
if (s.includes('关注') || s.includes('follow')) return '⭐'
return '📌'
},
getTaskStatusText(task) {
const s = Number(task && task.status)
if (s === 2) return '已完成'
if (s === 1) return '进行中'
return '未开始'
},
getTaskBtnText(task) {
if (task.status === 2) return '已完成'
if (task.status === 1) return '去完成'
return '领取任务'
},
normalizeTaskProgress(res) {
const raw = res && (res.data ?? res.result ?? res)
if (!raw || typeof raw !== 'object') return null
const current = raw.current ?? raw.progress ?? raw.done ?? raw.completed ?? raw.value ?? null
const total = raw.total ?? raw.target ?? raw.required ?? raw.max ?? raw.goal ?? null
const percentRaw = raw.percent ?? raw.percentage ?? raw.rate ?? raw.ratio ?? null
let percent = null
if (percentRaw !== null && percentRaw !== undefined && Number.isFinite(Number(percentRaw))) {
const p = Number(percentRaw)
percent = p <= 1 ? Math.round(p * 100) : Math.round(p)
}
if (percent === null && current !== null && total !== null) {
const c = Number(current)
const t = Number(total)
if (Number.isFinite(c) && Number.isFinite(t) && t > 0) {
percent = Math.round((c / t) * 100)
}
}
if (percent === null) return null
if (percent < 0) percent = 0
if (percent > 100) percent = 100
const currentNum = Number.isFinite(Number(current)) ? Number(current) : null
const totalNum = Number.isFinite(Number(total)) ? Number(total) : null
return { percent, current: currentNum, total: totalNum }
},
getTaskProgress(task) {
const id = task && (task.id ?? task.task_id ?? task.taskId)
const s = Number(task && task.status)
if (s === 2) return { percent: 100, current: null, total: null }
if (id === undefined || id === null || id === '') return { percent: 0, current: null, total: null }
const p = this.tasksProgressMap[String(id)]
if (p && Number.isFinite(Number(p.percent))) {
const percent = Math.max(0, Math.min(100, Number(p.percent)))
return { percent, current: p.current ?? null, total: p.total ?? null }
}
return { percent: 0, current: null, total: null }
},
getTaskProgressText(task) {
const p = this.getTaskProgress(task)
if (Number(task && task.status) === 2) return '进度 100%'
if (p.current !== null && p.total !== null) return `进度 ${p.current}/${p.total}`
return `进度 ${p.percent}%`
},
async loadTasksProgress() {
const user_id = this.userId || uni.getStorageSync('user_id')
const list = Array.isArray(this.tasksList) ? this.tasksList : []
if (!user_id || list.length === 0) return
const need = []
for (let i = 0; i < list.length; i++) {
const t = list[i]
const id = t && (t.id ?? t.task_id ?? t.taskId)
if (id === undefined || id === null || id === '') continue
const key = String(id)
if (this.tasksProgressMap[key]) continue
need.push({ key, id })
}
if (need.length === 0) return
const settled = await Promise.allSettled(need.map(it => getTaskProgress(it.id, user_id)))
const next = { ...this.tasksProgressMap }
for (let i = 0; i < settled.length; i++) {
const it = need[i]
const r = settled[i]
if (r.status !== 'fulfilled') continue
const normalized = this.normalizeTaskProgress(r.value)
if (normalized) next[it.key] = normalized
}
this.tasksProgressMap = next
},
handleTask(task) {
if (task.status === 2) return
// Implement task action logic
uni.showToast({ title: '任务进行中', icon: 'none' })
},
// --- Invites Logic ---
async showInvitesPopup() {
this.invitesVisible = true
this.invitesLoading = true
try {
const res = await getInviteRecords(1, 20)
this.invitesList = Array.isArray(res) ? res : (res.list || [])
} catch (e) {
this.invitesList = []
} finally {
this.invitesLoading = false
}
},
closeInvitesPopup() { this.invitesVisible = false },
getInviteRewardsTotal() {
// Calculate total rewards from invitesList if available
return '0'
},
// Utils
toDate(input) {
if (!input) return null
if (input instanceof Date) return isNaN(input.getTime()) ? null : input
if (typeof input === 'number') {
if (!Number.isFinite(input)) return null
const ms = input > 1e12 ? input : input * 1000
const d = new Date(ms)
return isNaN(d.getTime()) ? null : d
}
const s = String(input || '').trim()
if (!s) return null
if (/^\d+$/.test(s)) {
const n = Number(s)
return this.toDate(n)
}
const c1 = s
const c2 = s.replace(/([+-]\d{2}):(\d{2})$/, '$1$2')
const c3 = c2.replace(/-/g, '/').replace('T', ' ')
const c4 = s.replace(/-/g, '/').replace('T', ' ')
const candidates = [c1, c2, c3, c4]
for (let i = 0; i < candidates.length; i++) {
const d = new Date(candidates[i])
if (!isNaN(d.getTime())) return d
}
return null
},
formatDate(ts) {
const d = this.toDate(ts)
if (!d) return ''
return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`
},
formatDateTime(ts) {
const d = this.toDate(ts)
if (!d) return ''
return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')} ${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`
},
formatDateTimeSeconds(ts) {
const d = this.toDate(ts)
if (!d) return ''
return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')} ${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}:${String(d.getSeconds()).padStart(2,'0')}`
}
}
}
</script>
<style lang="scss">
/* ============================================
个人中心 - 视觉升级 (SCSS Integration)
============================================ */
.page-container {
min-height: 100vh;
background-color: $bg-page;
padding-bottom: calc(env(safe-area-inset-bottom) + 120rpx); /* TabBar space */
position: relative;
overflow-x: hidden;
}
/* 背景装饰 - 漂浮光球 */
.bg-decoration {
position: absolute;
top: 0; left: 0; width: 100%; height: 100%;
pointer-events: none;
z-index: 0;
overflow: hidden;
&::before {
content: '';
position: absolute;
top: -100rpx; right: -100rpx;
width: 600rpx; height: 600rpx;
background: radial-gradient(circle, rgba($brand-primary, 0.15) 0%, transparent 70%);
filter: blur(60rpx);
border-radius: 50%;
opacity: 0.8;
animation: float 10s ease-in-out infinite;
}
&::after {
content: '';
position: absolute;
top: 200rpx; left: -200rpx;
width: 500rpx; height: 500rpx;
background: radial-gradient(circle, rgba($brand-secondary, 0.1) 0%, transparent 70%);
filter: blur(50rpx);
border-radius: 50%;
opacity: 0.6;
animation: float 15s ease-in-out infinite reverse;
}
}
@keyframes float {
0%, 100% { transform: translate(0, 0); }
50% { transform: translate(30rpx, 50rpx); }
}
/* 通用毛玻璃卡片 */
.glass-card {
background: $bg-glass;
backdrop-filter: blur(20rpx);
border: 1px solid rgba(255, 255, 255, 0.6);
box-shadow: $shadow-card;
border-radius: $radius-lg;
position: relative;
z-index: 1;
}
/* 头部区域 */
.header-section {
padding: 0 $spacing-lg;
padding-top: calc(env(safe-area-inset-top) + 20rpx);
margin-bottom: $spacing-lg;
}
.user-info-card {
padding: $spacing-xl;
background: linear-gradient(135deg, rgba(255,255,255,0.95), rgba(255,255,255,0.8));
box-shadow: $shadow-float;
border: 1px solid rgba(255,255,255,0.8);
}
.user-main {
display: flex;
align-items: center;
margin-bottom: 40rpx;
}
.avatar {
width: 120rpx; height: 120rpx;
background: $bg-card;
border-radius: 50%;
border: 4rpx solid $bg-card;
box-shadow: $shadow-sm;
margin-right: 24rpx;
}
.user-meta { flex: 1; }
.name-row {
display: flex; align-items: center; margin-bottom: 12rpx;
}
.nickname {
font-size: $font-xl; font-weight: 900; color: $text-main;
margin-right: 16rpx;
}
.level-badge {
background: linear-gradient(90deg, #333, #555);
color: $accent-gold;
font-size: $font-xs;
padding: 4rpx 12rpx;
border-radius: 100rpx;
display: flex; align-items: center;
box-shadow: 0 4rpx 8rpx rgba(0,0,0,0.2);
}
.level-icon { width: 24rpx; height: 24rpx; margin-right: 4rpx; }
.userid { font-size: $font-sm; color: $text-tertiary; font-family: monospace; }
.join-btn {
background: $text-main;
color: $text-inverse;
padding: 12rpx 32rpx;
border-radius: 100rpx;
font-size: 26rpx;
font-weight: 700;
box-shadow: $shadow-sm;
}
/* 进度条 */
.progress-container {
width: 100%;
}
.progress-bar {
height: 8rpx;
background: $border-color-light;
border-radius: 100rpx;
overflow: hidden;
margin-bottom: 6rpx;
}
.progress-fill {
height: 100%;
background: $gradient-brand;
border-radius: 100rpx;
}
.progress-text {
font-size: $font-xs; color: $text-sub;
}
/* 数据统计 */
.stats-row {
display: flex; align-items: center; justify-content: space-around;
padding-top: 20rpx;
border-top: 1px solid rgba(0,0,0,0.03);
}
.stat-item {
display: flex; flex-direction: column; align-items: center;
flex: 1;
&:active { opacity: 0.7; }
}
.stat-num {
font-size: $font-xl; font-weight: 900; color: $text-main;
font-family: 'DIN Alternate', sans-serif;
margin-bottom: 4rpx;
}
.stat-label { font-size: $font-sm; color: $text-sub; }
.stat-divider { width: 1px; height: 30rpx; background: $border-color-light; }
/* 邀请Banner */
.invite-banner {
margin: 0 $spacing-lg $spacing-lg;
background: linear-gradient(135deg, $uni-bg-color-hover 0%, $bg-card 100%);
border: 1px solid rgba($brand-primary, 0.1);
padding: 30rpx;
display: flex; align-items: center; justify-content: space-between;
position: relative;
overflow: hidden;
}
.invite-content { position: relative; z-index: 2; flex: 1; display: flex; align-items: center; justify-content: space-between; }
.invite-text-group { display: flex; flex-direction: column; }
.invite-title-row { display: flex; align-items: center; margin-bottom: 8rpx; }
.invite-tag {
background: $accent-red; color: $text-inverse; font-size: 18rpx; padding: 2rpx 8rpx; border-radius: 4rpx; margin-right: 12rpx;
}
.invite-title { font-size: 30rpx; font-weight: 800; color: #5D3A20; }
.invite-desc { font-size: $font-xs; color: #9E7D60; }
.invite-action-btn {
background: $gradient-brand;
color: $text-inverse;
padding: 10rpx 24rpx;
border-radius: 100rpx;
font-size: $font-sm;
font-weight: 700;
display: flex; align-items: center;
box-shadow: 0 4rpx 12rpx rgba($brand-primary, 0.2);
}
.invite-share-btn {
line-height: 1;
margin: 0;
}
.invite-share-btn::after { border: none; }
.invite-share-icon {
width: 30rpx;
height: 30rpx;
margin-right: 10rpx;
border-radius: 8rpx;
}
.invite-arrow { font-size: 28rpx; margin-left: 4rpx; line-height: 1; }
.invite-bg-icon {
position: absolute; right: -20rpx; bottom: -20rpx;
width: 140rpx; height: 140rpx;
opacity: 0.1;
transform: rotate(-15deg);
}
/* 常用功能 & 订单 */
.section-card {
margin: 0 $spacing-lg $spacing-lg;
padding: 30rpx;
}
.section-header {
display: flex; justify-content: space-between; align-items: center;
margin-bottom: 30rpx;
}
.section-title { font-size: 30rpx; font-weight: 800; color: $text-main; }
.section-more { font-size: $font-sm; color: $text-sub; }
.grid-row, .grid-menu {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20rpx;
}
.grid-item, .menu-item {
display: flex; flex-direction: column; align-items: center;
position: relative;
&:active { transform: scale(0.96); transition: 0.1s; }
}
.icon-wrapper, .menu-icon-box {
width: 80rpx; height: 80rpx;
background: $bg-secondary;
border-radius: 24rpx;
display: flex; align-items: center; justify-content: center;
margin-bottom: 12rpx;
transition: all 0.3s;
}
.grid-item:active .icon-wrapper, .menu-item:active .menu-icon-box {
background: $uni-bg-color-hover;
}
.grid-icon-img, .menu-icon-img { width: 44rpx; height: 44rpx; }
.grid-label, .menu-label { font-size: $font-sm; color: $text-main; }
/* 弹窗通用样式 */
.popup-mask {
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.6);
backdrop-filter: blur(5rpx);
z-index: 999;
display: flex; align-items: center; justify-content: center;
animation: fadeIn 0.2s;
}
.popup-content {
width: 640rpx;
max-height: 80vh;
background: $bg-card;
border-radius: $radius-xl;
display: flex; flex-direction: column;
overflow: hidden;
animation: zoomIn 0.3s cubic-bezier(0.18, 0.89, 0.32, 1.28);
}
.popup-header {
padding: 30rpx;
text-align: center;
position: relative;
border-bottom: 1px solid $border-color-light;
}
.popup-title { font-size: $font-lg; font-weight: 800; }
.close-btn {
position: absolute; right: 30rpx; top: 30rpx;
font-size: 40rpx; color: $text-tertiary;
line-height: 1;
padding: 10rpx;
}
.status-text, .empty-state {
padding: 60rpx; text-align: center; color: $text-sub; font-size: 26rpx;
}
.empty-icon { font-size: 80rpx; display: block; margin-bottom: 20rpx; }
.no-more {
text-align: center;
font-size: 24rpx;
color: $text-tertiary;
padding: 30rpx 0;
display: flex;
align-items: center;
justify-content: center;
.text {
margin: 0 16rpx;
}
.divider {
width: 60rpx;
height: 1px;
background: #e0e0e0;
}
}
/* 弹窗 Tab 栏 */
.popup-tabs {
display: flex;
justify-content: space-around;
padding: 0 20rpx;
background: #fff;
border-bottom: 1rpx solid $border-color-light;
margin-bottom: 20rpx;
}
.popup-tab {
flex: 1;
text-align: center;
font-size: 28rpx;
color: $text-sub;
padding: 24rpx 0;
position: relative;
font-weight: 500;
transition: all 0.2s;
}
.popup-tab.active {
color: $text-main;
font-weight: 700;
}
.popup-tab.active::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 40rpx;
height: 6rpx;
background: $brand-primary;
border-radius: 6rpx;
}
/* 列表滚动区 */
.points-list, .coupon-scroll, .item-cards-scroll, .task-list-scroll, .invite-list-scroll {
flex: 1; min-height: 400rpx; background: $bg-grey; padding: 20rpx;
}
/* 积分明细 */
.point-item {
background: $bg-card; padding: 24rpx; border-radius: $radius-md; margin-bottom: 20rpx;
display: flex; justify-content: space-between; align-items: center;
box-shadow: $shadow-sm;
}
.point-desc { font-size: $font-md; color: $text-main; margin-bottom: 6rpx; display: block; }
.point-time { font-size: $font-xs; color: $text-tertiary; }
.point-amount { font-size: $font-lg; font-weight: 700; color: $text-main; }
.point-amount.positive { color: $brand-primary; }
.point-amount.negative { color: $text-main; }
/* 优惠券 V2样式 */
.coupon-ticket-v2 {
background: #fff; border-radius: 16rpx; margin-bottom: 20rpx;
display: flex; overflow: hidden; box-shadow: $shadow-sm;
position: relative;
}
.coupon-left-v2 {
width: 180rpx; background: linear-gradient(135deg, #FFF5E6, #fff);
display: flex; flex-direction: column; align-items: center; justify-content: center;
padding: 20rpx;
position: relative;
}
.coupon-remaining { color: $brand-primary; font-weight: 900; }
.coupon-symbol { font-size: 24rpx; }
.coupon-amount-num { font-size: 56rpx; line-height: 1; }
.coupon-label { font-size: 20rpx; color: $brand-primary; margin-top: 8rpx; border: 1px solid $brand-primary; padding: 2rpx 8rpx; border-radius: 6rpx; }
/* 优惠券分割线 */
.coupon-divider-v2 {
width: 30rpx;
position: relative;
background: #fff;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
overflow: hidden;
}
.divider-notch {
width: 24rpx;
height: 24rpx;
background: $bg-grey; /* Match scroll container bg */
border-radius: 50%;
position: absolute;
left: 50%;
transform: translateX(-50%);
z-index: 2;
}
.divider-notch.top { top: -12rpx; }
.divider-notch.bottom { bottom: -12rpx; }
.divider-dash {
width: 0;
height: 80%;
border-left: 2rpx dashed #eee;
}
.coupon-right-v2 {
flex: 1;
padding: 24rpx;
display: flex;
flex-direction: column;
justify-content: space-between;
overflow: hidden; /* Ensure content stays inside */
}
.coupon-name-v2 {
font-size: $font-md;
font-weight: 700;
color: $text-main;
margin-bottom: 8rpx;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.coupon-original { font-size: 20rpx; color: $text-tertiary; text-decoration: line-through; margin-left: 8rpx; display: inline-block; }
.coupon-rules { font-size: $font-xs; color: $text-sub; margin-bottom: 16rpx; }
/* 优惠券进度条 */
.coupon-progress-wrap {
margin-bottom: 12rpx;
}
.coupon-progress-bar {
height: 6rpx;
background: $bg-secondary;
border-radius: 100rpx;
overflow: hidden;
margin-bottom: 4rpx;
}
.coupon-progress-fill {
height: 100%;
background: $brand-primary;
border-radius: 100rpx;
}
.coupon-progress-text {
font-size: 18rpx;
color: $text-tertiary;
}
.coupon-footer-row { display: flex; justify-content: space-between; align-items: center; margin-top: auto; }
.coupon-footer-left { display: flex; flex-direction: column; }
.coupon-expire-v2 { font-size: 20rpx; color: $text-tertiary; }
.use-btn-v2 {
background: $brand-primary;
color: #fff;
font-size: 22rpx;
padding: 8rpx 24rpx;
border-radius: 100rpx;
display: flex;
align-items: center;
justify-content: center;
line-height: normal;
}
.status-tag { font-size: 22rpx; color: $text-tertiary; background: #F5F5F5; padding: 4rpx 12rpx; border-radius: 6rpx; }
.coupon-used-time { font-size: 18rpx; color: $text-tertiary; margin-top: 4rpx; text-align: left; }
/* 过期/已使用状态 */
.coupon-used .coupon-left-v2, .coupon-expired .coupon-left-v2 {
background: #f9f9f9;
}
.coupon-used .coupon-remaining, .coupon-expired .coupon-remaining,
.coupon-used .coupon-label, .coupon-expired .coupon-label {
color: $text-tertiary;
border-color: $text-tertiary;
}
.coupon-used .coupon-name-v2, .coupon-expired .coupon-name-v2 {
color: $text-sub;
}
/* 道具卡 */
.item-cards-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20rpx; }
.item-card {
background: #fff; border-radius: 20rpx; padding: 24rpx;
position: relative; overflow: hidden; box-shadow: $shadow-sm;
border: 1px solid transparent;
}
.item-card:active { transform: scale(0.98); }
.card-icon-wrap { width: 80rpx; height: 80rpx; background: #F0F8FF; border-radius: 50%; display: flex; align-items: center; justify-content: center; margin-bottom: 16rpx; }
.card-icon { font-size: 40rpx; }
.card-name { font-size: $font-md; font-weight: 700; margin-bottom: 8rpx; display: block; }
.card-desc { font-size: $font-xs; color: $text-sub; display: block; margin-bottom: 16rpx; line-height: 1.4; }
.card-count-badge { position: absolute; top: 20rpx; right: 20rpx; background: rgba(0,0,0,0.05); padding: 4rpx 12rpx; border-radius: 100rpx; }
.count-num { font-size: 22rpx; font-weight: 700; color: $text-main; }
/* 道具卡已使用状态 */
.item-card.used {
background: #fafafa;
}
.item-card.used .card-name {
color: $text-sub;
}
.item-card.used .card-desc {
color: $text-tertiary;
}
.item-card.used .card-icon-wrap {
background: #f0f0f0;
opacity: 0.6;
}
.card-used-badge {
position: absolute;
top: 20rpx;
right: 20rpx;
background: #eee;
padding: 4rpx 12rpx;
border-radius: 100rpx;
}
.used-text {
font-size: 22rpx;
color: $text-tertiary;
}
.card-use-time {
font-size: 18rpx;
color: $text-tertiary;
margin-top: 12rpx;
display: block;
border-top: 1rpx dashed #eee;
padding-top: 8rpx;
}
/* 任务中心弹窗 */
.task-center-popup { background: $bg-page; }
.task-hero {
margin: 20rpx;
border-radius: 24rpx;
overflow: hidden;
background: $gradient-brand;
box-shadow: $shadow-warm;
}
.hero-top {
padding: 28rpx;
display: flex;
justify-content: space-between;
align-items: center;
gap: 20rpx;
}
.hero-left { flex: 1; }
.hero-title { font-size: 22rpx; color: rgba(255,255,255,0.9); display: block; }
.hero-percent-row { display: flex; align-items: baseline; margin-top: 10rpx; }
.hero-percent { font-size: 54rpx; font-weight: 900; color: #fff; line-height: 1; }
.hero-percent-unit { font-size: 22rpx; font-weight: 700; color: rgba(255,255,255,0.95); margin-left: 8rpx; }
.hero-progress {
margin-top: 16rpx;
height: 10rpx;
background: rgba(255,255,255,0.25);
border-radius: 100rpx;
overflow: hidden;
}
.hero-progress-fill {
height: 100%;
background: rgba(255,255,255,0.95);
border-radius: 100rpx;
}
.hero-right {
display: flex;
flex-direction: column;
gap: 12rpx;
align-items: flex-end;
}
.hero-chip {
min-width: 140rpx;
padding: 10rpx 14rpx;
border-radius: 18rpx;
background: rgba(255,255,255,0.16);
border: 1rpx solid rgba(255,255,255,0.22);
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 10rpx;
}
.chip-num { font-size: 28rpx; font-weight: 900; color: #fff; }
.chip-label { font-size: 20rpx; color: rgba(255,255,255,0.9); }
.task-skeleton-list { padding: 10rpx 0; }
.task-skeleton {
background: $bg-card;
border-radius: 20rpx;
padding: 24rpx;
margin: 0 20rpx 16rpx;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: $shadow-sm;
}
.sk-left { display: flex; align-items: center; gap: 16rpx; flex: 1; }
.sk-icon { width: 68rpx; height: 68rpx; border-radius: 18rpx; background: rgba(0,0,0,0.06); }
.sk-text { flex: 1; display: flex; flex-direction: column; gap: 12rpx; }
.sk-line { height: 18rpx; border-radius: 10rpx; background: rgba(0,0,0,0.06); }
.sk-line-title { width: 58%; }
.sk-line-desc { width: 78%; }
.sk-btn { width: 140rpx; height: 56rpx; border-radius: 100rpx; background: rgba(0,0,0,0.06); }
.task-card {
background: $bg-card;
margin: 0 20rpx 16rpx;
padding: 24rpx;
border-radius: 20rpx;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: $shadow-sm;
border: 1rpx solid rgba(0,0,0,0.02);
}
.task-card:active { transform: scale(0.99); }
.task-card-left { display: flex; align-items: flex-start; gap: 16rpx; flex: 1; min-width: 0; }
.task-icon-bubble {
width: 72rpx;
height: 72rpx;
border-radius: 22rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
background: rgba($brand-primary, 0.12);
}
.task-ongoing .task-icon-bubble { background: rgba($brand-primary, 0.12); }
.task-waiting .task-icon-bubble { background: rgba(0,0,0,0.06); }
.task-done .task-icon-bubble { background: rgba(16,185,129,0.12); }
.task-icon { font-size: 36rpx; }
.task-text { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.task-title-row { display: flex; align-items: center; justify-content: space-between; gap: 12rpx; }
.task-name { font-size: $font-md; font-weight: 800; color: $text-main; max-width: 420rpx; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.task-status-tag {
font-size: 20rpx;
padding: 4rpx 12rpx;
border-radius: 100rpx;
flex-shrink: 0;
}
.task-ongoing .task-status-tag { background: rgba($brand-primary, 0.12); color: $brand-primary; }
.task-waiting .task-status-tag { background: rgba(0,0,0,0.06); color: $text-sub; }
.task-done .task-status-tag { background: rgba(16,185,129,0.12); color: #10B981; }
.task-desc {
font-size: $font-xs;
color: $text-sub;
margin-top: 8rpx;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.task-meta { margin-top: 10rpx; display: flex; gap: 10rpx; flex-wrap: wrap; }
.task-progress-wrap {
width: 100%;
display: flex;
flex-direction: column;
gap: 6rpx;
}
.task-progress-bar {
height: 8rpx;
background: rgba(0,0,0,0.06);
border-radius: 100rpx;
overflow: hidden;
}
.task-progress-fill {
height: 100%;
width: 0;
border-radius: 100rpx;
background: rgba($brand-primary, 0.9);
}
.task-progress-fill.task-done { background: rgba(16,185,129,0.9); }
.task-progress-fill.task-waiting { background: rgba($brand-primary, 0.4); }
.task-progress-text {
font-size: 20rpx;
color: $text-tertiary;
}
.task-reward-chip {
padding: 4rpx 10rpx;
border-radius: 10rpx;
background: #FFF7E6;
color: #D97706;
display: flex;
align-items: center;
gap: 6rpx;
}
.reward-icon { font-size: 18rpx; }
.reward-text { font-size: 20rpx; font-weight: 700; }
.task-card-right { display: flex; flex-direction: column; align-items: flex-end; gap: 10rpx; margin-left: 16rpx; }
.task-action-btn {
min-width: 140rpx;
text-align: center;
padding: 12rpx 18rpx;
border-radius: 100rpx;
font-size: 24rpx;
font-weight: 700;
background: $gradient-brand;
color: #fff;
box-shadow: $shadow-warm;
}
.task-waiting .task-action-btn { background: rgba($brand-primary, 0.12); color: $brand-primary; box-shadow: none; }
.task-done .task-action-btn { background: #F2F2F2; color: $text-tertiary; box-shadow: none; }
/* 邀请记录 */
.invite-summary { display: flex; padding: 30rpx; background: #fff; margin-bottom: 20rpx; }
.summary-item { flex: 1; display: flex; flex-direction: column; align-items: center; border-right: 1px solid #eee; }
.summary-item:last-child { border: none; }
.summary-num { font-size: 36rpx; font-weight: 900; color: $brand-primary; }
.summary-label { font-size: 22rpx; color: $text-sub; }
.invite-item {
background: #fff; padding: 20rpx; border-radius: 12rpx; margin-bottom: 16rpx;
display: flex; align-items: center;
}
.invite-avatar { width: 80rpx; height: 80rpx; border-radius: 50%; margin-right: 20rpx; background: #f0f0f0; }
.invite-info { flex: 1; display: flex; flex-direction: column; }
.invite-name { font-size: $font-md; font-weight: 700; }
.invite-time { font-size: 22rpx; color: $text-tertiary; margin-top: 4rpx; }
.invite-status { font-size: 24rpx; color: #10B981; }
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes zoomIn { from { transform: scale(0.9); opacity: 0; } to { transform: scale(1); opacity: 1; } }
</style>