2223 lines
82 KiB
Vue
2223 lines
82 KiB
Vue
<template>
|
||
<view class="page-container">
|
||
<!-- 顶部背景装饰 -->
|
||
<view class="bg-decoration"></view>
|
||
|
||
<!-- 自定义 tabBar -->
|
||
<!-- #ifdef MP-TOUTIAO -->
|
||
<custom-tab-bar-toutiao />
|
||
<!-- #endif -->
|
||
<!-- #ifndef MP-TOUTIAO -->
|
||
<custom-tab-bar />
|
||
<!-- #endif -->
|
||
|
||
<!-- 头部区域 -->
|
||
<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="douyin-id-row" v-if="douyinUserId">
|
||
<text class="douyin-id-label">抖音号:</text>
|
||
<text class="douyin-id-value">{{ douyinUserId }}</text>
|
||
</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>
|
||
<!-- #ifdef MP-TOUTIAO -->
|
||
<view class="menu-item" @click="toMinesweeper">
|
||
<view class="menu-icon-box">
|
||
<image class="menu-icon-img" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0OCIgaGVpZ2h0PSI0OCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiMzMzMiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik0xMCAxNGwtNiA2Ii8+PHBhdGggZD0iTTE0IDEwbDYgLTYiLz48cGF0aCBkPSJNMTEuNSAxMmwzIDNsLTMgM2wtMy0zeiIvPjxwYXRoIGQ9Ik0xMCAxNGw2IDZtLTItMmwtNiA2bTYtNjYgNm0tMi0ybDItMiIvPjwvc3ZnPg==" mode="aspectFit"></image>
|
||
</view>
|
||
<text class="menu-label">扫雷</text>
|
||
</view>
|
||
<!-- #endif -->
|
||
</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, bindDouyinID
|
||
} from '../../api/appUser.js'
|
||
import { checkPhoneBoundSync } from '../../utils/checkPhone.js'
|
||
// #ifdef MP-TOUTIAO
|
||
import customTabBarToutiao from '@/components/custom-tab-bar-toutiao.vue'
|
||
// #endif
|
||
// #ifndef MP-TOUTIAO
|
||
import customTabBar from '@/components/custom-tab-bar.vue'
|
||
// #endif
|
||
|
||
export default {
|
||
components: {
|
||
// #ifdef MP-TOUTIAO
|
||
customTabBarToutiao
|
||
// #endif
|
||
// #ifndef MP-TOUTIAO
|
||
customTabBar
|
||
// #endif
|
||
},
|
||
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: '绑定抖音',
|
||
content: '请输入您的抖店订单号即可完成绑定',
|
||
editable: true,
|
||
placeholderText: '请输入您的抖店订单号',
|
||
success: async (res) => {
|
||
if (res.confirm) {
|
||
const douyinId = res.content?.trim()
|
||
if (!douyinId) {
|
||
uni.showToast({ title: '抖店订单号不能为空', icon: 'none' })
|
||
return
|
||
}
|
||
|
||
try {
|
||
uni.showLoading({ title: '绑定中...' })
|
||
const data = await bindDouyinID(douyinId)
|
||
this.douyinUserId = data.douyin_id
|
||
|
||
// 更新缓存
|
||
const userInfo = uni.getStorageSync('user_info') || {}
|
||
userInfo.douyin_user_id = data.douyin_id
|
||
uni.setStorageSync('user_info', userInfo)
|
||
|
||
uni.hideLoading()
|
||
uni.showToast({ title: '绑定成功', icon: 'success' })
|
||
} catch (err) {
|
||
uni.hideLoading()
|
||
uni.showToast({ title: err.message || '绑定失败', icon: 'none' })
|
||
}
|
||
}
|
||
}
|
||
})
|
||
},
|
||
toMinesweeper() {
|
||
if (!this.checkPhoneBound()) return
|
||
uni.navigateTo({ url: '/pages-game/game/minesweeper/index' })
|
||
},
|
||
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; }
|
||
|
||
/* 绑定的抖音号 */
|
||
.douyin-id-row {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-top: 8rpx;
|
||
font-size: $font-sm;
|
||
}
|
||
.douyin-id-label {
|
||
color: $text-tertiary;
|
||
margin-right: 8rpx;
|
||
}
|
||
.douyin-id-value {
|
||
color: $brand-primary;
|
||
font-weight: 600;
|
||
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>
|