index.vue 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957
  1. <template>
  2. <view class="page-profile">
  3. <!-- ========== Region 1: User Card ========== -->
  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. <text class="user-card__nickname">{{ userStore.nickname }}</text>
  21. <!-- Role tag -->
  22. <view
  23. class="user-card__tag"
  24. :class="{
  25. 'user-card__tag--practitioner': userStore.vipType === 'practitioner',
  26. 'user-card__tag--annual': userStore.vipType === 'annual',
  27. 'user-card__tag--normal': !userStore.vipType
  28. }"
  29. >
  30. <text v-if="!userStore.vipType">普通用户</text>
  31. <text v-else-if="userStore.vipType === 'annual'">C端会员</text>
  32. <text v-else>能量师 🌟</text>
  33. </view>
  34. <!-- Social summary (optional) -->
  35. <text v-if="userStore.bio || userStore.city" class="user-card__summary">
  36. {{ userStore.city || '' }}{{ userStore.city && userStore.bio ? ' · ' : '' }}{{ userStore.bio || '' }}
  37. </text>
  38. </view>
  39. <text class="user-card__arrow">›</text>
  40. </view>
  41. </view>
  42. </view>
  43. <!-- ========== Region 2: Membership Status Cards ========== -->
  44. <!-- US-5.1 §13: Profile incomplete banner -->
  45. <view v-if="userStore.isLoggedIn && userStore.profileIncomplete" class="banner" @click="goEditProfile">
  46. <text class="banner__text">📝 请完善个人资料,开启能量匹配</text>
  47. <text class="banner__arrow">›</text>
  48. </view>
  49. <!-- US-4.3: Renewal reminder (7 days before expiry) -->
  50. <view v-if="isExpiringSoon" class="banner banner--warning" @click="goRenew">
  51. <text class="banner__text">⏰ 您的会员即将到期,请及时续费</text>
  52. <text class="banner__arrow">›</text>
  53. </view>
  54. <!-- Seed-price badge -->
  55. <view v-if="userStore.isSeedPriceUser" class="seed-badge">
  56. <text class="seed-badge__text">🌱 种子价专属会员</text>
  57. </view>
  58. <!-- VIP status card — role-based -->
  59. <!-- Case 1: Energy master (practitioner, in valid period) -->
  60. <view
  61. v-if="userStore.vipType === 'practitioner' && userStore.isVip && !isExpired"
  62. class="vip-card vip-card--practitioner"
  63. >
  64. <view class="vip-card__body">
  65. <text class="vip-card__title">能量师会员 🌟</text>
  66. <text class="vip-card__desc">
  67. 有效期至 {{ formatDate(userStore.vipEndTime) }}
  68. </text>
  69. </view>
  70. <text class="vip-card__action" @click="goRenew">续费</text>
  71. </view>
  72. <!-- Case 2: C-end annual subscriber (in valid period) -->
  73. <view
  74. v-else-if="userStore.vipType === 'annual' && userStore.isVip && !isExpired"
  75. class="vip-card vip-card--annual"
  76. >
  77. <view class="vip-card__body">
  78. <text class="vip-card__title">C端会员</text>
  79. <text class="vip-card__desc">
  80. 有效期至 {{ formatDate(userStore.vipEndTime) }}
  81. </text>
  82. </view>
  83. <text class="vip-card__action" @click="goUpgradePractitioner">升级能量师</text>
  84. </view>
  85. <!-- Case 3: Expired member (vipEndTime < now) -->
  86. <view v-else-if="userStore.vipType && isExpired" class="vip-card vip-card--expired">
  87. <view class="vip-card__body">
  88. <text class="vip-card__title">会员已过期</text>
  89. <text class="vip-card__desc">
  90. 过期时间 {{ formatDate(userStore.vipEndTime) }}
  91. </text>
  92. </view>
  93. <text class="vip-card__action" @click="goPayment">重新开通</text>
  94. </view>
  95. <!-- Case 4: Free user — two payment options -->
  96. <view v-else class="vip-card vip-card--upsell">
  97. <view class="vip-card__body">
  98. <text class="vip-card__title">选择方案</text>
  99. <view class="vip-card__dual">
  100. <view class="vip-card__option" @click="goPayment">
  101. <text class="option-title">C端会员</text>
  102. <text class="option-price">¥131/年</text>
  103. <text class="option-desc">无限AI解读</text>
  104. <button class="option-btn light">开通</button>
  105. </view>
  106. <view class="vip-card__option vip-card__option--pro" @click="goUpgradePractitioner">
  107. <text class="option-title">能量师</text>
  108. <text class="option-price">¥1,314/年</text>
  109. <text class="option-desc">完整功能+分销</text>
  110. <button class="option-btn primary">申请</button>
  111. </view>
  112. </view>
  113. </view>
  114. </view>
  115. <!-- Daily quota (non-VIP only) -->
  116. <view v-if="!userStore.isVip" class="quota-bar">
  117. <view class="quota-bar__item">
  118. <text class="quota-bar__label">今日分析</text>
  119. <text class="quota-bar__value">{{ userStore.usedCharts }}/{{ userStore.maxCharts }}</text>
  120. </view>
  121. <view class="quota-bar__divider"></view>
  122. <view class="quota-bar__item">
  123. <text class="quota-bar__label">今日解读</text>
  124. <text class="quota-bar__value">{{ userStore.usedChats }}/{{ userStore.maxChats }}</text>
  125. </view>
  126. </view>
  127. <!-- ========== Region 3: Feature Menu ========== -->
  128. <!-- Group 1: My Services -->
  129. <view class="menu-section">
  130. <view class="menu-section__header">我的服务</view>
  131. <!-- Analysis history (all logged-in) -->
  132. <view v-if="userStore.isLoggedIn" class="menu-item" @click="goRecords">
  133. <text class="menu-item__label">📋 我的分析</text>
  134. <text class="menu-item__arrow">›</text>
  135. </view>
  136. <!-- Distribution panel (paid users only) -->
  137. <view v-if="userStore.isLoggedIn && userStore.isVip" class="menu-item" @click="goCommission">
  138. <text class="menu-item__label">💰 我的推广</text>
  139. <text class="menu-item__arrow">›</text>
  140. </view>
  141. <!-- Practitioner workbench (practitioners only) — EPIC 9 -->
  142. <view v-if="userStore.vipType === 'practitioner' && userStore.isVip" class="menu-item" @click="goWorkbench">
  143. <text class="menu-item__label">🔧 工作台</text>
  144. <view class="menu-item__badge menu-item__badge--new">NEW</view>
  145. <text class="menu-item__arrow">›</text>
  146. </view>
  147. </view>
  148. <!-- Group 2: Promotion Tools (paid users only) -->
  149. <view v-if="userStore.isLoggedIn && userStore.isVip && userStore.referralCode" class="menu-section">
  150. <view class="menu-section__header">推广工具</view>
  151. <view class="promo-card">
  152. <view class="promo-card__code-area">
  153. <text class="promo-card__prefix">推广码</text>
  154. <text class="promo-card__code">{{ userStore.referralCode }}</text>
  155. </view>
  156. <view class="promo-card__actions">
  157. <text class="promo-card__btn" @click="copyReferral">复制推广码</text>
  158. <text class="promo-card__divider">|</text>
  159. <text class="promo-card__btn" @click="generatePoster">生成推广海报</text>
  160. </view>
  161. </view>
  162. </view>
  163. <!-- Group 3: More -->
  164. <view class="menu-section">
  165. <view class="menu-section__header">更多</view>
  166. <!-- My tags (practitioner only, P2) -->
  167. <view v-if="userStore.vipType === 'practitioner'" class="menu-item" @click="goTags">
  168. <text class="menu-item__label">🏷 我的标签</text>
  169. <text class="menu-item__arrow">›</text>
  170. </view>
  171. <!-- About us (all) -->
  172. <view class="menu-item" @click="goAbout">
  173. <text class="menu-item__label">ℹ️ 关于我们</text>
  174. <text class="menu-item__arrow">›</text>
  175. </view>
  176. </view>
  177. <!-- ========== Region 4: Logout ========== -->
  178. <view v-if="userStore.isLoggedIn" class="menu-section">
  179. <view class="menu-item menu-item--danger" @click="handleLogout">
  180. <text class="menu-item__label menu-item__label--danger">退出登录</text>
  181. </view>
  182. </view>
  183. <!-- Hidden canvas for poster generation -->
  184. <canvas canvas-id="posterCanvas" id="posterCanvas" class="poster-canvas" />
  185. </view>
  186. </template>
  187. <script setup>
  188. import { computed, getCurrentInstance } from 'vue'
  189. import { onShow } from '@dcloudio/uni-app'
  190. import { useUserStore } from '@/stores/user'
  191. import { profileApi } from '@/utils/api'
  192. const userStore = useUserStore()
  193. // US-4.3: Renewal reminder if expiry within 7 days
  194. const isExpiringSoon = computed(() => {
  195. if (!userStore.vipEndTime) return false
  196. const expiry = new Date(userStore.vipEndTime)
  197. const now = new Date()
  198. const diffHours = (expiry - now) / (1000 * 60 * 60)
  199. return diffHours > 0 && diffHours <= 7 * 24
  200. })
  201. // US-4.3: Expired check
  202. const isExpired = computed(() => {
  203. if (!userStore.vipEndTime) return false
  204. return new Date(userStore.vipEndTime) < new Date()
  205. })
  206. function formatDate(dateStr) {
  207. if (!dateStr) return ''
  208. return dateStr.slice(0, 10)
  209. }
  210. onShow(() => {
  211. if (userStore.isLoggedIn) {
  212. userStore.fetchProfile()
  213. userStore.fetchQuota()
  214. userStore.fetchCommissionSummary()
  215. }
  216. })
  217. function goLogin() {
  218. uni.navigateTo({ url: '/pages/login/index' })
  219. }
  220. function goEditProfile() {
  221. if (!userStore.isLoggedIn) {
  222. uni.navigateTo({ url: '/pages/login/index' })
  223. return
  224. }
  225. uni.navigateTo({ url: '/pages/profile/edit' })
  226. }
  227. function goPayment() {
  228. uni.navigateTo({ url: '/pages/payment/index?product=annual' })
  229. }
  230. function goRenew() {
  231. const product = userStore.vipType === 'practitioner' ? 'practitioner' : 'annual'
  232. uni.navigateTo({ url: `/pages/payment/index?product=${product}` })
  233. }
  234. function goUpgradePractitioner() {
  235. uni.navigateTo({ url: '/pages/payment/index?product=practitioner' })
  236. }
  237. function goRecords() {
  238. uni.switchTab({ url: '/pages/records/index' })
  239. }
  240. function goCommission() {
  241. if (!userStore.isLoggedIn) {
  242. uni.navigateTo({ url: '/pages/login/index' })
  243. return
  244. }
  245. uni.navigateTo({ url: '/pages/commission/index' })
  246. }
  247. // EPIC 9: Navigate to practitioner workbench
  248. function goWorkbench() {
  249. uni.navigateTo({ url: '/pages/plan/workbench' })
  250. }
  251. function copyReferral() {
  252. if (userStore.referralCode) {
  253. uni.setClipboardData({
  254. data: userStore.referralCode,
  255. success: () => uni.showToast({ title: '已复制推广码', icon: 'success' })
  256. })
  257. }
  258. }
  259. function generatePoster() {
  260. uni.showLoading({ title: '生成海报…', mask: true })
  261. const baseUrl = 'http://127.0.0.1:8186'
  262. Promise.all([
  263. downloadImage(userStore.avatarUrl),
  264. profileApi.qrcode().then(res => {
  265. const url = res.qrCodeUrl.startsWith('http') ? res.qrCodeUrl : baseUrl + res.qrCodeUrl
  266. return downloadImage(url)
  267. })
  268. ]).then(([avatarPath, qrPath]) => {
  269. const ctx = uni.createCanvasContext('posterCanvas')
  270. const W = 375, H = 670
  271. // Background
  272. const grd = ctx.createLinearGradient(0, 0, 0, H)
  273. grd.addColorStop(0, '#0c0a1a')
  274. grd.addColorStop(1, '#1a1040')
  275. ctx.fillStyle = grd
  276. ctx.fillRect(0, 0, W, H)
  277. // Top glow
  278. const topGlow = ctx.createLinearGradient(0, 0, 0, 220)
  279. topGlow.addColorStop(0, 'rgba(90, 50, 180, 0.35)')
  280. topGlow.addColorStop(1, 'rgba(90, 50, 180, 0)')
  281. ctx.fillStyle = topGlow
  282. ctx.fillRect(0, 0, W, 220)
  283. // Gold accent line
  284. ctx.fillStyle = '#C9A84C'
  285. ctx.fillRect(W * 0.32, 28, W * 0.36, 1.5)
  286. // Brand title
  287. ctx.font = '13px sans-serif'
  288. ctx.fillStyle = 'rgba(201, 168, 76, 0.7)'
  289. ctx.textAlign = 'center'
  290. ctx.fillText('数字能量学', W / 2, 52)
  291. // Avatar (circular)
  292. const avatarSize = 76
  293. ctx.save()
  294. ctx.beginPath()
  295. ctx.arc(W / 2, 128, avatarSize / 2, 0, 2 * Math.PI)
  296. ctx.closePath()
  297. ctx.fillStyle = '#ffffff'
  298. ctx.fill()
  299. if (avatarPath) {
  300. ctx.clip()
  301. ctx.drawImage(avatarPath, (W - avatarSize) / 2, 128 - avatarSize / 2, avatarSize, avatarSize)
  302. }
  303. ctx.restore()
  304. // Nickname
  305. ctx.font = 'bold 18px sans-serif'
  306. ctx.fillStyle = '#FFFFFF'
  307. ctx.textAlign = 'center'
  308. ctx.fillText(userStore.nickname || '数字能量探索者', W / 2, 192)
  309. // Role badge
  310. const roleText = userStore.vipType === 'practitioner' ? '能量师' : userStore.vipType === 'annual' ? 'C端会员' : '探索者'
  311. ctx.font = '11px sans-serif'
  312. ctx.fillStyle = userStore.vipType === 'practitioner' ? '#C9A84C' : 'rgba(255,255,255,0.5)'
  313. ctx.fillText(roleText, W / 2, 214)
  314. // Divider
  315. ctx.fillStyle = 'rgba(255,255,255,0.07)'
  316. ctx.fillRect(30, 240, W - 60, 1)
  317. // Section title
  318. ctx.font = '12px sans-serif'
  319. ctx.fillStyle = 'rgba(255,255,255,0.38)'
  320. ctx.fillText('推荐体验', W / 2, 270)
  321. // QR code background
  322. const qrSize = 160
  323. const qrX = (W - qrSize) / 2
  324. const qrY = 288
  325. ctx.fillStyle = '#FFFFFF'
  326. ctx.fillRect(qrX, qrY, qrSize, qrSize)
  327. if (qrPath) {
  328. ctx.drawImage(qrPath, qrX + 8, qrY + 8, qrSize - 16, qrSize - 16)
  329. }
  330. // Hint
  331. ctx.font = '11px sans-serif'
  332. ctx.fillStyle = 'rgba(255,255,255,0.32)'
  333. ctx.fillText('扫码开启你的数字能量之旅', W / 2, qrY + qrSize + 24)
  334. // Bottom
  335. ctx.font = '10px sans-serif'
  336. ctx.fillStyle = 'rgba(255,255,255,0.18)'
  337. ctx.fillText('数字能量学 · 数字能量智能体', W / 2, H - 22)
  338. ctx.draw(false)
  339. setTimeout(() => {
  340. uni.canvasToTempFilePath({
  341. canvasId: 'posterCanvas',
  342. quality: 1,
  343. success: (res) => {
  344. uni.saveImageToPhotosAlbum({
  345. filePath: res.tempFilePath,
  346. success: () => {
  347. uni.hideLoading()
  348. uni.showToast({ title: '海报已保存到相册', icon: 'success' })
  349. },
  350. fail: (e) => {
  351. uni.hideLoading()
  352. if (e.errMsg && e.errMsg.includes('auth deny')) {
  353. uni.showModal({
  354. title: '保存海报',
  355. content: '需要相册权限才能保存海报到本地',
  356. confirmText: '去设置',
  357. success: (r) => { if (r.confirm) uni.openSetting() }
  358. })
  359. } else {
  360. uni.showToast({ title: '保存失败,请重试', icon: 'none' })
  361. }
  362. }
  363. })
  364. },
  365. fail: () => {
  366. uni.hideLoading()
  367. uni.showToast({ title: '生成失败,请重试', icon: 'none' })
  368. }
  369. }, getCurrentInstance())
  370. }, 150)
  371. }).catch(e => {
  372. console.error('generatePoster error:', e)
  373. uni.hideLoading()
  374. uni.showToast({ title: '生成失败,请重试', icon: 'none' })
  375. })
  376. }
  377. function downloadImage(url) {
  378. if (!url) return Promise.resolve('')
  379. return new Promise((resolve) => {
  380. uni.downloadFile({
  381. url,
  382. success: (res) => resolve(res.tempFilePath),
  383. fail: () => resolve('')
  384. })
  385. })
  386. }
  387. function goTags() {
  388. uni.navigateTo({ url: '/pages/tags/index' })
  389. }
  390. function goAbout() {
  391. uni.navigateTo({ url: '/pages/about/index' })
  392. }
  393. function handleLogout() {
  394. uni.showModal({
  395. title: '确认退出',
  396. content: '确定要退出登录吗?',
  397. success: (res) => {
  398. if (res.confirm) {
  399. userStore.logout()
  400. uni.showToast({ title: '已退出', icon: 'success' })
  401. }
  402. }
  403. })
  404. }
  405. </script>
  406. <style scoped lang="scss">
  407. // Brand colors (aligned with style-guide.md)
  408. $blue-dark: #1E3A5F;
  409. $gold: #C9A84C;
  410. $gold-light: #F8F0D8;
  411. $warm-white: #F8F6F1;
  412. $white: #FFFFFF;
  413. $text-primary: #1F2937;
  414. $text-secondary: #6B7280;
  415. $text-muted: #9CA3AF;
  416. $danger: #EF4444;
  417. .page-profile {
  418. padding: 16px;
  419. min-height: 100vh;
  420. background: $warm-white;
  421. }
  422. // ========== Region 1: User Card ==========
  423. .user-card {
  424. background: linear-gradient(135deg, $blue-dark, darken($blue-dark, 6%));
  425. border-radius: 16px;
  426. padding: 20px 18px;
  427. margin-bottom: 16px;
  428. &__login {
  429. cursor: pointer;
  430. text-align: center;
  431. padding: 8px 0;
  432. }
  433. &__logged {
  434. cursor: pointer;
  435. }
  436. &__inner {
  437. display: flex;
  438. align-items: center;
  439. gap: 14px;
  440. }
  441. &__info {
  442. flex: 1;
  443. min-width: 0;
  444. }
  445. &__nickname {
  446. font-size: 17px;
  447. font-weight: 600;
  448. color: $white;
  449. display: block;
  450. }
  451. &__login-hint {
  452. font-size: 12px;
  453. color: rgba(255, 255, 255, 0.7);
  454. display: block;
  455. margin-top: 4px;
  456. }
  457. &__tag {
  458. display: inline-flex;
  459. padding: 2px 12px;
  460. border-radius: 20px;
  461. font-size: 11px;
  462. margin-top: 6px;
  463. &--practitioner {
  464. background: $gold;
  465. color: $blue-dark;
  466. font-weight: 600;
  467. }
  468. &--annual {
  469. background: rgba($gold, 0.25);
  470. color: $gold;
  471. }
  472. &--normal {
  473. background: rgba(255, 255, 255, 0.15);
  474. color: rgba(255, 255, 255, 0.8);
  475. }
  476. }
  477. &__summary {
  478. font-size: 11px;
  479. color: rgba(255, 255, 255, 0.55);
  480. margin-top: 4px;
  481. display: block;
  482. overflow: hidden;
  483. text-overflow: ellipsis;
  484. white-space: nowrap;
  485. }
  486. &__arrow {
  487. font-size: 22px;
  488. color: rgba(255, 255, 255, 0.3);
  489. flex-shrink: 0;
  490. padding-left: 4px;
  491. }
  492. }
  493. // Avatar
  494. .avatar {
  495. width: 56px;
  496. height: 56px;
  497. border-radius: 50%;
  498. overflow: hidden;
  499. flex-shrink: 0;
  500. image {
  501. width: 100%;
  502. height: 100%;
  503. border-radius: 50%;
  504. border: 2px solid rgba(255, 255, 255, 0.25);
  505. }
  506. &--empty {
  507. width: 56px;
  508. height: 56px;
  509. background: rgba(255, 255, 255, 0.12);
  510. display: flex;
  511. align-items: center;
  512. justify-content: center;
  513. border: 2px dashed rgba(255, 255, 255, 0.25);
  514. }
  515. &__icon {
  516. font-size: 24px;
  517. color: rgba(255, 255, 255, 0.5);
  518. }
  519. }
  520. // ========== Region 2: Membership Status ==========
  521. // Banners
  522. .banner {
  523. display: flex;
  524. align-items: center;
  525. justify-content: space-between;
  526. padding: 10px 14px;
  527. border-radius: 10px;
  528. margin-bottom: 12px;
  529. background: #FEF3C7;
  530. border: 1px solid #FDE68A;
  531. &--warning {
  532. background: #FEF3C7;
  533. border-color: #FDE68A;
  534. }
  535. &__text {
  536. font-size: 13px;
  537. color: #92400E;
  538. flex: 1;
  539. }
  540. &__arrow {
  541. font-size: 18px;
  542. color: #92400E;
  543. margin-left: 8px;
  544. }
  545. }
  546. // Seed-price badge
  547. .seed-badge {
  548. display: flex;
  549. align-items: center;
  550. justify-content: center;
  551. padding: 6px 14px;
  552. border-radius: 8px;
  553. margin-bottom: 12px;
  554. background: $gold-light;
  555. border: 1px solid $gold;
  556. &__text {
  557. font-size: 12px;
  558. color: $blue-dark;
  559. font-weight: 500;
  560. }
  561. }
  562. // VIP status card
  563. .vip-card {
  564. border-radius: 12px;
  565. padding: 16px;
  566. display: flex;
  567. justify-content: space-between;
  568. align-items: center;
  569. margin-bottom: 16px;
  570. &__body {
  571. flex: 1;
  572. }
  573. &__title {
  574. font-weight: 600;
  575. font-size: 16px;
  576. display: block;
  577. }
  578. &__desc {
  579. font-size: 12px;
  580. opacity: 0.8;
  581. display: block;
  582. margin-top: 4px;
  583. }
  584. &__action {
  585. font-size: 14px;
  586. font-weight: 500;
  587. padding: 6px 14px;
  588. border-radius: 8px;
  589. white-space: nowrap;
  590. cursor: pointer;
  591. }
  592. // Practitioner
  593. &--practitioner {
  594. background: linear-gradient(135deg, #7c3aed, #6d28d9);
  595. .vip-card__title, .vip-card__desc { color: #fff; }
  596. .vip-card__action {
  597. color: #7c3aed;
  598. background: rgba(255, 255, 255, 0.95);
  599. }
  600. }
  601. // Annual
  602. &--annual {
  603. background: linear-gradient(135deg, #f59e0b, #d97706);
  604. .vip-card__title, .vip-card__desc { color: #fff; }
  605. .vip-card__action {
  606. color: #d97706;
  607. background: rgba(255, 255, 255, 0.95);
  608. }
  609. }
  610. // Expired
  611. &--expired {
  612. background: #E5E7EB;
  613. .vip-card__title { color: $text-secondary; }
  614. .vip-card__desc { color: $text-muted; }
  615. .vip-card__action {
  616. color: $text-secondary;
  617. background: $white;
  618. }
  619. }
  620. // Upsell (free user — two options)
  621. &--upsell {
  622. background: linear-gradient(135deg, $blue-dark, darken($blue-dark, 4%));
  623. flex-direction: column;
  624. align-items: flex-start;
  625. gap: 14px;
  626. .vip-card__body {
  627. width: 100%;
  628. }
  629. .vip-card__title {
  630. color: $gold;
  631. font-size: 16px;
  632. margin-bottom: 8px;
  633. }
  634. .vip-card__dual {
  635. display: flex;
  636. gap: 12px;
  637. width: 100%;
  638. }
  639. .vip-card__option {
  640. flex: 1;
  641. background: rgba(255, 255, 255, 0.06);
  642. border: 1px solid rgba(255, 255, 255, 0.08);
  643. border-radius: 12px;
  644. padding: 14px 12px;
  645. display: flex;
  646. flex-direction: column;
  647. align-items: center;
  648. gap: 6px;
  649. cursor: pointer;
  650. }
  651. .vip-card__option:active {
  652. background: rgba(255, 255, 255, 0.1);
  653. }
  654. .vip-card__option .option-title {
  655. font-size: 15px;
  656. font-weight: 600;
  657. color: rgba(255, 255, 255, 0.9);
  658. }
  659. .vip-card__option .option-price {
  660. font-size: 18px;
  661. font-weight: 700;
  662. color: $gold;
  663. }
  664. .vip-card__option .option-desc {
  665. font-size: 11px;
  666. color: rgba(255, 255, 255, 0.45);
  667. }
  668. .vip-card__option .option-btn {
  669. width: 100%;
  670. border: none;
  671. border-radius: 8px;
  672. padding: 8px 0;
  673. font-size: 13px;
  674. font-weight: 600;
  675. cursor: pointer;
  676. margin-top: 4px;
  677. }
  678. .option-btn.light {
  679. background: rgba(255, 255, 255, 0.12);
  680. color: #fff;
  681. }
  682. .option-btn.primary {
  683. background: $gold;
  684. color: $blue-dark;
  685. }
  686. .vip-card__option--pro {
  687. border-color: rgba($gold, 0.2);
  688. background: rgba($gold, 0.06);
  689. }
  690. }
  691. }
  692. // Daily quota
  693. .quota-bar {
  694. background: $white;
  695. border-radius: 12px;
  696. padding: 16px;
  697. margin-bottom: 16px;
  698. display: flex;
  699. align-items: center;
  700. justify-content: space-around;
  701. &__item {
  702. text-align: center;
  703. }
  704. &__label {
  705. font-size: 12px;
  706. color: $text-secondary;
  707. display: block;
  708. margin-bottom: 4px;
  709. }
  710. &__value {
  711. font-size: 20px;
  712. font-weight: 700;
  713. color: $text-primary;
  714. }
  715. &__divider {
  716. width: 1px;
  717. height: 36px;
  718. background: #E5E7EB;
  719. }
  720. }
  721. // ========== Region 3: Feature Menu ==========
  722. .menu-section {
  723. background: $white;
  724. border-radius: 12px;
  725. margin-bottom: 16px;
  726. overflow: hidden;
  727. &__header {
  728. font-size: 12px;
  729. font-weight: 600;
  730. color: $text-muted;
  731. text-transform: uppercase;
  732. letter-spacing: 0.5px;
  733. padding: 14px 16px 6px;
  734. }
  735. }
  736. .menu-item {
  737. display: flex;
  738. align-items: center;
  739. gap: 8px;
  740. padding: 14px 16px;
  741. border-bottom: 1px solid #F3F4F6;
  742. cursor: pointer;
  743. transition: background 0.15s;
  744. &:active {
  745. background: #F9FAFB;
  746. }
  747. &:last-child {
  748. border-bottom: none;
  749. }
  750. &__label {
  751. flex: 1;
  752. font-size: 15px;
  753. color: $text-primary;
  754. &--danger {
  755. color: $danger;
  756. }
  757. }
  758. &__badge {
  759. font-size: 10px;
  760. font-weight: 700;
  761. padding: 2px 6px;
  762. border-radius: 4px;
  763. &--new {
  764. background: #FEF3C7;
  765. color: #D97706;
  766. }
  767. }
  768. &__arrow {
  769. color: $text-muted;
  770. font-size: 18px;
  771. }
  772. &--danger {
  773. justify-content: center;
  774. border-bottom: none;
  775. }
  776. }
  777. // Promotion tools card
  778. .promo-card {
  779. margin: 0 16px 14px;
  780. background: #FFFBEB;
  781. border: 1px solid #FDE68A;
  782. border-radius: 10px;
  783. overflow: hidden;
  784. &__code-area {
  785. display: flex;
  786. align-items: center;
  787. justify-content: center;
  788. gap: 10px;
  789. padding: 14px 16px 8px;
  790. }
  791. &__prefix {
  792. font-size: 13px;
  793. color: $text-secondary;
  794. }
  795. &__code {
  796. font-size: 22px;
  797. font-weight: 700;
  798. color: $blue-dark;
  799. letter-spacing: 3px;
  800. font-family: monospace;
  801. }
  802. &__actions {
  803. display: flex;
  804. align-items: center;
  805. justify-content: center;
  806. gap: 10px;
  807. padding: 6px 16px 14px;
  808. }
  809. &__btn {
  810. font-size: 12px;
  811. color: $blue-dark;
  812. font-weight: 500;
  813. cursor: pointer;
  814. }
  815. &__divider {
  816. color: #D1D5DB;
  817. font-size: 12px;
  818. }
  819. }
  820. .poster-canvas {
  821. position: fixed;
  822. left: -9999px;
  823. top: -9999px;
  824. width: 375px;
  825. height: 670px;
  826. }
  827. </style>