bindbox-mini/pages/mine/index.vue
2026-01-05 11:08:23 +08:00

2166 lines
80 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">
<view class="avatar-wrapper" @click="handleEditAvatar" v-if="nickname">
<image class="avatar" :src="avatar || '/static/logo.png'" mode="aspectFill"></image>
<view class="avatar-edit-badge">
<text class="edit-icon">📷</text>
</view>
</view>
<image v-else class="avatar" :src="avatar || '/static/logo.png'" mode="aspectFill"></image>
<view class="user-meta">
<view class="name-row" @click="handleEditNickname" v-if="nickname">
<text class="nickname">{{ nickname || '未登录' }}</text>
<text class="edit-nickname-icon"></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="name-row" v-else>
<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 class="menu-item" @click="toBindDouyinOrder">
<view class="menu-icon-box">
<image class="menu-icon-img" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0OCIgaGVpZ2h0PSI0OCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiMzMzMiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik05IDExVjlhMyAzIDAgMCAxIDMtM2g0YTMgMyAwIDAgMSAzIDN2Mm0tMTAtM2gtNGEzIDMgMCAwIDAtMyAzdi04YTIgMiAwIDAgMSAyLTJoMTRhMiAyIDAgMCAxIDIgMnY4YTIgMiAwIDAgMS0yIDJoLTRhMyAzIDAgMCAxLTMtM3oiLz48Y2lyY2xlIGN4PSIxMiIgY3k9IjEzIiByPSIzIi8+PC9zdmc+" mode="aspectFit"></image>
</view>
<text class="menu-label">{{ douyinUserId ? '已绑定' : '绑定订单' }}</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">{{ formatCouponRules(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, modifyUser, getUserProfile, bindDouyinOrder
} from '../../api/appUser.js'
import { checkPhoneBoundSync } from '../../utils/checkPhone.js'
export default {
data() {
return {
userId: '',
nickname: '',
avatar: '',
title: '', // 用户头衔
inviteCode: '',
mobile: '', // 手机号
douyinUserId: '', // 抖音用户ID
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() {
// 检查手机号绑定状态(快速检查本地缓存)
if (!checkPhoneBoundSync()) return
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: {
// 检查是否已绑定手机号
checkPhoneBound() {
if (!this.mobile) {
uni.showModal({
title: '需要绑定手机号',
content: '为了账号安全,请先绑定手机号',
showCancel: false,
confirmText: '去绑定',
success: () => {
uni.navigateTo({ url: '/pages/login/index?mode=sms' })
}
})
return false
}
return true
},
// 修改头像
handleEditAvatar() {
if (!this.userId) {
uni.showToast({ title: '请先登录', icon: 'none' })
return
}
if (!this.checkPhoneBound()) return
// 检查冷却时间
const cooldownKey = 'avatar_update_cooldown'
const lastUpdate = uni.getStorageSync(cooldownKey)
const now = Date.now()
const cooldownDays = 7
if (lastUpdate && now - lastUpdate < cooldownDays * 24 * 60 * 60 * 1000) {
const daysRemaining = Math.ceil((cooldownDays * 24 * 60 * 60 * 1000 - (now - lastUpdate)) / (24 * 60 * 60 * 1000))
uni.showToast({
title: `头像修改冷却中,还需${daysRemaining}`,
icon: 'none'
})
return
}
uni.chooseImage({
count: 1,
sizeType: ['compressed'], // 选择压缩图
sourceType: ['album', 'camera'],
success: (res) => {
const tempFilePath = res.tempFilePaths[0]
this.uploadAvatar(tempFilePath)
}
})
},
// 上传头像并转换为 base64
async uploadAvatar(filePath) {
uni.showLoading({ title: '上传中...' })
try {
// 将图片转换为 base64
const base64 = await this.fileToBase64(filePath)
// 调用修改用户信息接口
const data = await modifyUser(this.userId, { avatar: base64 })
// 更新本地存储
uni.setStorageSync('avatar', data.avatar || base64)
this.avatar = data.avatar || base64
const userInfo = uni.getStorageSync('user_info') || {}
userInfo.avatar = data.avatar || base64
uni.setStorageSync('user_info', userInfo)
// 记录修改时间7天冷却
uni.setStorageSync('avatar_update_cooldown', Date.now())
uni.hideLoading()
uni.showToast({ title: '头像修改成功', icon: 'success' })
} catch (err) {
uni.hideLoading()
uni.showToast({ title: err.message || '上传失败', icon: 'none' })
}
},
// 文件转 base64
fileToBase64(filePath) {
return new Promise((resolve, reject) => {
uni.getFileSystemManager().readFile({
filePath: filePath,
encoding: 'base64',
success: (res) => {
// 判断文件类型并添加 data URI 前缀
const ext = filePath.split('.').pop().toLowerCase()
let mimeType = 'image/jpeg'
if (ext === 'png') mimeType = 'image/png'
if (ext === 'gif') mimeType = 'image/gif'
if (ext === 'webp') mimeType = 'image/webp'
resolve(`data:${mimeType};base64,${res.data}`)
},
fail: (err) => {
reject(err)
}
})
})
},
// 修改昵称
handleEditNickname() {
if (!this.userId) {
uni.showToast({ title: '请先登录', icon: 'none' })
return
}
// 检查冷却时间
const cooldownKey = 'nickname_update_cooldown'
const lastUpdate = uni.getStorageSync(cooldownKey)
const now = Date.now()
const cooldownDays = 7
if (lastUpdate && now - lastUpdate < cooldownDays * 24 * 60 * 60 * 1000) {
const daysRemaining = Math.ceil((cooldownDays * 24 * 60 * 60 * 1000 - (now - lastUpdate)) / (24 * 60 * 60 * 1000))
uni.showToast({
title: `昵称修改冷却中,还需${daysRemaining}`,
icon: 'none'
})
return
}
uni.showModal({
title: '修改昵称',
editable: true,
placeholderText: '请输入新昵称',
content: this.nickname,
success: async (res) => {
if (res.confirm) {
const newNickname = res.content?.trim()
if (!newNickname) {
uni.showToast({ title: '昵称不能为空', icon: 'none' })
return
}
if (newNickname.length > 20) {
uni.showToast({ title: '昵称不能超过20个字符', icon: 'none' })
return
}
try {
uni.showLoading({ title: '修改中...' })
// 调用修改用户信息接口
const data = await modifyUser(this.userId, { nickname: newNickname })
// 更新本地存储
uni.setStorageSync('nickname', data.nickname || newNickname)
this.nickname = data.nickname || newNickname
const userInfo = uni.getStorageSync('user_info') || {}
userInfo.nickname = data.nickname || newNickname
uni.setStorageSync('user_info', userInfo)
// 记录修改时间7天冷却
uni.setStorageSync('nickname_update_cooldown', Date.now())
uni.hideLoading()
uni.showToast({ title: '昵称修改成功', icon: 'success' })
} catch (err) {
uni.hideLoading()
uni.showToast({ title: err.message || '修改失败', icon: 'none' })
}
}
}
})
},
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.0'
const f = n / 100
return f.toFixed(1)
},
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 {
// 优先使用新的 profile API
let profile = null
try {
profile = await getUserProfile()
} catch (err) {
console.warn('[Mine] getUserProfile 调用失败,将尝试降级逻辑:', err)
}
if (profile) {
console.log('[Mine] 从 profile API 获取用户信息:', profile)
this.userId = profile.id
this.nickname = profile.nickname
this.avatar = profile.avatar
this.title = profile.title || ''
this.inviteCode = profile.invite_code
this.pointsBalance = this.normalizePointsBalance(profile.balance)
this.mobile = profile.mobile || ''
this.douyinUserId = profile.douyin_user_id || ''
// 更新缓存
const cachedUser = uni.getStorageSync('user_info') || {}
cachedUser.id = profile.id
cachedUser.nickname = profile.nickname
cachedUser.avatar = profile.avatar
cachedUser.title = profile.title
cachedUser.invite_code = profile.invite_code
cachedUser.mobile = profile.mobile
cachedUser.points_balance = this.pointsBalance
uni.setStorageSync('user_info', cachedUser)
uni.setStorageSync('user_id', profile.id)
// 如果有手机号,更新缓存
if (profile.mobile) {
uni.setStorageSync('phone_number', profile.mobile)
}
// Load stats
const s = await getUserStats(profile.id)
if (s) this.stats = { ...this.stats, ...s }
return
}
// 降级到旧逻辑
console.log('[Mine] profile API 无数据,使用降级逻辑')
// 先尝试从缓存获取基础信息
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)
this.mobile = cachedUser.mobile || cachedUser.phone || cachedUser.phone_number || ''
this.douyinUserId = cachedUser.douyin_user_id || ''
} 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)
this.mobile = res.mobile || res.phone || res.phone_number || ''
this.douyinUserId = res.douyin_user_id || ''
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('[Mine] loadUserInfo 错误:', e)
// If 401, maybe clear token
}
},
resetUser() {
this.userId = ''
this.nickname = ''
this.avatar = ''
this.title = ''
this.pointsBalance = 0
this.douyinUserId = ''
this.stats = { coupon_count: 0, item_card_count: 0 }
},
handleJoin() {
uni.navigateTo({ url: '/pages/login/index' })
},
toOrders(status) {
if (!this.checkPhoneBound()) return
uni.navigateTo({ url: `/pages-user/orders/index?status=${status}` })
},
toCabinetTab(tabIndex) {
if (!this.checkPhoneBound()) return
uni.setStorageSync('cabinet_target_tab', tabIndex)
uni.switchTab({ url: '/pages/cabinet/index' })
},
toAddresses() {
if (!this.checkPhoneBound()) return
uni.navigateTo({ url: '/pages-user/address/index' })
},
toPointsPage() {
if (!this.checkPhoneBound()) return
uni.navigateTo({ url: '/pages-user/points/index' })
},
toCouponsPage() {
if (!this.checkPhoneBound()) return
uni.navigateTo({ url: '/pages-user/coupons/index' })
},
toItemCardsPage() {
if (!this.checkPhoneBound()) return
uni.navigateTo({ url: '/pages-user/item-cards/index' })
},
toInvitesPage() {
if (!this.checkPhoneBound()) return
uni.navigateTo({ url: '/pages-user/invites/index' })
},
toTasksPage() {
if (!this.checkPhoneBound()) return
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
}
}
})
},
toBindDouyinOrder() {
if (!this.checkPhoneBound()) return
// 检查是否已绑定
if (this.douyinUserId) {
uni.showToast({
title: '您已绑定抖音账号,暂不支持换绑',
icon: 'none'
})
return
}
uni.showModal({
title: '绑定抖音订单',
editable: true,
placeholderText: '请输入抖音订单号',
success: async (res) => {
if (res.confirm) {
const orderId = res.content?.trim()
if (!orderId) {
uni.showToast({ title: '订单号不能为空', icon: 'none' })
return
}
try {
uni.showLoading({ title: '绑定中...' })
const data = await bindDouyinOrder(orderId)
this.douyinUserId = data.douyin_user_id
// 更新缓存
const userInfo = uni.getStorageSync('user_info') || {}
userInfo.douyin_user_id = data.douyin_user_id
uni.setStorageSync('user_info', userInfo)
uni.hideLoading()
uni.showToast({ title: '绑定成功', icon: 'success' })
} catch (err) {
uni.hideLoading()
uni.showToast({ title: err.message || '绑定失败', icon: 'none' })
}
}
}
})
},
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() {
if (!this.checkPhoneBound()) return
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() {
if (!this.checkPhoneBound()) return
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)
},
formatCouponRules(rules) {
if (!rules) return '全场通用'
// 将"XXX分"替换为"¥X.XX元"格式
return rules.replace(/(\d+)分/g, (match, p1) => {
const yuan = (Number(p1) / 100).toFixed(2)
// 去掉末尾的.00
const formatted = yuan.endsWith('.00') ? yuan.slice(0, -3) : yuan
return `¥${formatted}`
})
},
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() {
if (!this.checkPhoneBound()) return
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() {
if (!this.checkPhoneBound()) return
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() {
if (!this.checkPhoneBound()) return
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;
}
.avatar-wrapper {
position: relative;
margin-right: 24rpx;
cursor: pointer;
.avatar {
margin-right: 0;
}
.avatar-edit-badge {
position: absolute;
bottom: 0;
right: 0;
width: 40rpx;
height: 40rpx;
background: linear-gradient(135deg, $brand-primary, $brand-secondary);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
border: 3rpx solid $bg-card;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.15);
.edit-icon {
font-size: 20rpx;
line-height: 1;
}
}
}
.user-meta { flex: 1; }
.name-row {
display: flex; align-items: center; margin-bottom: 12rpx;
cursor: pointer;
&:active {
opacity: 0.7;
}
}
.nickname {
font-size: $font-xl; font-weight: 900; color: $text-main;
margin-right: 16rpx;
}
.edit-nickname-icon {
font-size: 28rpx;
margin-right: 8rpx;
opacity: 0.6;
}
.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>