| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477 |
- <template>
- <view class="page-profile" :class="themeClass">
- <!-- ========== Merged Region 1+2: User Card + Membership ========== -->
- <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">
- <view class="user-card__name-row">
- <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--super': userStore.vipType === 'super_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>
- </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>
- <view class="user-card__header-right">
- <view class="theme-toggle-btn" @click.stop="themeStore.toggle">
- <text class="theme-toggle-text">{{ themeStore.theme === 'dark' ? '亮' : '暗' }}</text>
- </view>
- <text class="user-card__arrow">›</text>
- </view>
- </view>
- </view>
- <!-- ========== Membership Status (merged into User Card) ========== -->
- <!-- 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: Super annual subscriber (in valid period) -->
- <view
- v-else-if="userStore.vipType === 'super_annual' && userStore.isVip && !isExpired"
- class="vip-card vip-card--super"
- >
- <view class="vip-card__body">
- <text class="vip-card__title">超级会员 🏆</text>
- <text class="vip-card__desc">
- 有效期至 {{ formatDate(userStore.vipEndTime) }}
- </text>
- <view class="vip-card__perks">
- <text class="perk-tag">关系能量</text>
- <text class="perk-tag">八星号码</text>
- <text class="perk-tag">分销推广</text>
- </view>
- </view>
- <text class="vip-card__action" @click="goUpgradePractitioner">升级能量师</text>
- </view>
- <!-- Case 3: 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>
- <view style="display:flex;gap:8px;flex-shrink:0;">
- <text class="vip-card__action" @click="goSuperMember">升超级</text>
- <text class="vip-card__action" @click="goUpgradePractitioner">升能量师</text>
- </view>
- </view>
- <!-- Case 4: 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 5: Free user — three 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" @click="goSuperMember">
- <text class="option-title">超级会员</text>
- <text class="option-price">¥1,314/年</text>
- <text class="option-desc">关系能量+号码分析</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>
- </view>
- <!-- ========== Region 3: Feature Menu ========== -->
- <!-- Group 1: My Services (practitioner workbench only) -->
- <view v-if="userStore.vipType === 'practitioner' && userStore.isVip" class="menu-section">
- <view class="menu-section__header">我的服务</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>
- <!-- ===== Promotion Center (paid users only) ===== -->
- <template v-if="userStore.isLoggedIn && userStore.isVip">
- <!-- 收益 -->
- <view class="promo-section earnings-section">
- <view class="promo-section__header">
- <text class="promo-section__title">💰 我的收益</text>
- </view>
- <view class="earnings-grid">
- <view class="earn-item">
- <text class="earn-label">累计收益</text>
- <text class="earn-value accent">¥{{ formatCommissionMoney(stats.totalEarnings) }}</text>
- </view>
- <view class="earn-item">
- <text class="earn-label">可提现余额</text>
- <text class="earn-value">¥{{ formatCommissionMoney(stats.availableBalance) }}</text>
- </view>
- <view class="earn-item">
- <text class="earn-label">本月收益</text>
- <text class="earn-value">¥{{ formatCommissionMoney(stats.monthEarnings) }}</text>
- </view>
- <view class="earn-item">
- <text class="earn-label">今日预估</text>
- <text class="earn-value">¥{{ formatCommissionMoney(stats.todayEstimate) }}</text>
- </view>
- </view>
- <view v-if="l1Total > 0 || l2Total > 0" class="earnings-breakdown">
- <view class="breakdown-row">
- <text class="breakdown-label">一级推广佣金</text>
- <text class="breakdown-value">¥{{ formatCommissionMoney(l1Total) }}</text>
- </view>
- <view class="breakdown-row">
- <text class="breakdown-label">二级推广佣金</text>
- <text class="breakdown-value">¥{{ formatCommissionMoney(l2Total) }}</text>
- </view>
- </view>
- </view>
- <!-- 团队 -->
- <view class="promo-section">
- <view class="promo-section__header">
- <text class="promo-section__title">👥 我的团队</text>
- <text v-if="team.length > 0" class="promo-section__sub" @click="showTeamDetail">查看全部 →</text>
- </view>
- <view class="team-grid">
- <view class="team-stat">
- <text class="stat-num">{{ stats.directInvites }}</text>
- <text class="stat-label">直接下级</text>
- </view>
- <view class="team-stat">
- <text class="stat-num">{{ stats.indirectInvites }}</text>
- <text class="stat-label">间接下级</text>
- </view>
- <view class="team-stat">
- <text class="stat-num">{{ stats.convertedCount }}</text>
- <text class="stat-label">下级升级</text>
- </view>
- <view class="team-stat">
- <text class="stat-num">{{ stats.totalCommissions }}</text>
- <text class="stat-label">佣金笔数</text>
- </view>
- </view>
- <view v-if="team.length > 0" class="team-preview">
- <view v-for="(m, idx) in team.slice(0, 5)" :key="idx" class="member-row">
- <image class="member-avatar" :src="m.avatarUrl || '/static/default-avatar.png'" mode="aspectFill" />
- <view class="member-info">
- <text class="member-name">{{ m.nickname || '微信用户' }}</text>
- <text class="member-date">{{ formatDate(m.createdAt) }} 加入</text>
- </view>
- <text v-if="m.isVip" class="member-vip">VIP</text>
- </view>
- </view>
- <view v-else class="promo-empty">
- <text class="promo-empty-text">暂无推广成员</text>
- </view>
- </view>
- <!-- 佣金明细 -->
- <view class="promo-section">
- <view class="promo-section__header">
- <text class="promo-section__title">📋 佣金明细</text>
- </view>
- <view v-if="commissions.length === 0" class="promo-empty">
- <text class="promo-empty-text">暂无佣金记录</text>
- </view>
- <view v-for="(c, idx) in commissions" :key="idx" class="commission-row">
- <view class="comm-left">
- <text class="comm-amount">+¥{{ (c.amount / 100).toFixed(2) }}</text>
- <text class="comm-level">{{ c.level === 1 ? '一级推广' : '二级推广' }}</text>
- <text v-if="c.remark" class="comm-remark">{{ c.remark }}</text>
- </view>
- <view class="comm-right">
- <text class="comm-status" :class="'status-' + c.status">{{ commissionStatusText(c.status) }}</text>
- <text class="comm-date">{{ formatDate(c.createdAt) }}</text>
- </view>
- </view>
- </view>
- </template>
- <!-- 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 { ref, computed, getCurrentInstance } from 'vue'
- import { onShow } from '@dcloudio/uni-app'
- import { useUserStore } from '@/stores/user'
- import { useThemeStore } from '@/stores/theme'
- import { useTheme } from '@/composables/useThemeStyle'
- import { profileApi, commissionApi } from '@/utils/api'
- const userStore = useUserStore()
- const themeStore = useThemeStore()
- const { themeClass } = useTheme()
- // Commission reactive data
- const commissions = ref([])
- const team = ref([])
- const stats = ref({
- totalEarnings: 0,
- availableBalance: 0,
- todayEstimate: 0,
- monthEarnings: 0,
- directInvites: 0,
- indirectInvites: 0,
- convertedCount: 0,
- totalCommissions: 0
- })
- const l1Total = computed(() =>
- commissions.value.filter(c => c.level === 1 && c.status === 'settled').reduce((s, c) => s + (c.amount || 0), 0)
- )
- const l2Total = computed(() =>
- commissions.value.filter(c => c.level === 2 && c.status === 'settled').reduce((s, c) => s + (c.amount || 0), 0)
- )
- // 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(() => {
- themeStore.init()
- if (userStore.isLoggedIn) {
- userStore.fetchProfile()
- userStore.fetchQuota()
- userStore.fetchCommissionSummary()
- fetchCommissionData()
- }
- })
- 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 goSuperMember() {
- uni.navigateTo({ url: '/pages/payment/index?product=super_member' })
- }
- function formatCommissionMoney(cents) {
- if (cents == null) return '0.00'
- return (cents / 100).toFixed(2)
- }
- function commissionStatusText(status) {
- const map = { settled: '已结算', pending: '处理中', withdrawn: '已提现', available: '可提现' }
- return map[status] || status
- }
- async function fetchCommissionData() {
- if (!userStore.isVip) return
- try {
- const [listData, teamData, statsData] = await Promise.all([
- commissionApi.list(),
- commissionApi.team(),
- commissionApi.stats()
- ])
- commissions.value = listData || []
- team.value = teamData || []
- if (statsData) stats.value = statsData
- } catch (e) { /* ignore */ }
- }
- function showTeamDetail() {
- uni.showModal({
- title: '推广团队',
- content: `直接下级 ${stats.value.directInvites} 人\n间接下级 ${stats.value.indirectInvites} 人\n已升级VIP ${stats.value.convertedCount} 人`,
- showCancel: false
- })
- }
- // 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')
- 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 (using CSS custom properties — responsive to theme)
- $blue-dark: var(--color-brand);
- $gold: var(--color-gold);
- $gold-light: var(--color-gold-light);
- $warm-white: var(--color-bg);
- $white: #FFFFFF;
- $card-bg: var(--color-bg-card);
- $text-primary: var(--color-text-primary);
- $text-secondary: var(--color-text-secondary);
- $text-muted: var(--color-text-tertiary);
- $danger: var(--color-danger);
- // ===== Theme Variables (class-based, replaces inline :style) =====
- .page-profile {
- // Default: Dark Theme
- --color-bg: #0c0a1a;
- --color-bg-secondary: #120d28;
- --color-bg-tertiary: #0f0c1e;
- --color-bg-card: rgba(255, 255, 255, 0.06);
- --color-bg-card-hover: rgba(255, 255, 255, 0.1);
- --color-bg-input: rgba(255, 255, 255, 0.1);
- --color-bg-tag: rgba(255, 255, 255, 0.08);
- --color-bg-overlay: rgba(12, 10, 26, 0.85);
- --color-text-primary: #FFFFFF;
- --color-text-secondary: #CBD5E1;
- --color-text-tertiary: rgba(255, 255, 255, 0.6);
- --color-text-link: #60A5FA;
- --color-text-gold: #C9A84C;
- --color-brand: #1E3A5F;
- --color-brand-light: #1E3A5F;
- --color-elevated: #172943;
- --color-gold: #C9A84C;
- --color-gold-bg: rgba(201, 168, 76, 0.15);
- --color-gold-light: #FEF3C7;
- --color-border: rgba(255, 255, 255, 0.1);
- --color-border-light: rgba(255, 255, 255, 0.06);
- --color-border-focus: rgba(201, 168, 76, 0.5);
- --color-success: #10b981;
- --color-success-bg: rgba(16, 185, 129, 0.15);
- --color-warning: #fbbf24;
- --color-warning-bg: rgba(251, 191, 36, 0.15);
- --color-danger: #ef4444;
- --color-danger-bg: rgba(239, 68, 68, 0.15);
- --color-info: #3B82F6;
- --color-info-bg: rgba(59, 130, 246, 0.15);
- --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3);
- --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4);
- --shadow-card: 0 4px 20px rgba(0, 0, 0, 0.3);
- --shadow-modal: 0 10px 40px rgba(0, 0, 0, 0.5);
- --shadow-btn: 0 4px 15px rgba(30, 58, 95, 0.4);
- --shadow-gold: 0 4px 15px rgba(201, 168, 76, 0.3);
- // Light Theme override
- &.theme-light {
- --color-bg: #F8F6F1;
- --color-bg-secondary: #F0EDE6;
- --color-bg-tertiary: #E8E5DE;
- --color-bg-card: #FFFFFF;
- --color-bg-card-hover: #F7F8FC;
- --color-bg-input: #F3F4F6;
- --color-bg-tag: #F3F4F6;
- --color-bg-overlay: rgba(248, 246, 241, 0.9);
- --color-text-primary: #1A1A2E;
- --color-text-secondary: #4A5568;
- --color-text-tertiary: #9CA3AF;
- --color-text-link: #2563EB;
- --color-text-gold: #92400E;
- --color-brand: #1E3A5F;
- --color-brand-light: #1E3A5F;
- --color-elevated: #2C5282;
- --color-gold: #C9A84C;
- --color-gold-bg: #FEF3C7;
- --color-gold-light: #FEF3C7;
- --color-border: #E2E8F0;
- --color-border-light: #F3F4F6;
- --color-border-focus: rgba(201, 168, 76, 0.6);
- --color-success: #059669;
- --color-success-bg: #D1FAE5;
- --color-warning: #D97706;
- --color-warning-bg: #FEF3C7;
- --color-danger: #DC2626;
- --color-danger-bg: #FEE2E2;
- --color-info: #2563EB;
- --color-info-bg: #DBEAFE;
- --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.06);
- --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08);
- --shadow-card: 0 2px 12px rgba(0, 0, 0, 0.06);
- --shadow-modal: 0 8px 30px rgba(0, 0, 0, 0.1);
- --shadow-btn: 0 4px 12px rgba(30, 58, 95, 0.2);
- --shadow-gold: 0 4px 12px rgba(201, 168, 76, 0.2);
- }
- }
- .page-profile {
- padding: 16px;
- min-height: 100vh;
- background: $warm-white;
- }
- // ========== Merged Region 1+2: User Card + Membership ==========
- .user-card {
- background: linear-gradient(135deg, var(--color-brand), var(--color-elevated));
- border-radius: 16px;
- padding: 20px 18px;
- margin-bottom: 24px;
- &__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;
- }
- &__name-row {
- display: flex;
- align-items: center;
- gap: 8px;
- flex-wrap: wrap;
- }
- &__nickname {
- font-size: 17px;
- font-weight: 600;
- color: $white;
- }
- &__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;
- background: rgba(255, 255, 255, 0.22);
- color: #fff;
- &--practitioner {
- background: $gold;
- color: #fff;
- font-weight: 600;
- }
- &--annual {
- background: $gold;
- color: #fff;
- font-weight: 600;
- }
- &--super {
- background: $gold;
- color: #fff;
- font-weight: 600;
- }
- &--normal {
- background: rgba(255, 255, 255, 0.22);
- color: #fff;
- }
- }
- &__summary {
- font-size: 11px;
- color: rgba(255, 255, 255, 0.78);
- 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;
- }
- &__header-right {
- display: flex;
- align-items: center;
- gap: 8px;
- flex-shrink: 0;
- align-self: flex-start;
- padding-top: 2px;
- }
- }
- // Theme toggle button
- .theme-toggle-btn {
- width: 36px;
- height: 36px;
- display: flex;
- align-items: center;
- justify-content: center;
- background: rgba(255, 255, 255, 0.2);
- border: 1.5px solid rgba(255, 255, 255, 0.35);
- border-radius: 50%;
- cursor: pointer;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
- }
- .theme-toggle-text {
- font-size: 13px;
- font-weight: 700;
- color: #fff;
- line-height: 1;
- }
- // ===== Promotion Center Sections =====
- .promo-section {
- background: var(--color-bg-card);
- border-radius: 14px;
- padding: 16px;
- margin-bottom: 16px;
- &__header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 14px;
- }
- &__title {
- font-size: 16px;
- font-weight: 700;
- color: $text-primary;
- }
- &__sub {
- font-size: 12px;
- color: #3B82F6;
- font-weight: 500;
- }
- }
- .earnings-section {
- background: linear-gradient(135deg, var(--color-brand), var(--color-elevated));
- .promo-section__title {
- color: rgba(255,255,255,0.9);
- }
- }
- .earnings-grid {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 12px;
- }
- .earn-item {
- text-align: center;
- padding: 8px 0;
- }
- .earn-label {
- font-size: 11px;
- color: rgba(255,255,255,0.78);
- display: block;
- margin-bottom: 4px;
- }
- .earn-value {
- font-size: 22px;
- font-weight: 700;
- color: #fff;
- display: block;
- }
- .earn-value.accent {
- color: var(--color-gold);
- }
- .earnings-breakdown {
- margin-top: 12px;
- padding: 10px 14px;
- background: rgba(255,255,255,0.03);
- border-radius: 8px;
- }
- .breakdown-row {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 4px 0;
- }
- .breakdown-label {
- font-size: 12px;
- color: rgba(255,255,255,0.7);
- }
- .breakdown-value {
- font-size: 13px;
- font-weight: 500;
- color: rgba(255,255,255,0.9);
- }
- // Team
- .team-grid {
- display: grid;
- grid-template-columns: 1fr 1fr 1fr 1fr;
- gap: 8px;
- margin-bottom: 14px;
- }
- .team-stat {
- text-align: center;
- padding: 8px 0;
- }
- .stat-num {
- font-size: 24px;
- font-weight: 700;
- color: var(--color-text-primary);
- display: block;
- }
- .stat-label {
- font-size: 11px;
- color: $text-secondary;
- margin-top: 2px;
- display: block;
- }
- .team-preview {
- border-top: 1px solid var(--color-border);
- padding-top: 10px;
- }
- .member-row {
- display: flex;
- align-items: center;
- padding: 8px 0;
- border-bottom: 1px solid var(--color-bg);
- }
- .member-row:last-child { border-bottom: none; }
- .member-avatar {
- width: 32px;
- height: 32px;
- border-radius: 50%;
- margin-right: 10px;
- flex-shrink: 0;
- }
- .member-info { flex: 1; }
- .member-name {
- font-size: 13px;
- color: $text-primary;
- display: block;
- }
- .member-date {
- font-size: 11px;
- color: $text-secondary;
- margin-top: 1px;
- display: block;
- }
- .member-vip {
- font-size: 10px;
- color: var(--color-gold);
- background: #FEF3C7;
- padding: 2px 8px;
- border-radius: 4px;
- font-weight: 500;
- }
- .promo-empty {
- padding: 24px 0;
- text-align: center;
- }
- .promo-empty-text {
- font-size: 13px;
- color: $text-secondary;
- }
- // Commission List
- .commission-row {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 12px 0;
- border-bottom: 1px solid var(--color-border);
- }
- .commission-row:last-child { border-bottom: none; }
- .comm-left { flex: 1; }
- .comm-amount {
- font-size: 15px;
- font-weight: 600;
- color: #2D8B67;
- display: block;
- }
- .comm-level {
- font-size: 12px;
- color: $text-secondary;
- margin-top: 2px;
- display: block;
- }
- .comm-remark {
- font-size: 11px;
- color: $text-secondary;
- margin-top: 1px;
- display: block;
- }
- .comm-right { text-align: right; }
- .comm-status {
- font-size: 11px;
- padding: 2px 8px;
- border-radius: 4px;
- display: inline-block;
- margin-bottom: 2px;
- }
- .comm-status.status-settled { background: #ECFDF5; color: #2D8B67; }
- .comm-status.status-pending { background: #FEF3C7; color: #D4A017; }
- .comm-status.status-withdrawn { background: var(--color-bg-input); color: $text-secondary; }
- .comm-status.status-available { background: #EFF6FF; color: #3B82F6; }
- .comm-date {
- font-size: 11px;
- color: $text-secondary;
- display: block;
- }
- // 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);
- }
- }
- // ========== (merged into Region 1+2) ==========
- // 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-top: 16px;
- &__body {
- flex: 1;
- }
- &__title {
- font-weight: 600;
- font-size: 16px;
- display: block;
- }
- &__desc {
- font-size: 12px;
- opacity: 0.92;
- 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);
- }
- }
- // Super member
- &--super {
- background: linear-gradient(135deg, #C9A84C, #B8922E);
- .vip-card__title, .vip-card__desc { color: #1E3A5F; }
- .vip-card__action {
- color: #C9A84C;
- background: #1E3A5F;
- }
- .vip-card__perks {
- display: flex;
- gap: 6px;
- margin-top: 6px;
- }
- .perk-tag {
- font-size: 10px;
- font-weight: 500;
- padding: 2px 8px;
- border-radius: 4px;
- background: rgba(30, 58, 95, 0.15);
- color: #1E3A5F;
- }
- }
- // 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-primary; }
- .vip-card__desc { color: $text-secondary; }
- .vip-card__action {
- color: #fff;
- background: $text-secondary;
- }
- }
- // Upsell (free user — two options)
- &--upsell {
- background: linear-gradient(135deg, var(--color-brand), var(--color-elevated));
- 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.7);
- }
- .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(201, 168, 76, 0.2);
- background: rgba(201, 168, 76, 0.06);
- }
- }
- }
- // Daily quota
- .quota-bar {
- background: $card-bg;
- 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: $card-bg;
- border-radius: 12px;
- margin-bottom: 16px;
- overflow: hidden;
- &__header {
- font-size: 12px;
- font-weight: 600;
- color: $text-secondary;
- 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-secondary;
- 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: $blue-dark;
- }
- &__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>
|