index.vue 27 KB

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