index.vue 40 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477
  1. <template>
  2. <view class="page-profile" :class="themeClass">
  3. <!-- ========== Merged Region 1+2: User Card + Membership ========== -->
  4. <view class="user-card">
  5. <!-- Login prompt for unauthenticated users -->
  6. <view v-if="!userStore.isLoggedIn" class="user-card__login" @click="goLogin">
  7. <view class="avatar avatar--empty">
  8. <text class="avatar__icon">+</text>
  9. </view>
  10. <text class="user-card__nickname">未登录</text>
  11. <text class="user-card__login-hint">点击登录 / 注册</text>
  12. </view>
  13. <!-- Authenticated user card — tap to edit -->
  14. <view v-else class="user-card__logged" @click="goEditProfile">
  15. <view class="user-card__inner">
  16. <view class="avatar">
  17. <image :src="userStore.avatarUrl || '/static/default-avatar.png'" mode="aspectFill" />
  18. </view>
  19. <view class="user-card__info">
  20. <view class="user-card__name-row">
  21. <text class="user-card__nickname">{{ userStore.nickname }}</text>
  22. <!-- Role tag -->
  23. <view
  24. class="user-card__tag"
  25. :class="{
  26. 'user-card__tag--practitioner': userStore.vipType === 'practitioner',
  27. 'user-card__tag--annual': userStore.vipType === 'annual',
  28. 'user-card__tag--super': userStore.vipType === 'super_annual',
  29. 'user-card__tag--normal': !userStore.vipType
  30. }"
  31. >
  32. <text v-if="!userStore.vipType">普通用户</text>
  33. <text v-else-if="userStore.vipType === 'annual'">C端会员</text>
  34. <text v-else>能量师 🌟</text>
  35. </view>
  36. </view>
  37. <!-- Social summary (optional) -->
  38. <text v-if="userStore.bio || userStore.city" class="user-card__summary">
  39. {{ userStore.city || '' }}{{ userStore.city && userStore.bio ? ' · ' : '' }}{{ userStore.bio || '' }}
  40. </text>
  41. </view>
  42. <view class="user-card__header-right">
  43. <view class="theme-toggle-btn" @click.stop="themeStore.toggle">
  44. <text class="theme-toggle-text">{{ themeStore.theme === 'dark' ? '亮' : '暗' }}</text>
  45. </view>
  46. <text class="user-card__arrow">›</text>
  47. </view>
  48. </view>
  49. </view>
  50. <!-- ========== Membership Status (merged into User Card) ========== -->
  51. <!-- US-5.1 §13: Profile incomplete banner -->
  52. <view v-if="userStore.isLoggedIn && userStore.profileIncomplete" class="banner" @click="goEditProfile">
  53. <text class="banner__text">📝 请完善个人资料,开启能量匹配</text>
  54. <text class="banner__arrow">›</text>
  55. </view>
  56. <!-- US-4.3: Renewal reminder (7 days before expiry) -->
  57. <view v-if="isExpiringSoon" class="banner banner--warning" @click="goRenew">
  58. <text class="banner__text">⏰ 您的会员即将到期,请及时续费</text>
  59. <text class="banner__arrow">›</text>
  60. </view>
  61. <!-- Seed-price badge -->
  62. <view v-if="userStore.isSeedPriceUser" class="seed-badge">
  63. <text class="seed-badge__text">🌱 种子价专属会员</text>
  64. </view>
  65. <!-- VIP status card — role-based -->
  66. <!-- Case 1: Energy master (practitioner, in valid period) -->
  67. <view
  68. v-if="userStore.vipType === 'practitioner' && userStore.isVip && !isExpired"
  69. class="vip-card vip-card--practitioner"
  70. >
  71. <view class="vip-card__body">
  72. <text class="vip-card__title">能量师会员 🌟</text>
  73. <text class="vip-card__desc">
  74. 有效期至 {{ formatDate(userStore.vipEndTime) }}
  75. </text>
  76. </view>
  77. <text class="vip-card__action" @click="goRenew">续费</text>
  78. </view>
  79. <!-- Case 2: Super annual subscriber (in valid period) -->
  80. <view
  81. v-else-if="userStore.vipType === 'super_annual' && userStore.isVip && !isExpired"
  82. class="vip-card vip-card--super"
  83. >
  84. <view class="vip-card__body">
  85. <text class="vip-card__title">超级会员 🏆</text>
  86. <text class="vip-card__desc">
  87. 有效期至 {{ formatDate(userStore.vipEndTime) }}
  88. </text>
  89. <view class="vip-card__perks">
  90. <text class="perk-tag">关系能量</text>
  91. <text class="perk-tag">八星号码</text>
  92. <text class="perk-tag">分销推广</text>
  93. </view>
  94. </view>
  95. <text class="vip-card__action" @click="goUpgradePractitioner">升级能量师</text>
  96. </view>
  97. <!-- Case 3: C-end annual subscriber (in valid period) -->
  98. <view
  99. v-else-if="userStore.vipType === 'annual' && userStore.isVip && !isExpired"
  100. class="vip-card vip-card--annual"
  101. >
  102. <view class="vip-card__body">
  103. <text class="vip-card__title">C端会员</text>
  104. <text class="vip-card__desc">
  105. 有效期至 {{ formatDate(userStore.vipEndTime) }}
  106. </text>
  107. </view>
  108. <view style="display:flex;gap:8px;flex-shrink:0;">
  109. <text class="vip-card__action" @click="goSuperMember">升超级</text>
  110. <text class="vip-card__action" @click="goUpgradePractitioner">升能量师</text>
  111. </view>
  112. </view>
  113. <!-- Case 4: Expired member (vipEndTime < now) -->
  114. <view v-else-if="userStore.vipType && isExpired" class="vip-card vip-card--expired">
  115. <view class="vip-card__body">
  116. <text class="vip-card__title">会员已过期</text>
  117. <text class="vip-card__desc">
  118. 过期时间 {{ formatDate(userStore.vipEndTime) }}
  119. </text>
  120. </view>
  121. <text class="vip-card__action" @click="goPayment">重新开通</text>
  122. </view>
  123. <!-- Case 5: Free user — three payment options -->
  124. <view v-else class="vip-card vip-card--upsell">
  125. <view class="vip-card__body">
  126. <text class="vip-card__title">选择方案</text>
  127. <view class="vip-card__dual">
  128. <view class="vip-card__option" @click="goPayment">
  129. <text class="option-title">C端会员</text>
  130. <text class="option-price">¥131/年</text>
  131. <text class="option-desc">无限AI解读</text>
  132. <button class="option-btn light">开通</button>
  133. </view>
  134. <view class="vip-card__option" @click="goSuperMember">
  135. <text class="option-title">超级会员</text>
  136. <text class="option-price">¥1,314/年</text>
  137. <text class="option-desc">关系能量+号码分析</text>
  138. <button class="option-btn light">开通</button>
  139. </view>
  140. <view class="vip-card__option vip-card__option--pro" @click="goUpgradePractitioner">
  141. <text class="option-title">能量师</text>
  142. <text class="option-price">¥1,314/年</text>
  143. <text class="option-desc">完整功能+分销</text>
  144. <button class="option-btn primary">申请</button>
  145. </view>
  146. </view>
  147. </view>
  148. </view>
  149. <!-- Daily quota (non-VIP only) -->
  150. <view v-if="!userStore.isVip" class="quota-bar">
  151. <view class="quota-bar__item">
  152. <text class="quota-bar__label">今日分析</text>
  153. <text class="quota-bar__value">{{ userStore.usedCharts }}/{{ userStore.maxCharts }}</text>
  154. </view>
  155. <view class="quota-bar__divider"></view>
  156. <view class="quota-bar__item">
  157. <text class="quota-bar__label">今日解读</text>
  158. <text class="quota-bar__value">{{ userStore.usedChats }}/{{ userStore.maxChats }}</text>
  159. </view>
  160. </view>
  161. </view>
  162. <!-- ========== Region 3: Feature Menu ========== -->
  163. <!-- Group 1: My Services (practitioner workbench only) -->
  164. <view v-if="userStore.vipType === 'practitioner' && userStore.isVip" class="menu-section">
  165. <view class="menu-section__header">我的服务</view>
  166. <!-- Practitioner workbench (practitioners only) — EPIC 9 -->
  167. <view v-if="userStore.vipType === 'practitioner' && userStore.isVip" class="menu-item" @click="goWorkbench">
  168. <text class="menu-item__label">🔧 工作台</text>
  169. <view class="menu-item__badge menu-item__badge--new">NEW</view>
  170. <text class="menu-item__arrow">›</text>
  171. </view>
  172. </view>
  173. <!-- ===== Promotion Center (paid users only) ===== -->
  174. <template v-if="userStore.isLoggedIn && userStore.isVip">
  175. <!-- 收益 -->
  176. <view class="promo-section earnings-section">
  177. <view class="promo-section__header">
  178. <text class="promo-section__title">💰 我的收益</text>
  179. </view>
  180. <view class="earnings-grid">
  181. <view class="earn-item">
  182. <text class="earn-label">累计收益</text>
  183. <text class="earn-value accent">¥{{ formatCommissionMoney(stats.totalEarnings) }}</text>
  184. </view>
  185. <view class="earn-item">
  186. <text class="earn-label">可提现余额</text>
  187. <text class="earn-value">¥{{ formatCommissionMoney(stats.availableBalance) }}</text>
  188. </view>
  189. <view class="earn-item">
  190. <text class="earn-label">本月收益</text>
  191. <text class="earn-value">¥{{ formatCommissionMoney(stats.monthEarnings) }}</text>
  192. </view>
  193. <view class="earn-item">
  194. <text class="earn-label">今日预估</text>
  195. <text class="earn-value">¥{{ formatCommissionMoney(stats.todayEstimate) }}</text>
  196. </view>
  197. </view>
  198. <view v-if="l1Total > 0 || l2Total > 0" class="earnings-breakdown">
  199. <view class="breakdown-row">
  200. <text class="breakdown-label">一级推广佣金</text>
  201. <text class="breakdown-value">¥{{ formatCommissionMoney(l1Total) }}</text>
  202. </view>
  203. <view class="breakdown-row">
  204. <text class="breakdown-label">二级推广佣金</text>
  205. <text class="breakdown-value">¥{{ formatCommissionMoney(l2Total) }}</text>
  206. </view>
  207. </view>
  208. </view>
  209. <!-- 团队 -->
  210. <view class="promo-section">
  211. <view class="promo-section__header">
  212. <text class="promo-section__title">👥 我的团队</text>
  213. <text v-if="team.length > 0" class="promo-section__sub" @click="showTeamDetail">查看全部 →</text>
  214. </view>
  215. <view class="team-grid">
  216. <view class="team-stat">
  217. <text class="stat-num">{{ stats.directInvites }}</text>
  218. <text class="stat-label">直接下级</text>
  219. </view>
  220. <view class="team-stat">
  221. <text class="stat-num">{{ stats.indirectInvites }}</text>
  222. <text class="stat-label">间接下级</text>
  223. </view>
  224. <view class="team-stat">
  225. <text class="stat-num">{{ stats.convertedCount }}</text>
  226. <text class="stat-label">下级升级</text>
  227. </view>
  228. <view class="team-stat">
  229. <text class="stat-num">{{ stats.totalCommissions }}</text>
  230. <text class="stat-label">佣金笔数</text>
  231. </view>
  232. </view>
  233. <view v-if="team.length > 0" class="team-preview">
  234. <view v-for="(m, idx) in team.slice(0, 5)" :key="idx" class="member-row">
  235. <image class="member-avatar" :src="m.avatarUrl || '/static/default-avatar.png'" mode="aspectFill" />
  236. <view class="member-info">
  237. <text class="member-name">{{ m.nickname || '微信用户' }}</text>
  238. <text class="member-date">{{ formatDate(m.createdAt) }} 加入</text>
  239. </view>
  240. <text v-if="m.isVip" class="member-vip">VIP</text>
  241. </view>
  242. </view>
  243. <view v-else class="promo-empty">
  244. <text class="promo-empty-text">暂无推广成员</text>
  245. </view>
  246. </view>
  247. <!-- 佣金明细 -->
  248. <view class="promo-section">
  249. <view class="promo-section__header">
  250. <text class="promo-section__title">📋 佣金明细</text>
  251. </view>
  252. <view v-if="commissions.length === 0" class="promo-empty">
  253. <text class="promo-empty-text">暂无佣金记录</text>
  254. </view>
  255. <view v-for="(c, idx) in commissions" :key="idx" class="commission-row">
  256. <view class="comm-left">
  257. <text class="comm-amount">+¥{{ (c.amount / 100).toFixed(2) }}</text>
  258. <text class="comm-level">{{ c.level === 1 ? '一级推广' : '二级推广' }}</text>
  259. <text v-if="c.remark" class="comm-remark">{{ c.remark }}</text>
  260. </view>
  261. <view class="comm-right">
  262. <text class="comm-status" :class="'status-' + c.status">{{ commissionStatusText(c.status) }}</text>
  263. <text class="comm-date">{{ formatDate(c.createdAt) }}</text>
  264. </view>
  265. </view>
  266. </view>
  267. </template>
  268. <!-- Group 2: Promotion Tools (paid users only) -->
  269. <view v-if="userStore.isLoggedIn && userStore.isVip && userStore.referralCode" class="menu-section">
  270. <view class="menu-section__header">推广工具</view>
  271. <view class="promo-card">
  272. <view class="promo-card__code-area">
  273. <text class="promo-card__prefix">推广码</text>
  274. <text class="promo-card__code">{{ userStore.referralCode }}</text>
  275. </view>
  276. <view class="promo-card__actions">
  277. <text class="promo-card__btn" @click="copyReferral">复制推广码</text>
  278. <text class="promo-card__divider">|</text>
  279. <text class="promo-card__btn" @click="generatePoster">生成推广海报</text>
  280. </view>
  281. </view>
  282. </view>
  283. <!-- Group 3: More -->
  284. <view class="menu-section">
  285. <view class="menu-section__header">更多</view>
  286. <!-- My tags (practitioner only, P2) -->
  287. <view v-if="userStore.vipType === 'practitioner'" class="menu-item" @click="goTags">
  288. <text class="menu-item__label">🏷 我的标签</text>
  289. <text class="menu-item__arrow">›</text>
  290. </view>
  291. <!-- About us (all) -->
  292. <view class="menu-item" @click="goAbout">
  293. <text class="menu-item__label">ℹ️ 关于我们</text>
  294. <text class="menu-item__arrow">›</text>
  295. </view>
  296. </view>
  297. <!-- ========== Region 4: Logout ========== -->
  298. <view v-if="userStore.isLoggedIn" class="menu-section">
  299. <view class="menu-item menu-item--danger" @click="handleLogout">
  300. <text class="menu-item__label menu-item__label--danger">退出登录</text>
  301. </view>
  302. </view>
  303. <!-- Hidden canvas for poster generation -->
  304. <canvas canvas-id="posterCanvas" id="posterCanvas" class="poster-canvas" />
  305. </view>
  306. </template>
  307. <script setup>
  308. import { ref, computed, getCurrentInstance } from 'vue'
  309. import { onShow } from '@dcloudio/uni-app'
  310. import { useUserStore } from '@/stores/user'
  311. import { useThemeStore } from '@/stores/theme'
  312. import { useTheme } from '@/composables/useThemeStyle'
  313. import { profileApi, commissionApi } from '@/utils/api'
  314. const userStore = useUserStore()
  315. const themeStore = useThemeStore()
  316. const { themeClass } = useTheme()
  317. // Commission reactive data
  318. const commissions = ref([])
  319. const team = ref([])
  320. const stats = ref({
  321. totalEarnings: 0,
  322. availableBalance: 0,
  323. todayEstimate: 0,
  324. monthEarnings: 0,
  325. directInvites: 0,
  326. indirectInvites: 0,
  327. convertedCount: 0,
  328. totalCommissions: 0
  329. })
  330. const l1Total = computed(() =>
  331. commissions.value.filter(c => c.level === 1 && c.status === 'settled').reduce((s, c) => s + (c.amount || 0), 0)
  332. )
  333. const l2Total = computed(() =>
  334. commissions.value.filter(c => c.level === 2 && c.status === 'settled').reduce((s, c) => s + (c.amount || 0), 0)
  335. )
  336. // US-4.3: Renewal reminder if expiry within 7 days
  337. const isExpiringSoon = computed(() => {
  338. if (!userStore.vipEndTime) return false
  339. const expiry = new Date(userStore.vipEndTime)
  340. const now = new Date()
  341. const diffHours = (expiry - now) / (1000 * 60 * 60)
  342. return diffHours > 0 && diffHours <= 7 * 24
  343. })
  344. // US-4.3: Expired check
  345. const isExpired = computed(() => {
  346. if (!userStore.vipEndTime) return false
  347. return new Date(userStore.vipEndTime) < new Date()
  348. })
  349. function formatDate(dateStr) {
  350. if (!dateStr) return ''
  351. return dateStr.slice(0, 10)
  352. }
  353. onShow(() => {
  354. themeStore.init()
  355. if (userStore.isLoggedIn) {
  356. userStore.fetchProfile()
  357. userStore.fetchQuota()
  358. userStore.fetchCommissionSummary()
  359. fetchCommissionData()
  360. }
  361. })
  362. function goLogin() {
  363. uni.navigateTo({ url: '/pages/login/index' })
  364. }
  365. function goEditProfile() {
  366. if (!userStore.isLoggedIn) {
  367. uni.navigateTo({ url: '/pages/login/index' })
  368. return
  369. }
  370. uni.navigateTo({ url: '/pages/profile/edit' })
  371. }
  372. function goPayment() {
  373. uni.navigateTo({ url: '/pages/payment/index?product=annual' })
  374. }
  375. function goRenew() {
  376. const product = userStore.vipType === 'practitioner' ? 'practitioner' : 'annual'
  377. uni.navigateTo({ url: `/pages/payment/index?product=${product}` })
  378. }
  379. function goUpgradePractitioner() {
  380. uni.navigateTo({ url: '/pages/payment/index?product=practitioner' })
  381. }
  382. function goSuperMember() {
  383. uni.navigateTo({ url: '/pages/payment/index?product=super_member' })
  384. }
  385. function formatCommissionMoney(cents) {
  386. if (cents == null) return '0.00'
  387. return (cents / 100).toFixed(2)
  388. }
  389. function commissionStatusText(status) {
  390. const map = { settled: '已结算', pending: '处理中', withdrawn: '已提现', available: '可提现' }
  391. return map[status] || status
  392. }
  393. async function fetchCommissionData() {
  394. if (!userStore.isVip) return
  395. try {
  396. const [listData, teamData, statsData] = await Promise.all([
  397. commissionApi.list(),
  398. commissionApi.team(),
  399. commissionApi.stats()
  400. ])
  401. commissions.value = listData || []
  402. team.value = teamData || []
  403. if (statsData) stats.value = statsData
  404. } catch (e) { /* ignore */ }
  405. }
  406. function showTeamDetail() {
  407. uni.showModal({
  408. title: '推广团队',
  409. content: `直接下级 ${stats.value.directInvites} 人\n间接下级 ${stats.value.indirectInvites} 人\n已升级VIP ${stats.value.convertedCount} 人`,
  410. showCancel: false
  411. })
  412. }
  413. // EPIC 9: Navigate to practitioner workbench
  414. function goWorkbench() {
  415. uni.navigateTo({ url: '/pages/plan/workbench' })
  416. }
  417. function copyReferral() {
  418. if (userStore.referralCode) {
  419. uni.setClipboardData({
  420. data: userStore.referralCode,
  421. success: () => uni.showToast({ title: '已复制推广码', icon: 'success' })
  422. })
  423. }
  424. }
  425. function generatePoster() {
  426. uni.showLoading({ title: '生成海报…', mask: true })
  427. const baseUrl = 'http://127.0.0.1:8186'
  428. Promise.all([
  429. downloadImage(userStore.avatarUrl),
  430. profileApi.qrcode().then(res => {
  431. const url = res.qrCodeUrl.startsWith('http') ? res.qrCodeUrl : baseUrl + res.qrCodeUrl
  432. return downloadImage(url)
  433. })
  434. ]).then(([avatarPath, qrPath]) => {
  435. const ctx = uni.createCanvasContext('posterCanvas')
  436. const W = 375, H = 670
  437. // Background
  438. const grd = ctx.createLinearGradient(0, 0, 0, H)
  439. grd.addColorStop(0, '#0c0a1a')
  440. grd.addColorStop(1, '#1a1040')
  441. ctx.fillStyle = grd
  442. ctx.fillRect(0, 0, W, H)
  443. // Top glow
  444. const topGlow = ctx.createLinearGradient(0, 0, 0, 220)
  445. topGlow.addColorStop(0, 'rgba(90, 50, 180, 0.35)')
  446. topGlow.addColorStop(1, 'rgba(90, 50, 180, 0)')
  447. ctx.fillStyle = topGlow
  448. ctx.fillRect(0, 0, W, 220)
  449. // Gold accent line
  450. ctx.fillStyle = '#C9A84C'
  451. ctx.fillRect(W * 0.32, 28, W * 0.36, 1.5)
  452. // Brand title
  453. ctx.font = '13px sans-serif'
  454. ctx.fillStyle = 'rgba(201, 168, 76, 0.7)'
  455. ctx.textAlign = 'center'
  456. ctx.fillText('数字能量学', W / 2, 52)
  457. // Avatar (circular)
  458. const avatarSize = 76
  459. ctx.save()
  460. ctx.beginPath()
  461. ctx.arc(W / 2, 128, avatarSize / 2, 0, 2 * Math.PI)
  462. ctx.closePath()
  463. ctx.fillStyle = '#ffffff'
  464. ctx.fill()
  465. if (avatarPath) {
  466. ctx.clip()
  467. ctx.drawImage(avatarPath, (W - avatarSize) / 2, 128 - avatarSize / 2, avatarSize, avatarSize)
  468. }
  469. ctx.restore()
  470. // Nickname
  471. ctx.font = 'bold 18px sans-serif'
  472. ctx.fillStyle = '#FFFFFF'
  473. ctx.textAlign = 'center'
  474. ctx.fillText(userStore.nickname || '数字能量探索者', W / 2, 192)
  475. // Role badge
  476. const roleText = userStore.vipType === 'practitioner' ? '能量师' : userStore.vipType === 'annual' ? 'C端会员' : '探索者'
  477. ctx.font = '11px sans-serif'
  478. ctx.fillStyle = userStore.vipType === 'practitioner' ? '#C9A84C' : 'rgba(255,255,255,0.5)'
  479. ctx.fillText(roleText, W / 2, 214)
  480. // Divider
  481. ctx.fillStyle = 'rgba(255,255,255,0.07)'
  482. ctx.fillRect(30, 240, W - 60, 1)
  483. // Section title
  484. ctx.font = '12px sans-serif'
  485. ctx.fillStyle = 'rgba(255,255,255,0.38)'
  486. ctx.fillText('推荐体验', W / 2, 270)
  487. // QR code background
  488. const qrSize = 160
  489. const qrX = (W - qrSize) / 2
  490. const qrY = 288
  491. ctx.fillStyle = '#FFFFFF'
  492. ctx.fillRect(qrX, qrY, qrSize, qrSize)
  493. if (qrPath) {
  494. ctx.drawImage(qrPath, qrX + 8, qrY + 8, qrSize - 16, qrSize - 16)
  495. }
  496. // Hint
  497. ctx.font = '11px sans-serif'
  498. ctx.fillStyle = 'rgba(255,255,255,0.32)'
  499. ctx.fillText('扫码开启你的数字能量之旅', W / 2, qrY + qrSize + 24)
  500. // Bottom
  501. ctx.font = '10px sans-serif'
  502. ctx.fillStyle = 'rgba(255,255,255,0.18)'
  503. ctx.fillText('数字能量学 · 数字能量智能体', W / 2, H - 22)
  504. ctx.draw(false)
  505. setTimeout(() => {
  506. uni.canvasToTempFilePath({
  507. canvasId: 'posterCanvas',
  508. quality: 1,
  509. success: (res) => {
  510. uni.saveImageToPhotosAlbum({
  511. filePath: res.tempFilePath,
  512. success: () => {
  513. uni.hideLoading()
  514. uni.showToast({ title: '海报已保存到相册', icon: 'success' })
  515. },
  516. fail: (e) => {
  517. uni.hideLoading()
  518. if (e.errMsg && e.errMsg.includes('auth deny')) {
  519. uni.showModal({
  520. title: '保存海报',
  521. content: '需要相册权限才能保存海报到本地',
  522. confirmText: '去设置',
  523. success: (r) => { if (r.confirm) uni.openSetting() }
  524. })
  525. } else {
  526. uni.showToast({ title: '保存失败,请重试', icon: 'none' })
  527. }
  528. }
  529. })
  530. },
  531. fail: () => {
  532. uni.hideLoading()
  533. uni.showToast({ title: '生成失败,请重试', icon: 'none' })
  534. }
  535. }, getCurrentInstance())
  536. }, 150)
  537. }).catch(e => {
  538. console.error('generatePoster error:', e)
  539. uni.hideLoading()
  540. uni.showToast({ title: '生成失败,请重试', icon: 'none' })
  541. })
  542. }
  543. function downloadImage(url) {
  544. if (!url) return Promise.resolve('')
  545. return new Promise((resolve) => {
  546. uni.downloadFile({
  547. url,
  548. success: (res) => resolve(res.tempFilePath),
  549. fail: () => resolve('')
  550. })
  551. })
  552. }
  553. function goTags() {
  554. uni.navigateTo({ url: '/pages/tags/index' })
  555. }
  556. function goAbout() {
  557. uni.navigateTo({ url: '/pages/about/index' })
  558. }
  559. function handleLogout() {
  560. uni.showModal({
  561. title: '确认退出',
  562. content: '确定要退出登录吗?',
  563. success: (res) => {
  564. if (res.confirm) {
  565. userStore.logout()
  566. uni.showToast({ title: '已退出', icon: 'success' })
  567. }
  568. }
  569. })
  570. }
  571. </script>
  572. <style scoped lang="scss">
  573. // Brand colors (using CSS custom properties — responsive to theme)
  574. $blue-dark: var(--color-brand);
  575. $gold: var(--color-gold);
  576. $gold-light: var(--color-gold-light);
  577. $warm-white: var(--color-bg);
  578. $white: #FFFFFF;
  579. $card-bg: var(--color-bg-card);
  580. $text-primary: var(--color-text-primary);
  581. $text-secondary: var(--color-text-secondary);
  582. $text-muted: var(--color-text-tertiary);
  583. $danger: var(--color-danger);
  584. // ===== Theme Variables (class-based, replaces inline :style) =====
  585. .page-profile {
  586. // Default: Dark Theme
  587. --color-bg: #0c0a1a;
  588. --color-bg-secondary: #120d28;
  589. --color-bg-tertiary: #0f0c1e;
  590. --color-bg-card: rgba(255, 255, 255, 0.06);
  591. --color-bg-card-hover: rgba(255, 255, 255, 0.1);
  592. --color-bg-input: rgba(255, 255, 255, 0.1);
  593. --color-bg-tag: rgba(255, 255, 255, 0.08);
  594. --color-bg-overlay: rgba(12, 10, 26, 0.85);
  595. --color-text-primary: #FFFFFF;
  596. --color-text-secondary: #CBD5E1;
  597. --color-text-tertiary: rgba(255, 255, 255, 0.6);
  598. --color-text-link: #60A5FA;
  599. --color-text-gold: #C9A84C;
  600. --color-brand: #1E3A5F;
  601. --color-brand-light: #1E3A5F;
  602. --color-elevated: #172943;
  603. --color-gold: #C9A84C;
  604. --color-gold-bg: rgba(201, 168, 76, 0.15);
  605. --color-gold-light: #FEF3C7;
  606. --color-border: rgba(255, 255, 255, 0.1);
  607. --color-border-light: rgba(255, 255, 255, 0.06);
  608. --color-border-focus: rgba(201, 168, 76, 0.5);
  609. --color-success: #10b981;
  610. --color-success-bg: rgba(16, 185, 129, 0.15);
  611. --color-warning: #fbbf24;
  612. --color-warning-bg: rgba(251, 191, 36, 0.15);
  613. --color-danger: #ef4444;
  614. --color-danger-bg: rgba(239, 68, 68, 0.15);
  615. --color-info: #3B82F6;
  616. --color-info-bg: rgba(59, 130, 246, 0.15);
  617. --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3);
  618. --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4);
  619. --shadow-card: 0 4px 20px rgba(0, 0, 0, 0.3);
  620. --shadow-modal: 0 10px 40px rgba(0, 0, 0, 0.5);
  621. --shadow-btn: 0 4px 15px rgba(30, 58, 95, 0.4);
  622. --shadow-gold: 0 4px 15px rgba(201, 168, 76, 0.3);
  623. // Light Theme override
  624. &.theme-light {
  625. --color-bg: #F8F6F1;
  626. --color-bg-secondary: #F0EDE6;
  627. --color-bg-tertiary: #E8E5DE;
  628. --color-bg-card: #FFFFFF;
  629. --color-bg-card-hover: #F7F8FC;
  630. --color-bg-input: #F3F4F6;
  631. --color-bg-tag: #F3F4F6;
  632. --color-bg-overlay: rgba(248, 246, 241, 0.9);
  633. --color-text-primary: #1A1A2E;
  634. --color-text-secondary: #4A5568;
  635. --color-text-tertiary: #9CA3AF;
  636. --color-text-link: #2563EB;
  637. --color-text-gold: #92400E;
  638. --color-brand: #1E3A5F;
  639. --color-brand-light: #1E3A5F;
  640. --color-elevated: #2C5282;
  641. --color-gold: #C9A84C;
  642. --color-gold-bg: #FEF3C7;
  643. --color-gold-light: #FEF3C7;
  644. --color-border: #E2E8F0;
  645. --color-border-light: #F3F4F6;
  646. --color-border-focus: rgba(201, 168, 76, 0.6);
  647. --color-success: #059669;
  648. --color-success-bg: #D1FAE5;
  649. --color-warning: #D97706;
  650. --color-warning-bg: #FEF3C7;
  651. --color-danger: #DC2626;
  652. --color-danger-bg: #FEE2E2;
  653. --color-info: #2563EB;
  654. --color-info-bg: #DBEAFE;
  655. --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.06);
  656. --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08);
  657. --shadow-card: 0 2px 12px rgba(0, 0, 0, 0.06);
  658. --shadow-modal: 0 8px 30px rgba(0, 0, 0, 0.1);
  659. --shadow-btn: 0 4px 12px rgba(30, 58, 95, 0.2);
  660. --shadow-gold: 0 4px 12px rgba(201, 168, 76, 0.2);
  661. }
  662. }
  663. .page-profile {
  664. padding: 16px;
  665. min-height: 100vh;
  666. background: $warm-white;
  667. }
  668. // ========== Merged Region 1+2: User Card + Membership ==========
  669. .user-card {
  670. background: linear-gradient(135deg, var(--color-brand), var(--color-elevated));
  671. border-radius: 16px;
  672. padding: 20px 18px;
  673. margin-bottom: 24px;
  674. &__login {
  675. cursor: pointer;
  676. text-align: center;
  677. padding: 8px 0;
  678. }
  679. &__logged {
  680. cursor: pointer;
  681. }
  682. &__inner {
  683. display: flex;
  684. align-items: center;
  685. gap: 14px;
  686. }
  687. &__info {
  688. flex: 1;
  689. min-width: 0;
  690. }
  691. &__name-row {
  692. display: flex;
  693. align-items: center;
  694. gap: 8px;
  695. flex-wrap: wrap;
  696. }
  697. &__nickname {
  698. font-size: 17px;
  699. font-weight: 600;
  700. color: $white;
  701. }
  702. &__login-hint {
  703. font-size: 12px;
  704. color: rgba(255, 255, 255, 0.7);
  705. display: block;
  706. margin-top: 4px;
  707. }
  708. &__tag {
  709. display: inline-flex;
  710. padding: 2px 12px;
  711. border-radius: 20px;
  712. font-size: 11px;
  713. margin-top: 6px;
  714. background: rgba(255, 255, 255, 0.22);
  715. color: #fff;
  716. &--practitioner {
  717. background: $gold;
  718. color: #fff;
  719. font-weight: 600;
  720. }
  721. &--annual {
  722. background: $gold;
  723. color: #fff;
  724. font-weight: 600;
  725. }
  726. &--super {
  727. background: $gold;
  728. color: #fff;
  729. font-weight: 600;
  730. }
  731. &--normal {
  732. background: rgba(255, 255, 255, 0.22);
  733. color: #fff;
  734. }
  735. }
  736. &__summary {
  737. font-size: 11px;
  738. color: rgba(255, 255, 255, 0.78);
  739. margin-top: 4px;
  740. display: block;
  741. overflow: hidden;
  742. text-overflow: ellipsis;
  743. white-space: nowrap;
  744. }
  745. &__arrow {
  746. font-size: 22px;
  747. color: rgba(255, 255, 255, 0.3);
  748. flex-shrink: 0;
  749. padding-left: 4px;
  750. }
  751. &__header-right {
  752. display: flex;
  753. align-items: center;
  754. gap: 8px;
  755. flex-shrink: 0;
  756. align-self: flex-start;
  757. padding-top: 2px;
  758. }
  759. }
  760. // Theme toggle button
  761. .theme-toggle-btn {
  762. width: 36px;
  763. height: 36px;
  764. display: flex;
  765. align-items: center;
  766. justify-content: center;
  767. background: rgba(255, 255, 255, 0.2);
  768. border: 1.5px solid rgba(255, 255, 255, 0.35);
  769. border-radius: 50%;
  770. cursor: pointer;
  771. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
  772. }
  773. .theme-toggle-text {
  774. font-size: 13px;
  775. font-weight: 700;
  776. color: #fff;
  777. line-height: 1;
  778. }
  779. // ===== Promotion Center Sections =====
  780. .promo-section {
  781. background: var(--color-bg-card);
  782. border-radius: 14px;
  783. padding: 16px;
  784. margin-bottom: 16px;
  785. &__header {
  786. display: flex;
  787. justify-content: space-between;
  788. align-items: center;
  789. margin-bottom: 14px;
  790. }
  791. &__title {
  792. font-size: 16px;
  793. font-weight: 700;
  794. color: $text-primary;
  795. }
  796. &__sub {
  797. font-size: 12px;
  798. color: #3B82F6;
  799. font-weight: 500;
  800. }
  801. }
  802. .earnings-section {
  803. background: linear-gradient(135deg, var(--color-brand), var(--color-elevated));
  804. .promo-section__title {
  805. color: rgba(255,255,255,0.9);
  806. }
  807. }
  808. .earnings-grid {
  809. display: grid;
  810. grid-template-columns: 1fr 1fr;
  811. gap: 12px;
  812. }
  813. .earn-item {
  814. text-align: center;
  815. padding: 8px 0;
  816. }
  817. .earn-label {
  818. font-size: 11px;
  819. color: rgba(255,255,255,0.78);
  820. display: block;
  821. margin-bottom: 4px;
  822. }
  823. .earn-value {
  824. font-size: 22px;
  825. font-weight: 700;
  826. color: #fff;
  827. display: block;
  828. }
  829. .earn-value.accent {
  830. color: var(--color-gold);
  831. }
  832. .earnings-breakdown {
  833. margin-top: 12px;
  834. padding: 10px 14px;
  835. background: rgba(255,255,255,0.03);
  836. border-radius: 8px;
  837. }
  838. .breakdown-row {
  839. display: flex;
  840. justify-content: space-between;
  841. align-items: center;
  842. padding: 4px 0;
  843. }
  844. .breakdown-label {
  845. font-size: 12px;
  846. color: rgba(255,255,255,0.7);
  847. }
  848. .breakdown-value {
  849. font-size: 13px;
  850. font-weight: 500;
  851. color: rgba(255,255,255,0.9);
  852. }
  853. // Team
  854. .team-grid {
  855. display: grid;
  856. grid-template-columns: 1fr 1fr 1fr 1fr;
  857. gap: 8px;
  858. margin-bottom: 14px;
  859. }
  860. .team-stat {
  861. text-align: center;
  862. padding: 8px 0;
  863. }
  864. .stat-num {
  865. font-size: 24px;
  866. font-weight: 700;
  867. color: var(--color-text-primary);
  868. display: block;
  869. }
  870. .stat-label {
  871. font-size: 11px;
  872. color: $text-secondary;
  873. margin-top: 2px;
  874. display: block;
  875. }
  876. .team-preview {
  877. border-top: 1px solid var(--color-border);
  878. padding-top: 10px;
  879. }
  880. .member-row {
  881. display: flex;
  882. align-items: center;
  883. padding: 8px 0;
  884. border-bottom: 1px solid var(--color-bg);
  885. }
  886. .member-row:last-child { border-bottom: none; }
  887. .member-avatar {
  888. width: 32px;
  889. height: 32px;
  890. border-radius: 50%;
  891. margin-right: 10px;
  892. flex-shrink: 0;
  893. }
  894. .member-info { flex: 1; }
  895. .member-name {
  896. font-size: 13px;
  897. color: $text-primary;
  898. display: block;
  899. }
  900. .member-date {
  901. font-size: 11px;
  902. color: $text-secondary;
  903. margin-top: 1px;
  904. display: block;
  905. }
  906. .member-vip {
  907. font-size: 10px;
  908. color: var(--color-gold);
  909. background: #FEF3C7;
  910. padding: 2px 8px;
  911. border-radius: 4px;
  912. font-weight: 500;
  913. }
  914. .promo-empty {
  915. padding: 24px 0;
  916. text-align: center;
  917. }
  918. .promo-empty-text {
  919. font-size: 13px;
  920. color: $text-secondary;
  921. }
  922. // Commission List
  923. .commission-row {
  924. display: flex;
  925. justify-content: space-between;
  926. align-items: center;
  927. padding: 12px 0;
  928. border-bottom: 1px solid var(--color-border);
  929. }
  930. .commission-row:last-child { border-bottom: none; }
  931. .comm-left { flex: 1; }
  932. .comm-amount {
  933. font-size: 15px;
  934. font-weight: 600;
  935. color: #2D8B67;
  936. display: block;
  937. }
  938. .comm-level {
  939. font-size: 12px;
  940. color: $text-secondary;
  941. margin-top: 2px;
  942. display: block;
  943. }
  944. .comm-remark {
  945. font-size: 11px;
  946. color: $text-secondary;
  947. margin-top: 1px;
  948. display: block;
  949. }
  950. .comm-right { text-align: right; }
  951. .comm-status {
  952. font-size: 11px;
  953. padding: 2px 8px;
  954. border-radius: 4px;
  955. display: inline-block;
  956. margin-bottom: 2px;
  957. }
  958. .comm-status.status-settled { background: #ECFDF5; color: #2D8B67; }
  959. .comm-status.status-pending { background: #FEF3C7; color: #D4A017; }
  960. .comm-status.status-withdrawn { background: var(--color-bg-input); color: $text-secondary; }
  961. .comm-status.status-available { background: #EFF6FF; color: #3B82F6; }
  962. .comm-date {
  963. font-size: 11px;
  964. color: $text-secondary;
  965. display: block;
  966. }
  967. // Avatar
  968. .avatar {
  969. width: 56px;
  970. height: 56px;
  971. border-radius: 50%;
  972. overflow: hidden;
  973. flex-shrink: 0;
  974. image {
  975. width: 100%;
  976. height: 100%;
  977. border-radius: 50%;
  978. border: 2px solid rgba(255, 255, 255, 0.25);
  979. }
  980. &--empty {
  981. width: 56px;
  982. height: 56px;
  983. background: rgba(255, 255, 255, 0.12);
  984. display: flex;
  985. align-items: center;
  986. justify-content: center;
  987. border: 2px dashed rgba(255, 255, 255, 0.25);
  988. }
  989. &__icon {
  990. font-size: 24px;
  991. color: rgba(255, 255, 255, 0.5);
  992. }
  993. }
  994. // ========== (merged into Region 1+2) ==========
  995. // Banners
  996. .banner {
  997. display: flex;
  998. align-items: center;
  999. justify-content: space-between;
  1000. padding: 10px 14px;
  1001. border-radius: 10px;
  1002. margin-bottom: 12px;
  1003. background: #FEF3C7;
  1004. border: 1px solid #FDE68A;
  1005. &--warning {
  1006. background: #FEF3C7;
  1007. border-color: #FDE68A;
  1008. }
  1009. &__text {
  1010. font-size: 13px;
  1011. color: #92400E;
  1012. flex: 1;
  1013. }
  1014. &__arrow {
  1015. font-size: 18px;
  1016. color: #92400E;
  1017. margin-left: 8px;
  1018. }
  1019. }
  1020. // Seed-price badge
  1021. .seed-badge {
  1022. display: flex;
  1023. align-items: center;
  1024. justify-content: center;
  1025. padding: 6px 14px;
  1026. border-radius: 8px;
  1027. margin-bottom: 12px;
  1028. background: $gold-light;
  1029. border: 1px solid $gold;
  1030. &__text {
  1031. font-size: 12px;
  1032. color: $blue-dark;
  1033. font-weight: 500;
  1034. }
  1035. }
  1036. // VIP status card
  1037. .vip-card {
  1038. border-radius: 12px;
  1039. padding: 16px;
  1040. display: flex;
  1041. justify-content: space-between;
  1042. align-items: center;
  1043. margin-top: 16px;
  1044. &__body {
  1045. flex: 1;
  1046. }
  1047. &__title {
  1048. font-weight: 600;
  1049. font-size: 16px;
  1050. display: block;
  1051. }
  1052. &__desc {
  1053. font-size: 12px;
  1054. opacity: 0.92;
  1055. display: block;
  1056. margin-top: 4px;
  1057. }
  1058. &__action {
  1059. font-size: 14px;
  1060. font-weight: 500;
  1061. padding: 6px 14px;
  1062. border-radius: 8px;
  1063. white-space: nowrap;
  1064. cursor: pointer;
  1065. }
  1066. // Practitioner
  1067. &--practitioner {
  1068. background: linear-gradient(135deg, #7c3aed, #6d28d9);
  1069. .vip-card__title, .vip-card__desc { color: #fff; }
  1070. .vip-card__action {
  1071. color: #7c3aed;
  1072. background: rgba(255, 255, 255, 0.95);
  1073. }
  1074. }
  1075. // Super member
  1076. &--super {
  1077. background: linear-gradient(135deg, #C9A84C, #B8922E);
  1078. .vip-card__title, .vip-card__desc { color: #1E3A5F; }
  1079. .vip-card__action {
  1080. color: #C9A84C;
  1081. background: #1E3A5F;
  1082. }
  1083. .vip-card__perks {
  1084. display: flex;
  1085. gap: 6px;
  1086. margin-top: 6px;
  1087. }
  1088. .perk-tag {
  1089. font-size: 10px;
  1090. font-weight: 500;
  1091. padding: 2px 8px;
  1092. border-radius: 4px;
  1093. background: rgba(30, 58, 95, 0.15);
  1094. color: #1E3A5F;
  1095. }
  1096. }
  1097. // Annual
  1098. &--annual {
  1099. background: linear-gradient(135deg, #f59e0b, #d97706);
  1100. .vip-card__title, .vip-card__desc { color: #fff; }
  1101. .vip-card__action {
  1102. color: #d97706;
  1103. background: rgba(255, 255, 255, 0.95);
  1104. }
  1105. }
  1106. // Expired
  1107. &--expired {
  1108. background: #E5E7EB;
  1109. .vip-card__title { color: $text-primary; }
  1110. .vip-card__desc { color: $text-secondary; }
  1111. .vip-card__action {
  1112. color: #fff;
  1113. background: $text-secondary;
  1114. }
  1115. }
  1116. // Upsell (free user — two options)
  1117. &--upsell {
  1118. background: linear-gradient(135deg, var(--color-brand), var(--color-elevated));
  1119. flex-direction: column;
  1120. align-items: flex-start;
  1121. gap: 14px;
  1122. .vip-card__body {
  1123. width: 100%;
  1124. }
  1125. .vip-card__title {
  1126. color: $gold;
  1127. font-size: 16px;
  1128. margin-bottom: 8px;
  1129. }
  1130. .vip-card__dual {
  1131. display: flex;
  1132. gap: 12px;
  1133. width: 100%;
  1134. }
  1135. .vip-card__option {
  1136. flex: 1;
  1137. background: rgba(255, 255, 255, 0.06);
  1138. border: 1px solid rgba(255, 255, 255, 0.08);
  1139. border-radius: 12px;
  1140. padding: 14px 12px;
  1141. display: flex;
  1142. flex-direction: column;
  1143. align-items: center;
  1144. gap: 6px;
  1145. cursor: pointer;
  1146. }
  1147. .vip-card__option:active {
  1148. background: rgba(255, 255, 255, 0.1);
  1149. }
  1150. .vip-card__option .option-title {
  1151. font-size: 15px;
  1152. font-weight: 600;
  1153. color: rgba(255, 255, 255, 0.9);
  1154. }
  1155. .vip-card__option .option-price {
  1156. font-size: 18px;
  1157. font-weight: 700;
  1158. color: $gold;
  1159. }
  1160. .vip-card__option .option-desc {
  1161. font-size: 11px;
  1162. color: rgba(255, 255, 255, 0.7);
  1163. }
  1164. .vip-card__option .option-btn {
  1165. width: 100%;
  1166. border: none;
  1167. border-radius: 8px;
  1168. padding: 8px 0;
  1169. font-size: 13px;
  1170. font-weight: 600;
  1171. cursor: pointer;
  1172. margin-top: 4px;
  1173. }
  1174. .option-btn.light {
  1175. background: rgba(255, 255, 255, 0.12);
  1176. color: #fff;
  1177. }
  1178. .option-btn.primary {
  1179. background: $gold;
  1180. color: $blue-dark;
  1181. }
  1182. .vip-card__option--pro {
  1183. border-color: rgba(201, 168, 76, 0.2);
  1184. background: rgba(201, 168, 76, 0.06);
  1185. }
  1186. }
  1187. }
  1188. // Daily quota
  1189. .quota-bar {
  1190. background: $card-bg;
  1191. border-radius: 12px;
  1192. padding: 16px;
  1193. margin-bottom: 16px;
  1194. display: flex;
  1195. align-items: center;
  1196. justify-content: space-around;
  1197. &__item {
  1198. text-align: center;
  1199. }
  1200. &__label {
  1201. font-size: 12px;
  1202. color: $text-secondary;
  1203. display: block;
  1204. margin-bottom: 4px;
  1205. }
  1206. &__value {
  1207. font-size: 20px;
  1208. font-weight: 700;
  1209. color: $text-primary;
  1210. }
  1211. &__divider {
  1212. width: 1px;
  1213. height: 36px;
  1214. background: #E5E7EB;
  1215. }
  1216. }
  1217. // ========== Region 3: Feature Menu ==========
  1218. .menu-section {
  1219. background: $card-bg;
  1220. border-radius: 12px;
  1221. margin-bottom: 16px;
  1222. overflow: hidden;
  1223. &__header {
  1224. font-size: 12px;
  1225. font-weight: 600;
  1226. color: $text-secondary;
  1227. text-transform: uppercase;
  1228. letter-spacing: 0.5px;
  1229. padding: 14px 16px 6px;
  1230. }
  1231. }
  1232. .menu-item {
  1233. display: flex;
  1234. align-items: center;
  1235. gap: 8px;
  1236. padding: 14px 16px;
  1237. border-bottom: 1px solid #F3F4F6;
  1238. cursor: pointer;
  1239. transition: background 0.15s;
  1240. &:active {
  1241. background: #F9FAFB;
  1242. }
  1243. &:last-child {
  1244. border-bottom: none;
  1245. }
  1246. &__label {
  1247. flex: 1;
  1248. font-size: 15px;
  1249. color: $text-primary;
  1250. &--danger {
  1251. color: $danger;
  1252. }
  1253. }
  1254. &__badge {
  1255. font-size: 10px;
  1256. font-weight: 700;
  1257. padding: 2px 6px;
  1258. border-radius: 4px;
  1259. &--new {
  1260. background: #FEF3C7;
  1261. color: #D97706;
  1262. }
  1263. }
  1264. &__arrow {
  1265. color: $text-secondary;
  1266. font-size: 18px;
  1267. }
  1268. &--danger {
  1269. justify-content: center;
  1270. border-bottom: none;
  1271. }
  1272. }
  1273. // Promotion tools card
  1274. .promo-card {
  1275. margin: 0 16px 14px;
  1276. background: #FFFBEB;
  1277. border: 1px solid #FDE68A;
  1278. border-radius: 10px;
  1279. overflow: hidden;
  1280. &__code-area {
  1281. display: flex;
  1282. align-items: center;
  1283. justify-content: center;
  1284. gap: 10px;
  1285. padding: 14px 16px 8px;
  1286. }
  1287. &__prefix {
  1288. font-size: 13px;
  1289. color: $blue-dark;
  1290. }
  1291. &__code {
  1292. font-size: 22px;
  1293. font-weight: 700;
  1294. color: $blue-dark;
  1295. letter-spacing: 3px;
  1296. font-family: monospace;
  1297. }
  1298. &__actions {
  1299. display: flex;
  1300. align-items: center;
  1301. justify-content: center;
  1302. gap: 10px;
  1303. padding: 6px 16px 14px;
  1304. }
  1305. &__btn {
  1306. font-size: 12px;
  1307. color: $blue-dark;
  1308. font-weight: 500;
  1309. cursor: pointer;
  1310. }
  1311. &__divider {
  1312. color: #D1D5DB;
  1313. font-size: 12px;
  1314. }
  1315. }
  1316. .poster-canvas {
  1317. position: fixed;
  1318. left: -9999px;
  1319. top: -9999px;
  1320. width: 375px;
  1321. height: 670px;
  1322. }
  1323. </style>