| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538 |
- <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">
- <text>推广工具</text>
- <text class="menu-section__more" @click="goCommission">查看详细 ›</text>
- </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 type="2d" 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 ''
- // Handle Jackson array format: [2026, 6, 8, 23, 59, 59]
- if (Array.isArray(dateStr)) {
- const [y, m, d] = dateStr
- return `${y}-${String(m).padStart(2, '0')}-${String(d).padStart(2, '0')}`
- }
- // Handle ISO string: "2026-06-08T23:59:59"
- 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'
- // Get canvas node via selector query (Canvas 2D API)
- const query = uni.createSelectorQuery()
- query.select('#posterCanvas').fields({ node: true, size: true }).exec((res) => {
- if (!res || !res[0]) {
- uni.hideLoading()
- uni.showToast({ title: '生成失败,请重试', icon: 'none' })
- return
- }
- const canvas = res[0].node
- const ctx = canvas.getContext('2d')
- const W = 375, H = 670
- const dpr = uni.getSystemInfoSync().pixelRatio || 2
- canvas.width = W * dpr
- canvas.height = H * dpr
- ctx.scale(dpr, dpr)
- // Download images first
- 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]) => {
- // Load images into Canvas 2D Image objects
- const imgPromises = []
- let avatarImg = null
- let qrImg = null
- if (avatarPath) {
- imgPromises.push(new Promise((resolve) => {
- const img = canvas.createImage()
- img.onload = () => { avatarImg = img; resolve() }
- img.onerror = () => resolve()
- img.src = avatarPath
- }))
- }
- if (qrPath) {
- imgPromises.push(new Promise((resolve) => {
- const img = canvas.createImage()
- img.onload = () => { qrImg = img; resolve() }
- img.onerror = () => resolve()
- img.src = qrPath
- }))
- }
- Promise.all(imgPromises).then(() => {
- // ─── 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 (avatarImg) {
- ctx.clip()
- ctx.drawImage(avatarImg, (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 (qrImg) {
- ctx.drawImage(qrImg, 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)
- // Canvas 2D draws immediately — no ctx.draw() needed
- // Export to temp file (pass canvas object, not canvasId)
- uni.canvasToTempFilePath({
- canvas,
- 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' })
- }
- })
- })
- }).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.showToast({ title: '功能开发中', icon: 'none' })
- }
- function goCommission() {
- uni.navigateTo({ url: '/pages/commission/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;
- display: flex;
- justify-content: space-between;
- align-items: center;
- }
- &__more {
- font-size: 12px;
- font-weight: 500;
- color: #3B82F6;
- text-transform: none;
- letter-spacing: 0;
- cursor: pointer;
- }
- }
- .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>
|