|
@@ -1,804 +1,957 @@
|
|
|
-<template>
|
|
|
|
|
- <view class="page-profile">
|
|
|
|
|
- <!-- ========== Region 1: User Card ========== -->
|
|
|
|
|
- <view class="user-card">
|
|
|
|
|
- <!-- Login prompt for unauthenticated users -->
|
|
|
|
|
- <view v-if="!userStore.isLoggedIn" class="user-card__login" @click="goLogin">
|
|
|
|
|
- <view class="avatar avatar--empty">
|
|
|
|
|
- <text class="avatar__icon">+</text>
|
|
|
|
|
- </view>
|
|
|
|
|
- <text class="user-card__nickname">未登录</text>
|
|
|
|
|
- <text class="user-card__login-hint">点击登录 / 注册</text>
|
|
|
|
|
- </view>
|
|
|
|
|
-
|
|
|
|
|
- <!-- Authenticated user card — tap to edit -->
|
|
|
|
|
- <view v-else class="user-card__logged" @click="goEditProfile">
|
|
|
|
|
- <view class="user-card__inner">
|
|
|
|
|
- <view class="avatar">
|
|
|
|
|
- <image :src="userStore.avatarUrl || '/static/default-avatar.png'" mode="aspectFill" />
|
|
|
|
|
- </view>
|
|
|
|
|
- <view class="user-card__info">
|
|
|
|
|
- <text class="user-card__nickname">{{ userStore.nickname }}</text>
|
|
|
|
|
-
|
|
|
|
|
- <!-- Role tag -->
|
|
|
|
|
- <view
|
|
|
|
|
- class="user-card__tag"
|
|
|
|
|
- :class="{
|
|
|
|
|
- 'user-card__tag--practitioner': userStore.vipType === 'practitioner',
|
|
|
|
|
- 'user-card__tag--annual': userStore.vipType === 'annual',
|
|
|
|
|
- 'user-card__tag--normal': !userStore.vipType
|
|
|
|
|
- }"
|
|
|
|
|
- >
|
|
|
|
|
- <text v-if="!userStore.vipType">普通用户</text>
|
|
|
|
|
- <text v-else-if="userStore.vipType === 'annual'">C端会员</text>
|
|
|
|
|
- <text v-else>能量师 🌟</text>
|
|
|
|
|
- </view>
|
|
|
|
|
-
|
|
|
|
|
- <!-- Social summary (optional) -->
|
|
|
|
|
- <text v-if="userStore.bio || userStore.city" class="user-card__summary">
|
|
|
|
|
- {{ userStore.city || '' }}{{ userStore.city && userStore.bio ? ' · ' : '' }}{{ userStore.bio || '' }}
|
|
|
|
|
- </text>
|
|
|
|
|
- </view>
|
|
|
|
|
- <text class="user-card__arrow">›</text>
|
|
|
|
|
- </view>
|
|
|
|
|
- </view>
|
|
|
|
|
- </view>
|
|
|
|
|
-
|
|
|
|
|
- <!-- ========== Region 2: Membership Status Cards ========== -->
|
|
|
|
|
-
|
|
|
|
|
- <!-- US-5.1 §13: Profile incomplete banner -->
|
|
|
|
|
- <view v-if="userStore.isLoggedIn && userStore.profileIncomplete" class="banner" @click="goEditProfile">
|
|
|
|
|
- <text class="banner__text">📝 请完善个人资料,开启能量匹配</text>
|
|
|
|
|
- <text class="banner__arrow">›</text>
|
|
|
|
|
- </view>
|
|
|
|
|
-
|
|
|
|
|
- <!-- US-4.3: Renewal reminder (7 days before expiry) -->
|
|
|
|
|
- <view v-if="isExpiringSoon" class="banner banner--warning" @click="goRenew">
|
|
|
|
|
- <text class="banner__text">⏰ 您的会员即将到期,请及时续费</text>
|
|
|
|
|
- <text class="banner__arrow">›</text>
|
|
|
|
|
- </view>
|
|
|
|
|
-
|
|
|
|
|
- <!-- Seed-price badge -->
|
|
|
|
|
- <view v-if="userStore.isSeedPriceUser" class="seed-badge">
|
|
|
|
|
- <text class="seed-badge__text">🌱 种子价专属会员</text>
|
|
|
|
|
- </view>
|
|
|
|
|
-
|
|
|
|
|
- <!-- VIP status card — role-based -->
|
|
|
|
|
- <!-- Case 1: Energy master (practitioner, in valid period) -->
|
|
|
|
|
- <view
|
|
|
|
|
- v-if="userStore.vipType === 'practitioner' && userStore.isVip && !isExpired"
|
|
|
|
|
- class="vip-card vip-card--practitioner"
|
|
|
|
|
- >
|
|
|
|
|
- <view class="vip-card__body">
|
|
|
|
|
- <text class="vip-card__title">能量师会员 🌟</text>
|
|
|
|
|
- <text class="vip-card__desc">
|
|
|
|
|
- 有效期至 {{ formatDate(userStore.vipEndTime) }}
|
|
|
|
|
- </text>
|
|
|
|
|
- </view>
|
|
|
|
|
- <text class="vip-card__action" @click="goRenew">续费</text>
|
|
|
|
|
- </view>
|
|
|
|
|
-
|
|
|
|
|
- <!-- Case 2: C-end annual subscriber (in valid period) -->
|
|
|
|
|
- <view
|
|
|
|
|
- v-else-if="userStore.vipType === 'annual' && userStore.isVip && !isExpired"
|
|
|
|
|
- class="vip-card vip-card--annual"
|
|
|
|
|
- >
|
|
|
|
|
- <view class="vip-card__body">
|
|
|
|
|
- <text class="vip-card__title">C端会员</text>
|
|
|
|
|
- <text class="vip-card__desc">
|
|
|
|
|
- 有效期至 {{ formatDate(userStore.vipEndTime) }}
|
|
|
|
|
- </text>
|
|
|
|
|
- </view>
|
|
|
|
|
- <text class="vip-card__action" @click="goUpgradePractitioner">升级能量师</text>
|
|
|
|
|
- </view>
|
|
|
|
|
-
|
|
|
|
|
- <!-- Case 3: Expired member (vipEndTime < now) -->
|
|
|
|
|
- <view v-else-if="userStore.vipType && isExpired" class="vip-card vip-card--expired">
|
|
|
|
|
- <view class="vip-card__body">
|
|
|
|
|
- <text class="vip-card__title">会员已过期</text>
|
|
|
|
|
- <text class="vip-card__desc">
|
|
|
|
|
- 过期时间 {{ formatDate(userStore.vipEndTime) }}
|
|
|
|
|
- </text>
|
|
|
|
|
- </view>
|
|
|
|
|
- <text class="vip-card__action" @click="goPayment">重新开通</text>
|
|
|
|
|
- </view>
|
|
|
|
|
-
|
|
|
|
|
- <!-- Case 4: Free user — two payment options -->
|
|
|
|
|
- <view v-else class="vip-card vip-card--upsell">
|
|
|
|
|
- <view class="vip-card__body">
|
|
|
|
|
- <text class="vip-card__title">选择方案</text>
|
|
|
|
|
- <view class="vip-card__dual">
|
|
|
|
|
- <view class="vip-card__option" @click="goPayment">
|
|
|
|
|
- <text class="option-title">C端会员</text>
|
|
|
|
|
- <text class="option-price">¥131/年</text>
|
|
|
|
|
- <text class="option-desc">无限AI解读</text>
|
|
|
|
|
- <button class="option-btn light">开通</button>
|
|
|
|
|
- </view>
|
|
|
|
|
- <view class="vip-card__option vip-card__option--pro" @click="goUpgradePractitioner">
|
|
|
|
|
- <text class="option-title">能量师</text>
|
|
|
|
|
- <text class="option-price">¥1,314/年</text>
|
|
|
|
|
- <text class="option-desc">完整功能+分销</text>
|
|
|
|
|
- <button class="option-btn primary">申请</button>
|
|
|
|
|
- </view>
|
|
|
|
|
- </view>
|
|
|
|
|
- </view>
|
|
|
|
|
- </view>
|
|
|
|
|
-
|
|
|
|
|
- <!-- Daily quota (non-VIP only) -->
|
|
|
|
|
- <view v-if="!userStore.isVip" class="quota-bar">
|
|
|
|
|
- <view class="quota-bar__item">
|
|
|
|
|
- <text class="quota-bar__label">今日分析</text>
|
|
|
|
|
- <text class="quota-bar__value">{{ userStore.usedCharts }}/{{ userStore.maxCharts }}</text>
|
|
|
|
|
- </view>
|
|
|
|
|
- <view class="quota-bar__divider"></view>
|
|
|
|
|
- <view class="quota-bar__item">
|
|
|
|
|
- <text class="quota-bar__label">今日解读</text>
|
|
|
|
|
- <text class="quota-bar__value">{{ userStore.usedChats }}/{{ userStore.maxChats }}</text>
|
|
|
|
|
- </view>
|
|
|
|
|
- </view>
|
|
|
|
|
-
|
|
|
|
|
- <!-- ========== Region 3: Feature Menu ========== -->
|
|
|
|
|
-
|
|
|
|
|
- <!-- Group 1: My Services -->
|
|
|
|
|
- <view class="menu-section">
|
|
|
|
|
- <view class="menu-section__header">我的服务</view>
|
|
|
|
|
-
|
|
|
|
|
- <!-- Analysis history (all logged-in) -->
|
|
|
|
|
- <view v-if="userStore.isLoggedIn" class="menu-item" @click="goRecords">
|
|
|
|
|
- <text class="menu-item__label">📋 我的分析</text>
|
|
|
|
|
- <text class="menu-item__arrow">›</text>
|
|
|
|
|
- </view>
|
|
|
|
|
-
|
|
|
|
|
- <!-- Distribution panel (paid users only) -->
|
|
|
|
|
- <view v-if="userStore.isLoggedIn && userStore.isVip" class="menu-item" @click="goCommission">
|
|
|
|
|
- <text class="menu-item__label">💰 我的推广</text>
|
|
|
|
|
- <text class="menu-item__arrow">›</text>
|
|
|
|
|
- </view>
|
|
|
|
|
-
|
|
|
|
|
- <!-- Practitioner workbench (practitioners only) — EPIC 9 -->
|
|
|
|
|
- <view v-if="userStore.vipType === 'practitioner' && userStore.isVip" class="menu-item" @click="goWorkbench">
|
|
|
|
|
- <text class="menu-item__label">🔧 工作台</text>
|
|
|
|
|
- <view class="menu-item__badge menu-item__badge--new">NEW</view>
|
|
|
|
|
- <text class="menu-item__arrow">›</text>
|
|
|
|
|
- </view>
|
|
|
|
|
- </view>
|
|
|
|
|
-
|
|
|
|
|
- <!-- Group 2: Promotion Tools (paid users only) -->
|
|
|
|
|
- <view v-if="userStore.isLoggedIn && userStore.isVip && userStore.referralCode" class="menu-section">
|
|
|
|
|
- <view class="menu-section__header">推广工具</view>
|
|
|
|
|
- <view class="promo-card">
|
|
|
|
|
- <view class="promo-card__code-area">
|
|
|
|
|
- <text class="promo-card__prefix">推广码</text>
|
|
|
|
|
- <text class="promo-card__code">{{ userStore.referralCode }}</text>
|
|
|
|
|
- </view>
|
|
|
|
|
- <view class="promo-card__actions">
|
|
|
|
|
- <text class="promo-card__btn" @click="copyReferral">复制推广码</text>
|
|
|
|
|
- <text class="promo-card__divider">|</text>
|
|
|
|
|
- <text class="promo-card__btn" @click="generatePoster">生成推广海报</text>
|
|
|
|
|
- </view>
|
|
|
|
|
- </view>
|
|
|
|
|
- </view>
|
|
|
|
|
-
|
|
|
|
|
- <!-- Group 3: More -->
|
|
|
|
|
- <view class="menu-section">
|
|
|
|
|
- <view class="menu-section__header">更多</view>
|
|
|
|
|
-
|
|
|
|
|
- <!-- My tags (practitioner only, P2) -->
|
|
|
|
|
- <view v-if="userStore.vipType === 'practitioner'" class="menu-item" @click="goTags">
|
|
|
|
|
- <text class="menu-item__label">🏷 我的标签</text>
|
|
|
|
|
- <text class="menu-item__arrow">›</text>
|
|
|
|
|
- </view>
|
|
|
|
|
-
|
|
|
|
|
- <!-- About us (all) -->
|
|
|
|
|
- <view class="menu-item" @click="goAbout">
|
|
|
|
|
- <text class="menu-item__label">ℹ️ 关于我们</text>
|
|
|
|
|
- <text class="menu-item__arrow">›</text>
|
|
|
|
|
- </view>
|
|
|
|
|
- </view>
|
|
|
|
|
-
|
|
|
|
|
- <!-- ========== Region 4: Logout ========== -->
|
|
|
|
|
- <view v-if="userStore.isLoggedIn" class="menu-section">
|
|
|
|
|
- <view class="menu-item menu-item--danger" @click="handleLogout">
|
|
|
|
|
- <text class="menu-item__label menu-item__label--danger">退出登录</text>
|
|
|
|
|
- </view>
|
|
|
|
|
- </view>
|
|
|
|
|
- </view>
|
|
|
|
|
-</template>
|
|
|
|
|
-
|
|
|
|
|
-<script setup>
|
|
|
|
|
-import { computed } from 'vue'
|
|
|
|
|
-import { onShow } from '@dcloudio/uni-app'
|
|
|
|
|
-import { useUserStore } from '@/stores/user'
|
|
|
|
|
-
|
|
|
|
|
-const userStore = useUserStore()
|
|
|
|
|
-
|
|
|
|
|
-// US-4.3: Renewal reminder if expiry within 7 days
|
|
|
|
|
-const isExpiringSoon = computed(() => {
|
|
|
|
|
- if (!userStore.vipEndTime) return false
|
|
|
|
|
- const expiry = new Date(userStore.vipEndTime)
|
|
|
|
|
- const now = new Date()
|
|
|
|
|
- const diffHours = (expiry - now) / (1000 * 60 * 60)
|
|
|
|
|
- return diffHours > 0 && diffHours <= 7 * 24
|
|
|
|
|
-})
|
|
|
|
|
-
|
|
|
|
|
-// US-4.3: Expired check
|
|
|
|
|
-const isExpired = computed(() => {
|
|
|
|
|
- if (!userStore.vipEndTime) return false
|
|
|
|
|
- return new Date(userStore.vipEndTime) < new Date()
|
|
|
|
|
-})
|
|
|
|
|
-
|
|
|
|
|
-function formatDate(dateStr) {
|
|
|
|
|
- if (!dateStr) return ''
|
|
|
|
|
- return dateStr.slice(0, 10)
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-onShow(() => {
|
|
|
|
|
- if (userStore.isLoggedIn) {
|
|
|
|
|
- userStore.fetchProfile()
|
|
|
|
|
- userStore.fetchQuota()
|
|
|
|
|
- userStore.fetchCommissionSummary()
|
|
|
|
|
- }
|
|
|
|
|
-})
|
|
|
|
|
-
|
|
|
|
|
-function goLogin() {
|
|
|
|
|
- uni.navigateTo({ url: '/pages/login/index' })
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function goEditProfile() {
|
|
|
|
|
- if (!userStore.isLoggedIn) {
|
|
|
|
|
- uni.navigateTo({ url: '/pages/login/index' })
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
- uni.navigateTo({ url: '/pages/profile/edit' })
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function goPayment() {
|
|
|
|
|
- uni.navigateTo({ url: '/pages/payment/index?product=annual' })
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function goRenew() {
|
|
|
|
|
- const product = userStore.vipType === 'practitioner' ? 'practitioner' : 'annual'
|
|
|
|
|
- uni.navigateTo({ url: `/pages/payment/index?product=${product}` })
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function goUpgradePractitioner() {
|
|
|
|
|
- uni.navigateTo({ url: '/pages/payment/index?product=practitioner' })
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function goRecords() {
|
|
|
|
|
- uni.switchTab({ url: '/pages/records/index' })
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function goCommission() {
|
|
|
|
|
- if (!userStore.isLoggedIn) {
|
|
|
|
|
- uni.navigateTo({ url: '/pages/login/index' })
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
- uni.navigateTo({ url: '/pages/commission/index' })
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// EPIC 9: Navigate to practitioner workbench
|
|
|
|
|
-function goWorkbench() {
|
|
|
|
|
- uni.navigateTo({ url: '/pages/plan/workbench' })
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function copyReferral() {
|
|
|
|
|
- if (userStore.referralCode) {
|
|
|
|
|
- uni.setClipboardData({
|
|
|
|
|
- data: userStore.referralCode,
|
|
|
|
|
- success: () => uni.showToast({ title: '已复制推广码', icon: 'success' })
|
|
|
|
|
- })
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function generatePoster() {
|
|
|
|
|
- uni.showToast({ title: '海报生成中…', icon: 'none' })
|
|
|
|
|
- // US-6.5: Backend generates poster with mini-program code
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function goTags() {
|
|
|
|
|
- uni.navigateTo({ url: '/pages/tags/index' })
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function goAbout() {
|
|
|
|
|
- uni.navigateTo({ url: '/pages/about/index' })
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function handleLogout() {
|
|
|
|
|
- uni.showModal({
|
|
|
|
|
- title: '确认退出',
|
|
|
|
|
- content: '确定要退出登录吗?',
|
|
|
|
|
- success: (res) => {
|
|
|
|
|
- if (res.confirm) {
|
|
|
|
|
- userStore.logout()
|
|
|
|
|
- uni.showToast({ title: '已退出', icon: 'success' })
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- })
|
|
|
|
|
-}
|
|
|
|
|
-</script>
|
|
|
|
|
-
|
|
|
|
|
-<style scoped lang="scss">
|
|
|
|
|
-// Brand colors (aligned with style-guide.md)
|
|
|
|
|
-$blue-dark: #1E3A5F;
|
|
|
|
|
-$gold: #C9A84C;
|
|
|
|
|
-$gold-light: #F8F0D8;
|
|
|
|
|
-$warm-white: #F8F6F1;
|
|
|
|
|
-$white: #FFFFFF;
|
|
|
|
|
-$text-primary: #1F2937;
|
|
|
|
|
-$text-secondary: #6B7280;
|
|
|
|
|
-$text-muted: #9CA3AF;
|
|
|
|
|
-$danger: #EF4444;
|
|
|
|
|
-
|
|
|
|
|
-.page-profile {
|
|
|
|
|
- padding: 16px;
|
|
|
|
|
- min-height: 100vh;
|
|
|
|
|
- background: $warm-white;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// ========== Region 1: User Card ==========
|
|
|
|
|
-.user-card {
|
|
|
|
|
- background: linear-gradient(135deg, $blue-dark, darken($blue-dark, 6%));
|
|
|
|
|
- border-radius: 16px;
|
|
|
|
|
- padding: 20px 18px;
|
|
|
|
|
- margin-bottom: 16px;
|
|
|
|
|
-
|
|
|
|
|
- &__login {
|
|
|
|
|
- cursor: pointer;
|
|
|
|
|
- text-align: center;
|
|
|
|
|
- padding: 8px 0;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- &__logged {
|
|
|
|
|
- cursor: pointer;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- &__inner {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- gap: 14px;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- &__info {
|
|
|
|
|
- flex: 1;
|
|
|
|
|
- min-width: 0;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- &__nickname {
|
|
|
|
|
- font-size: 17px;
|
|
|
|
|
- font-weight: 600;
|
|
|
|
|
- color: $white;
|
|
|
|
|
- display: block;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- &__login-hint {
|
|
|
|
|
- font-size: 12px;
|
|
|
|
|
- color: rgba(255, 255, 255, 0.7);
|
|
|
|
|
- display: block;
|
|
|
|
|
- margin-top: 4px;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- &__tag {
|
|
|
|
|
- display: inline-flex;
|
|
|
|
|
- padding: 2px 12px;
|
|
|
|
|
- border-radius: 20px;
|
|
|
|
|
- font-size: 11px;
|
|
|
|
|
- margin-top: 6px;
|
|
|
|
|
-
|
|
|
|
|
- &--practitioner {
|
|
|
|
|
- background: $gold;
|
|
|
|
|
- color: $blue-dark;
|
|
|
|
|
- font-weight: 600;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- &--annual {
|
|
|
|
|
- background: rgba($gold, 0.25);
|
|
|
|
|
- color: $gold;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- &--normal {
|
|
|
|
|
- background: rgba(255, 255, 255, 0.15);
|
|
|
|
|
- color: rgba(255, 255, 255, 0.8);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- &__summary {
|
|
|
|
|
- font-size: 11px;
|
|
|
|
|
- color: rgba(255, 255, 255, 0.55);
|
|
|
|
|
- margin-top: 4px;
|
|
|
|
|
- display: block;
|
|
|
|
|
- overflow: hidden;
|
|
|
|
|
- text-overflow: ellipsis;
|
|
|
|
|
- white-space: nowrap;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- &__arrow {
|
|
|
|
|
- font-size: 22px;
|
|
|
|
|
- color: rgba(255, 255, 255, 0.3);
|
|
|
|
|
- flex-shrink: 0;
|
|
|
|
|
- padding-left: 4px;
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// Avatar
|
|
|
|
|
-.avatar {
|
|
|
|
|
- width: 56px;
|
|
|
|
|
- height: 56px;
|
|
|
|
|
- border-radius: 50%;
|
|
|
|
|
- overflow: hidden;
|
|
|
|
|
- flex-shrink: 0;
|
|
|
|
|
-
|
|
|
|
|
- image {
|
|
|
|
|
- width: 100%;
|
|
|
|
|
- height: 100%;
|
|
|
|
|
- border-radius: 50%;
|
|
|
|
|
- border: 2px solid rgba(255, 255, 255, 0.25);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- &--empty {
|
|
|
|
|
- width: 56px;
|
|
|
|
|
- height: 56px;
|
|
|
|
|
- background: rgba(255, 255, 255, 0.12);
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- justify-content: center;
|
|
|
|
|
- border: 2px dashed rgba(255, 255, 255, 0.25);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- &__icon {
|
|
|
|
|
- font-size: 24px;
|
|
|
|
|
- color: rgba(255, 255, 255, 0.5);
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// ========== Region 2: Membership Status ==========
|
|
|
|
|
-
|
|
|
|
|
-// Banners
|
|
|
|
|
-.banner {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- justify-content: space-between;
|
|
|
|
|
- padding: 10px 14px;
|
|
|
|
|
- border-radius: 10px;
|
|
|
|
|
- margin-bottom: 12px;
|
|
|
|
|
- background: #FEF3C7;
|
|
|
|
|
- border: 1px solid #FDE68A;
|
|
|
|
|
-
|
|
|
|
|
- &--warning {
|
|
|
|
|
- background: #FEF3C7;
|
|
|
|
|
- border-color: #FDE68A;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- &__text {
|
|
|
|
|
- font-size: 13px;
|
|
|
|
|
- color: #92400E;
|
|
|
|
|
- flex: 1;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- &__arrow {
|
|
|
|
|
- font-size: 18px;
|
|
|
|
|
- color: #92400E;
|
|
|
|
|
- margin-left: 8px;
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// Seed-price badge
|
|
|
|
|
-.seed-badge {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- justify-content: center;
|
|
|
|
|
- padding: 6px 14px;
|
|
|
|
|
- border-radius: 8px;
|
|
|
|
|
- margin-bottom: 12px;
|
|
|
|
|
- background: $gold-light;
|
|
|
|
|
- border: 1px solid $gold;
|
|
|
|
|
-
|
|
|
|
|
- &__text {
|
|
|
|
|
- font-size: 12px;
|
|
|
|
|
- color: $blue-dark;
|
|
|
|
|
- font-weight: 500;
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// VIP status card
|
|
|
|
|
-.vip-card {
|
|
|
|
|
- border-radius: 12px;
|
|
|
|
|
- padding: 16px;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- justify-content: space-between;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- margin-bottom: 16px;
|
|
|
|
|
-
|
|
|
|
|
- &__body {
|
|
|
|
|
- flex: 1;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- &__title {
|
|
|
|
|
- font-weight: 600;
|
|
|
|
|
- font-size: 16px;
|
|
|
|
|
- display: block;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- &__desc {
|
|
|
|
|
- font-size: 12px;
|
|
|
|
|
- opacity: 0.8;
|
|
|
|
|
- display: block;
|
|
|
|
|
- margin-top: 4px;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- &__action {
|
|
|
|
|
- font-size: 14px;
|
|
|
|
|
- font-weight: 500;
|
|
|
|
|
- padding: 6px 14px;
|
|
|
|
|
- border-radius: 8px;
|
|
|
|
|
- white-space: nowrap;
|
|
|
|
|
- cursor: pointer;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Practitioner
|
|
|
|
|
- &--practitioner {
|
|
|
|
|
- background: linear-gradient(135deg, #7c3aed, #6d28d9);
|
|
|
|
|
- .vip-card__title, .vip-card__desc { color: #fff; }
|
|
|
|
|
- .vip-card__action {
|
|
|
|
|
- color: #7c3aed;
|
|
|
|
|
- background: rgba(255, 255, 255, 0.95);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Annual
|
|
|
|
|
- &--annual {
|
|
|
|
|
- background: linear-gradient(135deg, #f59e0b, #d97706);
|
|
|
|
|
- .vip-card__title, .vip-card__desc { color: #fff; }
|
|
|
|
|
- .vip-card__action {
|
|
|
|
|
- color: #d97706;
|
|
|
|
|
- background: rgba(255, 255, 255, 0.95);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Expired
|
|
|
|
|
- &--expired {
|
|
|
|
|
- background: #E5E7EB;
|
|
|
|
|
- .vip-card__title { color: $text-secondary; }
|
|
|
|
|
- .vip-card__desc { color: $text-muted; }
|
|
|
|
|
- .vip-card__action {
|
|
|
|
|
- color: $text-secondary;
|
|
|
|
|
- background: $white;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Upsell (free user — two options)
|
|
|
|
|
- &--upsell {
|
|
|
|
|
- background: linear-gradient(135deg, $blue-dark, darken($blue-dark, 4%));
|
|
|
|
|
- flex-direction: column;
|
|
|
|
|
- align-items: flex-start;
|
|
|
|
|
- gap: 14px;
|
|
|
|
|
-
|
|
|
|
|
- .vip-card__body {
|
|
|
|
|
- width: 100%;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .vip-card__title {
|
|
|
|
|
- color: $gold;
|
|
|
|
|
- font-size: 16px;
|
|
|
|
|
- margin-bottom: 8px;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .vip-card__dual {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- gap: 12px;
|
|
|
|
|
- width: 100%;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .vip-card__option {
|
|
|
|
|
- flex: 1;
|
|
|
|
|
- background: rgba(255, 255, 255, 0.06);
|
|
|
|
|
- border: 1px solid rgba(255, 255, 255, 0.08);
|
|
|
|
|
- border-radius: 12px;
|
|
|
|
|
- padding: 14px 12px;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- flex-direction: column;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- gap: 6px;
|
|
|
|
|
- cursor: pointer;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .vip-card__option:active {
|
|
|
|
|
- background: rgba(255, 255, 255, 0.1);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .vip-card__option .option-title {
|
|
|
|
|
- font-size: 15px;
|
|
|
|
|
- font-weight: 600;
|
|
|
|
|
- color: rgba(255, 255, 255, 0.9);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .vip-card__option .option-price {
|
|
|
|
|
- font-size: 18px;
|
|
|
|
|
- font-weight: 700;
|
|
|
|
|
- color: $gold;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .vip-card__option .option-desc {
|
|
|
|
|
- font-size: 11px;
|
|
|
|
|
- color: rgba(255, 255, 255, 0.45);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .vip-card__option .option-btn {
|
|
|
|
|
- width: 100%;
|
|
|
|
|
- border: none;
|
|
|
|
|
- border-radius: 8px;
|
|
|
|
|
- padding: 8px 0;
|
|
|
|
|
- font-size: 13px;
|
|
|
|
|
- font-weight: 600;
|
|
|
|
|
- cursor: pointer;
|
|
|
|
|
- margin-top: 4px;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .option-btn.light {
|
|
|
|
|
- background: rgba(255, 255, 255, 0.12);
|
|
|
|
|
- color: #fff;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .option-btn.primary {
|
|
|
|
|
- background: $gold;
|
|
|
|
|
- color: $blue-dark;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .vip-card__option--pro {
|
|
|
|
|
- border-color: rgba($gold, 0.2);
|
|
|
|
|
- background: rgba($gold, 0.06);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// Daily quota
|
|
|
|
|
-.quota-bar {
|
|
|
|
|
- background: $white;
|
|
|
|
|
- border-radius: 12px;
|
|
|
|
|
- padding: 16px;
|
|
|
|
|
- margin-bottom: 16px;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- justify-content: space-around;
|
|
|
|
|
-
|
|
|
|
|
- &__item {
|
|
|
|
|
- text-align: center;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- &__label {
|
|
|
|
|
- font-size: 12px;
|
|
|
|
|
- color: $text-secondary;
|
|
|
|
|
- display: block;
|
|
|
|
|
- margin-bottom: 4px;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- &__value {
|
|
|
|
|
- font-size: 20px;
|
|
|
|
|
- font-weight: 700;
|
|
|
|
|
- color: $text-primary;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- &__divider {
|
|
|
|
|
- width: 1px;
|
|
|
|
|
- height: 36px;
|
|
|
|
|
- background: #E5E7EB;
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// ========== Region 3: Feature Menu ==========
|
|
|
|
|
-.menu-section {
|
|
|
|
|
- background: $white;
|
|
|
|
|
- border-radius: 12px;
|
|
|
|
|
- margin-bottom: 16px;
|
|
|
|
|
- overflow: hidden;
|
|
|
|
|
-
|
|
|
|
|
- &__header {
|
|
|
|
|
- font-size: 12px;
|
|
|
|
|
- font-weight: 600;
|
|
|
|
|
- color: $text-muted;
|
|
|
|
|
- text-transform: uppercase;
|
|
|
|
|
- letter-spacing: 0.5px;
|
|
|
|
|
- padding: 14px 16px 6px;
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.menu-item {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- gap: 8px;
|
|
|
|
|
- padding: 14px 16px;
|
|
|
|
|
- border-bottom: 1px solid #F3F4F6;
|
|
|
|
|
- cursor: pointer;
|
|
|
|
|
- transition: background 0.15s;
|
|
|
|
|
-
|
|
|
|
|
- &:active {
|
|
|
|
|
- background: #F9FAFB;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- &:last-child {
|
|
|
|
|
- border-bottom: none;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- &__label {
|
|
|
|
|
- flex: 1;
|
|
|
|
|
- font-size: 15px;
|
|
|
|
|
- color: $text-primary;
|
|
|
|
|
-
|
|
|
|
|
- &--danger {
|
|
|
|
|
- color: $danger;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- &__badge {
|
|
|
|
|
- font-size: 10px;
|
|
|
|
|
- font-weight: 700;
|
|
|
|
|
- padding: 2px 6px;
|
|
|
|
|
- border-radius: 4px;
|
|
|
|
|
-
|
|
|
|
|
- &--new {
|
|
|
|
|
- background: #FEF3C7;
|
|
|
|
|
- color: #D97706;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- &__arrow {
|
|
|
|
|
- color: $text-muted;
|
|
|
|
|
- font-size: 18px;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- &--danger {
|
|
|
|
|
- justify-content: center;
|
|
|
|
|
- border-bottom: none;
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// Promotion tools card
|
|
|
|
|
-.promo-card {
|
|
|
|
|
- margin: 0 16px 14px;
|
|
|
|
|
- background: #FFFBEB;
|
|
|
|
|
- border: 1px solid #FDE68A;
|
|
|
|
|
- border-radius: 10px;
|
|
|
|
|
- overflow: hidden;
|
|
|
|
|
-
|
|
|
|
|
- &__code-area {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- justify-content: center;
|
|
|
|
|
- gap: 10px;
|
|
|
|
|
- padding: 14px 16px 8px;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- &__prefix {
|
|
|
|
|
- font-size: 13px;
|
|
|
|
|
- color: $text-secondary;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- &__code {
|
|
|
|
|
- font-size: 22px;
|
|
|
|
|
- font-weight: 700;
|
|
|
|
|
- color: $blue-dark;
|
|
|
|
|
- letter-spacing: 3px;
|
|
|
|
|
- font-family: monospace;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- &__actions {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- justify-content: center;
|
|
|
|
|
- gap: 10px;
|
|
|
|
|
- padding: 6px 16px 14px;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- &__btn {
|
|
|
|
|
- font-size: 12px;
|
|
|
|
|
- color: $blue-dark;
|
|
|
|
|
- font-weight: 500;
|
|
|
|
|
- cursor: pointer;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- &__divider {
|
|
|
|
|
- color: #D1D5DB;
|
|
|
|
|
- font-size: 12px;
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
|
|
+<template>
|
|
|
|
|
+ <view class="page-profile">
|
|
|
|
|
+ <!-- ========== Region 1: User Card ========== -->
|
|
|
|
|
+ <view class="user-card">
|
|
|
|
|
+ <!-- Login prompt for unauthenticated users -->
|
|
|
|
|
+ <view v-if="!userStore.isLoggedIn" class="user-card__login" @click="goLogin">
|
|
|
|
|
+ <view class="avatar avatar--empty">
|
|
|
|
|
+ <text class="avatar__icon">+</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <text class="user-card__nickname">未登录</text>
|
|
|
|
|
+ <text class="user-card__login-hint">点击登录 / 注册</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- Authenticated user card — tap to edit -->
|
|
|
|
|
+ <view v-else class="user-card__logged" @click="goEditProfile">
|
|
|
|
|
+ <view class="user-card__inner">
|
|
|
|
|
+ <view class="avatar">
|
|
|
|
|
+ <image :src="userStore.avatarUrl || '/static/default-avatar.png'" mode="aspectFill" />
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="user-card__info">
|
|
|
|
|
+ <text class="user-card__nickname">{{ userStore.nickname }}</text>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- Role tag -->
|
|
|
|
|
+ <view
|
|
|
|
|
+ class="user-card__tag"
|
|
|
|
|
+ :class="{
|
|
|
|
|
+ 'user-card__tag--practitioner': userStore.vipType === 'practitioner',
|
|
|
|
|
+ 'user-card__tag--annual': userStore.vipType === 'annual',
|
|
|
|
|
+ 'user-card__tag--normal': !userStore.vipType
|
|
|
|
|
+ }"
|
|
|
|
|
+ >
|
|
|
|
|
+ <text v-if="!userStore.vipType">普通用户</text>
|
|
|
|
|
+ <text v-else-if="userStore.vipType === 'annual'">C端会员</text>
|
|
|
|
|
+ <text v-else>能量师 🌟</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- Social summary (optional) -->
|
|
|
|
|
+ <text v-if="userStore.bio || userStore.city" class="user-card__summary">
|
|
|
|
|
+ {{ userStore.city || '' }}{{ userStore.city && userStore.bio ? ' · ' : '' }}{{ userStore.bio || '' }}
|
|
|
|
|
+ </text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <text class="user-card__arrow">›</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- ========== Region 2: Membership Status Cards ========== -->
|
|
|
|
|
+
|
|
|
|
|
+ <!-- US-5.1 §13: Profile incomplete banner -->
|
|
|
|
|
+ <view v-if="userStore.isLoggedIn && userStore.profileIncomplete" class="banner" @click="goEditProfile">
|
|
|
|
|
+ <text class="banner__text">📝 请完善个人资料,开启能量匹配</text>
|
|
|
|
|
+ <text class="banner__arrow">›</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- US-4.3: Renewal reminder (7 days before expiry) -->
|
|
|
|
|
+ <view v-if="isExpiringSoon" class="banner banner--warning" @click="goRenew">
|
|
|
|
|
+ <text class="banner__text">⏰ 您的会员即将到期,请及时续费</text>
|
|
|
|
|
+ <text class="banner__arrow">›</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- Seed-price badge -->
|
|
|
|
|
+ <view v-if="userStore.isSeedPriceUser" class="seed-badge">
|
|
|
|
|
+ <text class="seed-badge__text">🌱 种子价专属会员</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- VIP status card — role-based -->
|
|
|
|
|
+ <!-- Case 1: Energy master (practitioner, in valid period) -->
|
|
|
|
|
+ <view
|
|
|
|
|
+ v-if="userStore.vipType === 'practitioner' && userStore.isVip && !isExpired"
|
|
|
|
|
+ class="vip-card vip-card--practitioner"
|
|
|
|
|
+ >
|
|
|
|
|
+ <view class="vip-card__body">
|
|
|
|
|
+ <text class="vip-card__title">能量师会员 🌟</text>
|
|
|
|
|
+ <text class="vip-card__desc">
|
|
|
|
|
+ 有效期至 {{ formatDate(userStore.vipEndTime) }}
|
|
|
|
|
+ </text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <text class="vip-card__action" @click="goRenew">续费</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- Case 2: C-end annual subscriber (in valid period) -->
|
|
|
|
|
+ <view
|
|
|
|
|
+ v-else-if="userStore.vipType === 'annual' && userStore.isVip && !isExpired"
|
|
|
|
|
+ class="vip-card vip-card--annual"
|
|
|
|
|
+ >
|
|
|
|
|
+ <view class="vip-card__body">
|
|
|
|
|
+ <text class="vip-card__title">C端会员</text>
|
|
|
|
|
+ <text class="vip-card__desc">
|
|
|
|
|
+ 有效期至 {{ formatDate(userStore.vipEndTime) }}
|
|
|
|
|
+ </text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <text class="vip-card__action" @click="goUpgradePractitioner">升级能量师</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- Case 3: Expired member (vipEndTime < now) -->
|
|
|
|
|
+ <view v-else-if="userStore.vipType && isExpired" class="vip-card vip-card--expired">
|
|
|
|
|
+ <view class="vip-card__body">
|
|
|
|
|
+ <text class="vip-card__title">会员已过期</text>
|
|
|
|
|
+ <text class="vip-card__desc">
|
|
|
|
|
+ 过期时间 {{ formatDate(userStore.vipEndTime) }}
|
|
|
|
|
+ </text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <text class="vip-card__action" @click="goPayment">重新开通</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- Case 4: Free user — two payment options -->
|
|
|
|
|
+ <view v-else class="vip-card vip-card--upsell">
|
|
|
|
|
+ <view class="vip-card__body">
|
|
|
|
|
+ <text class="vip-card__title">选择方案</text>
|
|
|
|
|
+ <view class="vip-card__dual">
|
|
|
|
|
+ <view class="vip-card__option" @click="goPayment">
|
|
|
|
|
+ <text class="option-title">C端会员</text>
|
|
|
|
|
+ <text class="option-price">¥131/年</text>
|
|
|
|
|
+ <text class="option-desc">无限AI解读</text>
|
|
|
|
|
+ <button class="option-btn light">开通</button>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="vip-card__option vip-card__option--pro" @click="goUpgradePractitioner">
|
|
|
|
|
+ <text class="option-title">能量师</text>
|
|
|
|
|
+ <text class="option-price">¥1,314/年</text>
|
|
|
|
|
+ <text class="option-desc">完整功能+分销</text>
|
|
|
|
|
+ <button class="option-btn primary">申请</button>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- Daily quota (non-VIP only) -->
|
|
|
|
|
+ <view v-if="!userStore.isVip" class="quota-bar">
|
|
|
|
|
+ <view class="quota-bar__item">
|
|
|
|
|
+ <text class="quota-bar__label">今日分析</text>
|
|
|
|
|
+ <text class="quota-bar__value">{{ userStore.usedCharts }}/{{ userStore.maxCharts }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="quota-bar__divider"></view>
|
|
|
|
|
+ <view class="quota-bar__item">
|
|
|
|
|
+ <text class="quota-bar__label">今日解读</text>
|
|
|
|
|
+ <text class="quota-bar__value">{{ userStore.usedChats }}/{{ userStore.maxChats }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- ========== Region 3: Feature Menu ========== -->
|
|
|
|
|
+
|
|
|
|
|
+ <!-- Group 1: My Services -->
|
|
|
|
|
+ <view class="menu-section">
|
|
|
|
|
+ <view class="menu-section__header">我的服务</view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- Analysis history (all logged-in) -->
|
|
|
|
|
+ <view v-if="userStore.isLoggedIn" class="menu-item" @click="goRecords">
|
|
|
|
|
+ <text class="menu-item__label">📋 我的分析</text>
|
|
|
|
|
+ <text class="menu-item__arrow">›</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- Distribution panel (paid users only) -->
|
|
|
|
|
+ <view v-if="userStore.isLoggedIn && userStore.isVip" class="menu-item" @click="goCommission">
|
|
|
|
|
+ <text class="menu-item__label">💰 我的推广</text>
|
|
|
|
|
+ <text class="menu-item__arrow">›</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- Practitioner workbench (practitioners only) — EPIC 9 -->
|
|
|
|
|
+ <view v-if="userStore.vipType === 'practitioner' && userStore.isVip" class="menu-item" @click="goWorkbench">
|
|
|
|
|
+ <text class="menu-item__label">🔧 工作台</text>
|
|
|
|
|
+ <view class="menu-item__badge menu-item__badge--new">NEW</view>
|
|
|
|
|
+ <text class="menu-item__arrow">›</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- Group 2: Promotion Tools (paid users only) -->
|
|
|
|
|
+ <view v-if="userStore.isLoggedIn && userStore.isVip && userStore.referralCode" class="menu-section">
|
|
|
|
|
+ <view class="menu-section__header">推广工具</view>
|
|
|
|
|
+ <view class="promo-card">
|
|
|
|
|
+ <view class="promo-card__code-area">
|
|
|
|
|
+ <text class="promo-card__prefix">推广码</text>
|
|
|
|
|
+ <text class="promo-card__code">{{ userStore.referralCode }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="promo-card__actions">
|
|
|
|
|
+ <text class="promo-card__btn" @click="copyReferral">复制推广码</text>
|
|
|
|
|
+ <text class="promo-card__divider">|</text>
|
|
|
|
|
+ <text class="promo-card__btn" @click="generatePoster">生成推广海报</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- Group 3: More -->
|
|
|
|
|
+ <view class="menu-section">
|
|
|
|
|
+ <view class="menu-section__header">更多</view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- My tags (practitioner only, P2) -->
|
|
|
|
|
+ <view v-if="userStore.vipType === 'practitioner'" class="menu-item" @click="goTags">
|
|
|
|
|
+ <text class="menu-item__label">🏷 我的标签</text>
|
|
|
|
|
+ <text class="menu-item__arrow">›</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- About us (all) -->
|
|
|
|
|
+ <view class="menu-item" @click="goAbout">
|
|
|
|
|
+ <text class="menu-item__label">ℹ️ 关于我们</text>
|
|
|
|
|
+ <text class="menu-item__arrow">›</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- ========== Region 4: Logout ========== -->
|
|
|
|
|
+ <view v-if="userStore.isLoggedIn" class="menu-section">
|
|
|
|
|
+ <view class="menu-item menu-item--danger" @click="handleLogout">
|
|
|
|
|
+ <text class="menu-item__label menu-item__label--danger">退出登录</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- Hidden canvas for poster generation -->
|
|
|
|
|
+ <canvas canvas-id="posterCanvas" id="posterCanvas" class="poster-canvas" />
|
|
|
|
|
+ </view>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script setup>
|
|
|
|
|
+import { computed } from 'vue'
|
|
|
|
|
+import { onShow } from '@dcloudio/uni-app'
|
|
|
|
|
+import { useUserStore } from '@/stores/user'
|
|
|
|
|
+import { profileApi } from '@/utils/api'
|
|
|
|
|
+
|
|
|
|
|
+const userStore = useUserStore()
|
|
|
|
|
+
|
|
|
|
|
+// US-4.3: Renewal reminder if expiry within 7 days
|
|
|
|
|
+const isExpiringSoon = computed(() => {
|
|
|
|
|
+ if (!userStore.vipEndTime) return false
|
|
|
|
|
+ const expiry = new Date(userStore.vipEndTime)
|
|
|
|
|
+ const now = new Date()
|
|
|
|
|
+ const diffHours = (expiry - now) / (1000 * 60 * 60)
|
|
|
|
|
+ return diffHours > 0 && diffHours <= 7 * 24
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+// US-4.3: Expired check
|
|
|
|
|
+const isExpired = computed(() => {
|
|
|
|
|
+ if (!userStore.vipEndTime) return false
|
|
|
|
|
+ return new Date(userStore.vipEndTime) < new Date()
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+function formatDate(dateStr) {
|
|
|
|
|
+ if (!dateStr) return ''
|
|
|
|
|
+ return dateStr.slice(0, 10)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+onShow(() => {
|
|
|
|
|
+ if (userStore.isLoggedIn) {
|
|
|
|
|
+ userStore.fetchProfile()
|
|
|
|
|
+ userStore.fetchQuota()
|
|
|
|
|
+ userStore.fetchCommissionSummary()
|
|
|
|
|
+ }
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+function goLogin() {
|
|
|
|
|
+ uni.navigateTo({ url: '/pages/login/index' })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function goEditProfile() {
|
|
|
|
|
+ if (!userStore.isLoggedIn) {
|
|
|
|
|
+ uni.navigateTo({ url: '/pages/login/index' })
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ uni.navigateTo({ url: '/pages/profile/edit' })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function goPayment() {
|
|
|
|
|
+ uni.navigateTo({ url: '/pages/payment/index?product=annual' })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function goRenew() {
|
|
|
|
|
+ const product = userStore.vipType === 'practitioner' ? 'practitioner' : 'annual'
|
|
|
|
|
+ uni.navigateTo({ url: `/pages/payment/index?product=${product}` })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function goUpgradePractitioner() {
|
|
|
|
|
+ uni.navigateTo({ url: '/pages/payment/index?product=practitioner' })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function goRecords() {
|
|
|
|
|
+ uni.switchTab({ url: '/pages/records/index' })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function goCommission() {
|
|
|
|
|
+ if (!userStore.isLoggedIn) {
|
|
|
|
|
+ uni.navigateTo({ url: '/pages/login/index' })
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ uni.navigateTo({ url: '/pages/commission/index' })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// EPIC 9: Navigate to practitioner workbench
|
|
|
|
|
+function goWorkbench() {
|
|
|
|
|
+ uni.navigateTo({ url: '/pages/plan/workbench' })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function copyReferral() {
|
|
|
|
|
+ if (userStore.referralCode) {
|
|
|
|
|
+ uni.setClipboardData({
|
|
|
|
|
+ data: userStore.referralCode,
|
|
|
|
|
+ success: () => uni.showToast({ title: '已复制推广码', icon: 'success' })
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function generatePoster() {
|
|
|
|
|
+ uni.showLoading({ title: '生成海报…', mask: true })
|
|
|
|
|
+ const baseUrl = 'http://127.0.0.1:8186'
|
|
|
|
|
+
|
|
|
|
|
+ Promise.all([
|
|
|
|
|
+ downloadImage(userStore.avatarUrl),
|
|
|
|
|
+ profileApi.qrcode().then(res => {
|
|
|
|
|
+ const url = res.qrCodeUrl.startsWith('http') ? res.qrCodeUrl : baseUrl + res.qrCodeUrl
|
|
|
|
|
+ return downloadImage(url)
|
|
|
|
|
+ })
|
|
|
|
|
+ ]).then(([avatarPath, qrPath]) => {
|
|
|
|
|
+ const ctx = uni.createCanvasContext('posterCanvas', getCurrentInstance())
|
|
|
|
|
+ const W = 375, H = 670
|
|
|
|
|
+
|
|
|
|
|
+ // Background
|
|
|
|
|
+ const grd = ctx.createLinearGradient(0, 0, 0, H)
|
|
|
|
|
+ grd.addColorStop(0, '#0c0a1a')
|
|
|
|
|
+ grd.addColorStop(1, '#1a1040')
|
|
|
|
|
+ ctx.fillStyle = grd
|
|
|
|
|
+ ctx.fillRect(0, 0, W, H)
|
|
|
|
|
+
|
|
|
|
|
+ // Top glow
|
|
|
|
|
+ const topGlow = ctx.createLinearGradient(0, 0, 0, 220)
|
|
|
|
|
+ topGlow.addColorStop(0, 'rgba(90, 50, 180, 0.35)')
|
|
|
|
|
+ topGlow.addColorStop(1, 'rgba(90, 50, 180, 0)')
|
|
|
|
|
+ ctx.fillStyle = topGlow
|
|
|
|
|
+ ctx.fillRect(0, 0, W, 220)
|
|
|
|
|
+
|
|
|
|
|
+ // Gold accent line
|
|
|
|
|
+ ctx.fillStyle = '#C9A84C'
|
|
|
|
|
+ ctx.fillRect(W * 0.32, 28, W * 0.36, 1.5)
|
|
|
|
|
+
|
|
|
|
|
+ // Brand title
|
|
|
|
|
+ ctx.font = '13px sans-serif'
|
|
|
|
|
+ ctx.fillStyle = 'rgba(201, 168, 76, 0.7)'
|
|
|
|
|
+ ctx.textAlign = 'center'
|
|
|
|
|
+ ctx.fillText('数字能量学', W / 2, 52)
|
|
|
|
|
+
|
|
|
|
|
+ // Avatar (circular)
|
|
|
|
|
+ const avatarSize = 76
|
|
|
|
|
+ ctx.save()
|
|
|
|
|
+ ctx.beginPath()
|
|
|
|
|
+ ctx.arc(W / 2, 128, avatarSize / 2, 0, 2 * Math.PI)
|
|
|
|
|
+ ctx.closePath()
|
|
|
|
|
+ ctx.fillStyle = '#ffffff'
|
|
|
|
|
+ ctx.fill()
|
|
|
|
|
+ if (avatarPath) {
|
|
|
|
|
+ ctx.clip()
|
|
|
|
|
+ ctx.drawImage(avatarPath, (W - avatarSize) / 2, 128 - avatarSize / 2, avatarSize, avatarSize)
|
|
|
|
|
+ }
|
|
|
|
|
+ ctx.restore()
|
|
|
|
|
+
|
|
|
|
|
+ // Nickname
|
|
|
|
|
+ ctx.font = 'bold 18px sans-serif'
|
|
|
|
|
+ ctx.fillStyle = '#FFFFFF'
|
|
|
|
|
+ ctx.textAlign = 'center'
|
|
|
|
|
+ ctx.fillText(userStore.nickname || '数字能量探索者', W / 2, 192)
|
|
|
|
|
+
|
|
|
|
|
+ // Role badge
|
|
|
|
|
+ const roleText = userStore.vipType === 'practitioner' ? '能量师' : userStore.vipType === 'annual' ? 'C端会员' : '探索者'
|
|
|
|
|
+ ctx.font = '11px sans-serif'
|
|
|
|
|
+ ctx.fillStyle = userStore.vipType === 'practitioner' ? '#C9A84C' : 'rgba(255,255,255,0.5)'
|
|
|
|
|
+ ctx.fillText(roleText, W / 2, 214)
|
|
|
|
|
+
|
|
|
|
|
+ // Divider
|
|
|
|
|
+ ctx.fillStyle = 'rgba(255,255,255,0.07)'
|
|
|
|
|
+ ctx.fillRect(30, 240, W - 60, 1)
|
|
|
|
|
+
|
|
|
|
|
+ // Section title
|
|
|
|
|
+ ctx.font = '12px sans-serif'
|
|
|
|
|
+ ctx.fillStyle = 'rgba(255,255,255,0.38)'
|
|
|
|
|
+ ctx.fillText('推荐体验', W / 2, 270)
|
|
|
|
|
+
|
|
|
|
|
+ // QR code background
|
|
|
|
|
+ const qrSize = 160
|
|
|
|
|
+ const qrX = (W - qrSize) / 2
|
|
|
|
|
+ const qrY = 288
|
|
|
|
|
+ ctx.fillStyle = '#FFFFFF'
|
|
|
|
|
+ ctx.fillRect(qrX, qrY, qrSize, qrSize)
|
|
|
|
|
+ if (qrPath) {
|
|
|
|
|
+ ctx.drawImage(qrPath, qrX + 8, qrY + 8, qrSize - 16, qrSize - 16)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Hint
|
|
|
|
|
+ ctx.font = '11px sans-serif'
|
|
|
|
|
+ ctx.fillStyle = 'rgba(255,255,255,0.32)'
|
|
|
|
|
+ ctx.fillText('扫码开启你的数字能量之旅', W / 2, qrY + qrSize + 24)
|
|
|
|
|
+
|
|
|
|
|
+ // Bottom
|
|
|
|
|
+ ctx.font = '10px sans-serif'
|
|
|
|
|
+ ctx.fillStyle = 'rgba(255,255,255,0.18)'
|
|
|
|
|
+ ctx.fillText('数字能量学 · 数字能量智能体', W / 2, H - 22)
|
|
|
|
|
+
|
|
|
|
|
+ ctx.draw(false)
|
|
|
|
|
+
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ uni.canvasToTempFilePath({
|
|
|
|
|
+ canvasId: 'posterCanvas',
|
|
|
|
|
+ quality: 1,
|
|
|
|
|
+ success: (res) => {
|
|
|
|
|
+ uni.saveImageToPhotosAlbum({
|
|
|
|
|
+ filePath: res.tempFilePath,
|
|
|
|
|
+ success: () => {
|
|
|
|
|
+ uni.hideLoading()
|
|
|
|
|
+ uni.showToast({ title: '海报已保存到相册', icon: 'success' })
|
|
|
|
|
+ },
|
|
|
|
|
+ fail: (e) => {
|
|
|
|
|
+ uni.hideLoading()
|
|
|
|
|
+ if (e.errMsg && e.errMsg.includes('auth deny')) {
|
|
|
|
|
+ uni.showModal({
|
|
|
|
|
+ title: '保存海报',
|
|
|
|
|
+ content: '需要相册权限才能保存海报到本地',
|
|
|
|
|
+ confirmText: '去设置',
|
|
|
|
|
+ success: (r) => { if (r.confirm) uni.openSetting() }
|
|
|
|
|
+ })
|
|
|
|
|
+ } else {
|
|
|
|
|
+ uni.showToast({ title: '保存失败,请重试', icon: 'none' })
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ },
|
|
|
|
|
+ fail: () => {
|
|
|
|
|
+ uni.hideLoading()
|
|
|
|
|
+ uni.showToast({ title: '生成失败,请重试', icon: 'none' })
|
|
|
|
|
+ }
|
|
|
|
|
+ }, getCurrentInstance())
|
|
|
|
|
+ }, 150)
|
|
|
|
|
+
|
|
|
|
|
+ }).catch(e => {
|
|
|
|
|
+ console.error('generatePoster error:', e)
|
|
|
|
|
+ uni.hideLoading()
|
|
|
|
|
+ uni.showToast({ title: '生成失败,请重试', icon: 'none' })
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function downloadImage(url) {
|
|
|
|
|
+ if (!url) return Promise.resolve('')
|
|
|
|
|
+ return new Promise((resolve) => {
|
|
|
|
|
+ uni.downloadFile({
|
|
|
|
|
+ url,
|
|
|
|
|
+ success: (res) => resolve(res.tempFilePath),
|
|
|
|
|
+ fail: () => resolve('')
|
|
|
|
|
+ })
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function goTags() {
|
|
|
|
|
+ uni.navigateTo({ url: '/pages/tags/index' })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function goAbout() {
|
|
|
|
|
+ uni.navigateTo({ url: '/pages/about/index' })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function handleLogout() {
|
|
|
|
|
+ uni.showModal({
|
|
|
|
|
+ title: '确认退出',
|
|
|
|
|
+ content: '确定要退出登录吗?',
|
|
|
|
|
+ success: (res) => {
|
|
|
|
|
+ if (res.confirm) {
|
|
|
|
|
+ userStore.logout()
|
|
|
|
|
+ uni.showToast({ title: '已退出', icon: 'success' })
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style scoped lang="scss">
|
|
|
|
|
+// Brand colors (aligned with style-guide.md)
|
|
|
|
|
+$blue-dark: #1E3A5F;
|
|
|
|
|
+$gold: #C9A84C;
|
|
|
|
|
+$gold-light: #F8F0D8;
|
|
|
|
|
+$warm-white: #F8F6F1;
|
|
|
|
|
+$white: #FFFFFF;
|
|
|
|
|
+$text-primary: #1F2937;
|
|
|
|
|
+$text-secondary: #6B7280;
|
|
|
|
|
+$text-muted: #9CA3AF;
|
|
|
|
|
+$danger: #EF4444;
|
|
|
|
|
+
|
|
|
|
|
+.page-profile {
|
|
|
|
|
+ padding: 16px;
|
|
|
|
|
+ min-height: 100vh;
|
|
|
|
|
+ background: $warm-white;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ========== Region 1: User Card ==========
|
|
|
|
|
+.user-card {
|
|
|
|
|
+ background: linear-gradient(135deg, $blue-dark, darken($blue-dark, 6%));
|
|
|
|
|
+ border-radius: 16px;
|
|
|
|
|
+ padding: 20px 18px;
|
|
|
|
|
+ margin-bottom: 16px;
|
|
|
|
|
+
|
|
|
|
|
+ &__login {
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ padding: 8px 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &__logged {
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &__inner {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 14px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &__info {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ min-width: 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &__nickname {
|
|
|
|
|
+ font-size: 17px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: $white;
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &__login-hint {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: rgba(255, 255, 255, 0.7);
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ margin-top: 4px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &__tag {
|
|
|
|
|
+ display: inline-flex;
|
|
|
|
|
+ padding: 2px 12px;
|
|
|
|
|
+ border-radius: 20px;
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+ margin-top: 6px;
|
|
|
|
|
+
|
|
|
|
|
+ &--practitioner {
|
|
|
|
|
+ background: $gold;
|
|
|
|
|
+ color: $blue-dark;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &--annual {
|
|
|
|
|
+ background: rgba($gold, 0.25);
|
|
|
|
|
+ color: $gold;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &--normal {
|
|
|
|
|
+ background: rgba(255, 255, 255, 0.15);
|
|
|
|
|
+ color: rgba(255, 255, 255, 0.8);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &__summary {
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+ color: rgba(255, 255, 255, 0.55);
|
|
|
|
|
+ margin-top: 4px;
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ text-overflow: ellipsis;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &__arrow {
|
|
|
|
|
+ font-size: 22px;
|
|
|
|
|
+ color: rgba(255, 255, 255, 0.3);
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ padding-left: 4px;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Avatar
|
|
|
|
|
+.avatar {
|
|
|
|
|
+ width: 56px;
|
|
|
|
|
+ height: 56px;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+
|
|
|
|
|
+ image {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ border: 2px solid rgba(255, 255, 255, 0.25);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &--empty {
|
|
|
|
|
+ width: 56px;
|
|
|
|
|
+ height: 56px;
|
|
|
|
|
+ background: rgba(255, 255, 255, 0.12);
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ border: 2px dashed rgba(255, 255, 255, 0.25);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &__icon {
|
|
|
|
|
+ font-size: 24px;
|
|
|
|
|
+ color: rgba(255, 255, 255, 0.5);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ========== Region 2: Membership Status ==========
|
|
|
|
|
+
|
|
|
|
|
+// Banners
|
|
|
|
|
+.banner {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ padding: 10px 14px;
|
|
|
|
|
+ border-radius: 10px;
|
|
|
|
|
+ margin-bottom: 12px;
|
|
|
|
|
+ background: #FEF3C7;
|
|
|
|
|
+ border: 1px solid #FDE68A;
|
|
|
|
|
+
|
|
|
|
|
+ &--warning {
|
|
|
|
|
+ background: #FEF3C7;
|
|
|
|
|
+ border-color: #FDE68A;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &__text {
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ color: #92400E;
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &__arrow {
|
|
|
|
|
+ font-size: 18px;
|
|
|
|
|
+ color: #92400E;
|
|
|
|
|
+ margin-left: 8px;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Seed-price badge
|
|
|
|
|
+.seed-badge {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ padding: 6px 14px;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ margin-bottom: 12px;
|
|
|
|
|
+ background: $gold-light;
|
|
|
|
|
+ border: 1px solid $gold;
|
|
|
|
|
+
|
|
|
|
|
+ &__text {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: $blue-dark;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// VIP status card
|
|
|
|
|
+.vip-card {
|
|
|
|
|
+ border-radius: 12px;
|
|
|
|
|
+ padding: 16px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ margin-bottom: 16px;
|
|
|
|
|
+
|
|
|
|
|
+ &__body {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &__title {
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &__desc {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ opacity: 0.8;
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ margin-top: 4px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &__action {
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ padding: 6px 14px;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Practitioner
|
|
|
|
|
+ &--practitioner {
|
|
|
|
|
+ background: linear-gradient(135deg, #7c3aed, #6d28d9);
|
|
|
|
|
+ .vip-card__title, .vip-card__desc { color: #fff; }
|
|
|
|
|
+ .vip-card__action {
|
|
|
|
|
+ color: #7c3aed;
|
|
|
|
|
+ background: rgba(255, 255, 255, 0.95);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Annual
|
|
|
|
|
+ &--annual {
|
|
|
|
|
+ background: linear-gradient(135deg, #f59e0b, #d97706);
|
|
|
|
|
+ .vip-card__title, .vip-card__desc { color: #fff; }
|
|
|
|
|
+ .vip-card__action {
|
|
|
|
|
+ color: #d97706;
|
|
|
|
|
+ background: rgba(255, 255, 255, 0.95);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Expired
|
|
|
|
|
+ &--expired {
|
|
|
|
|
+ background: #E5E7EB;
|
|
|
|
|
+ .vip-card__title { color: $text-secondary; }
|
|
|
|
|
+ .vip-card__desc { color: $text-muted; }
|
|
|
|
|
+ .vip-card__action {
|
|
|
|
|
+ color: $text-secondary;
|
|
|
|
|
+ background: $white;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Upsell (free user — two options)
|
|
|
|
|
+ &--upsell {
|
|
|
|
|
+ background: linear-gradient(135deg, $blue-dark, darken($blue-dark, 4%));
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ align-items: flex-start;
|
|
|
|
|
+ gap: 14px;
|
|
|
|
|
+
|
|
|
|
|
+ .vip-card__body {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .vip-card__title {
|
|
|
|
|
+ color: $gold;
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ margin-bottom: 8px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .vip-card__dual {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .vip-card__option {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ background: rgba(255, 255, 255, 0.06);
|
|
|
|
|
+ border: 1px solid rgba(255, 255, 255, 0.08);
|
|
|
|
|
+ border-radius: 12px;
|
|
|
|
|
+ padding: 14px 12px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 6px;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .vip-card__option:active {
|
|
|
|
|
+ background: rgba(255, 255, 255, 0.1);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .vip-card__option .option-title {
|
|
|
|
|
+ font-size: 15px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: rgba(255, 255, 255, 0.9);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .vip-card__option .option-price {
|
|
|
|
|
+ font-size: 18px;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ color: $gold;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .vip-card__option .option-desc {
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+ color: rgba(255, 255, 255, 0.45);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .vip-card__option .option-btn {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ border: none;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ padding: 8px 0;
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ margin-top: 4px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .option-btn.light {
|
|
|
|
|
+ background: rgba(255, 255, 255, 0.12);
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .option-btn.primary {
|
|
|
|
|
+ background: $gold;
|
|
|
|
|
+ color: $blue-dark;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .vip-card__option--pro {
|
|
|
|
|
+ border-color: rgba($gold, 0.2);
|
|
|
|
|
+ background: rgba($gold, 0.06);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Daily quota
|
|
|
|
|
+.quota-bar {
|
|
|
|
|
+ background: $white;
|
|
|
|
|
+ border-radius: 12px;
|
|
|
|
|
+ padding: 16px;
|
|
|
|
|
+ margin-bottom: 16px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: space-around;
|
|
|
|
|
+
|
|
|
|
|
+ &__item {
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &__label {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: $text-secondary;
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ margin-bottom: 4px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &__value {
|
|
|
|
|
+ font-size: 20px;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ color: $text-primary;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &__divider {
|
|
|
|
|
+ width: 1px;
|
|
|
|
|
+ height: 36px;
|
|
|
|
|
+ background: #E5E7EB;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ========== Region 3: Feature Menu ==========
|
|
|
|
|
+.menu-section {
|
|
|
|
|
+ background: $white;
|
|
|
|
|
+ border-radius: 12px;
|
|
|
|
|
+ margin-bottom: 16px;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+
|
|
|
|
|
+ &__header {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: $text-muted;
|
|
|
|
|
+ text-transform: uppercase;
|
|
|
|
|
+ letter-spacing: 0.5px;
|
|
|
|
|
+ padding: 14px 16px 6px;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.menu-item {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+ padding: 14px 16px;
|
|
|
|
|
+ border-bottom: 1px solid #F3F4F6;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ transition: background 0.15s;
|
|
|
|
|
+
|
|
|
|
|
+ &:active {
|
|
|
|
|
+ background: #F9FAFB;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &:last-child {
|
|
|
|
|
+ border-bottom: none;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &__label {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ font-size: 15px;
|
|
|
|
|
+ color: $text-primary;
|
|
|
|
|
+
|
|
|
|
|
+ &--danger {
|
|
|
|
|
+ color: $danger;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &__badge {
|
|
|
|
|
+ font-size: 10px;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ padding: 2px 6px;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+
|
|
|
|
|
+ &--new {
|
|
|
|
|
+ background: #FEF3C7;
|
|
|
|
|
+ color: #D97706;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &__arrow {
|
|
|
|
|
+ color: $text-muted;
|
|
|
|
|
+ font-size: 18px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &--danger {
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ border-bottom: none;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Promotion tools card
|
|
|
|
|
+.promo-card {
|
|
|
|
|
+ margin: 0 16px 14px;
|
|
|
|
|
+ background: #FFFBEB;
|
|
|
|
|
+ border: 1px solid #FDE68A;
|
|
|
|
|
+ border-radius: 10px;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+
|
|
|
|
|
+ &__code-area {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+ padding: 14px 16px 8px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &__prefix {
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ color: $text-secondary;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &__code {
|
|
|
|
|
+ font-size: 22px;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ color: $blue-dark;
|
|
|
|
|
+ letter-spacing: 3px;
|
|
|
|
|
+ font-family: monospace;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &__actions {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+ padding: 6px 16px 14px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &__btn {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: $blue-dark;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &__divider {
|
|
|
|
|
+ color: #D1D5DB;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.poster-canvas {
|
|
|
|
|
+ position: fixed;
|
|
|
|
|
+ left: -9999px;
|
|
|
|
|
+ top: -9999px;
|
|
|
|
|
+ width: 375px;
|
|
|
|
|
+ height: 670px;
|
|
|
|
|
+}
|
|
|
</style>
|
|
</style>
|