index.vue 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397
  1. <template>
  2. <view class="page">
  3. <view class="header">
  4. <text class="header-title">选择会员方案</text>
  5. <text class="header-desc">解锁数字能量的全部潜力</text>
  6. </view>
  7. <template v-if="loading">
  8. <view class="loading-state"><text class="loading-text">加载中...</text></view>
  9. </template>
  10. <template v-else>
  11. <view v-if="visiblePlans.length === 0" class="already-vip">
  12. <text class="already-vip__icon">✓</text>
  13. <text class="already-vip__title">您已是能量师</text>
  14. <text class="already-vip__desc">解锁了全部功能,可在能量盘页使用 AI 解读</text>
  15. </view>
  16. <view v-show="visiblePlans.length > 0" class="plans-section">
  17. <view
  18. v-if="visiblePlans.includes('annual')"
  19. class="plan-card"
  20. :class="{ selected: selectedPlan === 'annual' }"
  21. @click="selectedPlan = 'annual'"
  22. >
  23. <view class="plan-badge popular">推荐</view>
  24. <view class="plan-top">
  25. <text class="plan-name">C端会员</text>
  26. <view class="plan-price-row">
  27. <text class="price-symbol">¥</text>
  28. <text class="price">{{ formatYuan(pricing.annualFee) }}</text>
  29. <text class="per">/年</text>
  30. </view>
  31. <text class="price-daily">每天仅 ¥{{ (pricing.annualFee / 100 / 365).toFixed(2) }}</text>
  32. </view>
  33. <view class="plan-features">
  34. <text class="feature">✦ AI 互动问答 — 无限次</text>
  35. <text class="feature">✦ 获得推广权限,赚取佣金</text>
  36. <text class="feature">✦ 一级 40% + 二级 5% 佣金比例</text>
  37. <text class="feature">✦ VIP 标识</text>
  38. </view>
  39. </view>
  40. <view
  41. v-if="visiblePlans.includes('practitioner')"
  42. class="plan-card plan-pro"
  43. :class="{ selected: selectedPlan === 'practitioner' }"
  44. @click="selectedPlan = 'practitioner'"
  45. >
  46. <view class="plan-badge pro">专业</view>
  47. <view class="plan-top">
  48. <text class="plan-name">能量师</text>
  49. <view class="plan-price-row">
  50. <text class="price-symbol">¥</text>
  51. <text class="price">{{ formatYuan(currentPractitionerPrice) }}</text>
  52. <text class="per">/年</text>
  53. </view>
  54. <view v-if="pricing.isSeedPrice" class="seed-badge">
  55. 种子价 · 仅剩 {{ pricing.remainingSeats }}/{{ pricing.seedLimit }} 个名额
  56. </view>
  57. <text class="price-daily">每天仅 ¥{{ (currentPractitionerPrice / 100 / 365).toFixed(2) }}</text>
  58. </view>
  59. <view class="plan-features">
  60. <text class="feature">✦ 包含 C 端全部权益</text>
  61. <text class="feature">✦ 无限能量盘生成</text>
  62. <text class="feature">✦ AI 五区完整解读,随问随答</text>
  63. <text class="feature">✦ 全部历史记录永久保存</text>
  64. <text class="feature">✦ 推广佣金 — 固定 ¥500/人 + 二级 ¥100/人</text>
  65. </view>
  66. </view>
  67. <view class="compare-card">
  68. <view class="compare-header">
  69. <text class="col">功能</text>
  70. <text class="col">普通</text>
  71. <text class="col c">C端</text>
  72. <text class="col pro">能量师</text>
  73. </view>
  74. <view class="compare-row" v-for="row in compareData" :key="row.label">
  75. <text class="col">{{ row.label }}</text>
  76. <text class="col">{{ row.free }}</text>
  77. <text class="col c">{{ row.c }}</text>
  78. <text class="col pro">{{ row.pro }}</text>
  79. </view>
  80. </view>
  81. <button class="pay-btn" @click="onPay" :disabled="paying">
  82. {{ paying ? '处理中...' : payButtonText }}
  83. </button>
  84. <text class="note">支付即表示同意《订阅协议》</text>
  85. </view>
  86. </template>
  87. </view>
  88. </template>
  89. <script setup>
  90. import { ref, computed, onMounted, watch } from 'vue'
  91. import { useUserStore } from '@/stores/user'
  92. import { payApi, pricingApi } from '@/utils/api'
  93. const userStore = useUserStore()
  94. const paying = ref(false)
  95. const loading = ref(true)
  96. const selectedPlan = ref('annual')
  97. const pricing = ref({
  98. seedPrice: 131400,
  99. standardPrice: 198600,
  100. annualFee: 13100,
  101. isSeedPrice: true,
  102. remainingSeats: 300,
  103. seedLimit: 300
  104. })
  105. const compareData = [
  106. { label: '能量盘生成', free: '3次/天', c: '3次/天', pro: '不限' },
  107. { label: 'AI 解读', free: '3轮/天', c: '无限次', pro: '无限次' },
  108. { label: '历史记录', free: '7天', c: '永久', pro: '永久' },
  109. { label: '推广佣金', free: '--', c: '40%+5%', pro: '¥500+¥100/人' },
  110. { label: '分享导出', free: '1次/天', c: '不限', pro: '不限' },
  111. ]
  112. const currentPractitionerPrice = computed(() => {
  113. return pricing.value.isSeedPrice ? pricing.value.seedPrice : pricing.value.standardPrice
  114. })
  115. const payButtonText = computed(() => {
  116. if (selectedPlan.value === 'annual') {
  117. return `立即开通 · C端会员 ¥${formatYuan(pricing.value.annualFee)}/年`
  118. }
  119. const price = currentPractitionerPrice.value
  120. const label = pricing.value.isSeedPrice ? '种子价' : '标准价'
  121. return `立即开通 · 能量师 ${label} ¥${formatYuan(price)}/年`
  122. })
  123. function formatYuan(cents) {
  124. if (cents == null) return '0'
  125. return (cents / 100).toLocaleString()
  126. }
  127. onMounted(async () => {
  128. try {
  129. // Ensure user profile is loaded (for vipType check)
  130. await userStore.fetchProfile()
  131. const res = await pricingApi.current()
  132. if (res) pricing.value = res
  133. } catch (e) {
  134. // Use defaults if API fails
  135. }
  136. loading.value = false
  137. })
  138. // Show plans based on current VIP status
  139. const visiblePlans = computed(() => {
  140. const vipType = userStore.vipType
  141. if (!vipType) return ['annual', 'practitioner']
  142. if (vipType === 'annual') return ['practitioner']
  143. if (vipType === 'practitioner') return []
  144. return ['annual', 'practitioner']
  145. })
  146. // Auto-select first visible plan when VIP status changes
  147. watch(visiblePlans, (newPlans) => {
  148. if (newPlans.length === 0) return
  149. if (!newPlans.includes(selectedPlan.value)) {
  150. selectedPlan.value = newPlans[0]
  151. }
  152. }, { immediate: true })
  153. async function onPay() {
  154. if (selectedPlan.value === 'annual') {
  155. uni.navigateTo({ url: '/pages/payment/index?product=annual' })
  156. return
  157. }
  158. // practitioner — go to payment page
  159. uni.navigateTo({ url: '/pages/payment/index?product=practitioner' })
  160. }
  161. </script>
  162. <style scoped lang="scss">
  163. .page {
  164. padding: 20px;
  165. min-height: 100vh;
  166. background: linear-gradient(180deg, #0c0a1a 0%, #120d28 60%, #0f0c1e 100%);
  167. }
  168. .header {
  169. text-align: center;
  170. margin-bottom: 20px;
  171. }
  172. .header-title {
  173. font-size: 22px;
  174. font-weight: 700;
  175. color: rgba(255,255,255,0.9);
  176. display: block;
  177. }
  178. .header-desc {
  179. font-size: 13px;
  180. color: rgba(255,255,255,0.4);
  181. margin-top: 6px;
  182. display: block;
  183. }
  184. .loading-state {
  185. padding: 60px 0;
  186. text-align: center;
  187. }
  188. .loading-text {
  189. font-size: 14px;
  190. color: rgba(255,255,255,0.3);
  191. }
  192. /* Plan Cards */
  193. .plan-card {
  194. position: relative;
  195. background: rgba(255,255,255,0.03);
  196. border: 1px solid rgba(255,255,255,0.08);
  197. border-radius: 16px;
  198. padding: 22px 18px 18px;
  199. margin-bottom: 12px;
  200. transition: border-color 0.2s;
  201. }
  202. .plan-card.selected {
  203. border-color: #fbbf24;
  204. background: rgba(251,191,36,0.06);
  205. }
  206. .plan-card.plan-pro.selected {
  207. border-color: #a78bfa;
  208. background: rgba(167,139,250,0.06);
  209. }
  210. .plan-badge {
  211. position: absolute;
  212. top: -1px;
  213. right: 20px;
  214. font-size: 11px;
  215. font-weight: 600;
  216. padding: 3px 14px;
  217. border-radius: 0 0 8px 8px;
  218. }
  219. .plan-badge.popular {
  220. background: #f59e0b;
  221. color: #fff;
  222. }
  223. .plan-badge.pro {
  224. background: #7c3aed;
  225. color: #fff;
  226. }
  227. .plan-top { text-align: center; }
  228. .plan-name {
  229. font-size: 17px;
  230. font-weight: 600;
  231. color: rgba(255,255,255,0.85);
  232. display: block;
  233. margin-bottom: 10px;
  234. }
  235. .plan-price-row {
  236. display: flex;
  237. align-items: baseline;
  238. justify-content: center;
  239. }
  240. .price-symbol {
  241. font-size: 20px;
  242. color: #fbbf24;
  243. font-weight: 600;
  244. }
  245. .price {
  246. font-size: 38px;
  247. font-weight: 700;
  248. color: #fbbf24;
  249. margin: 0 2px;
  250. }
  251. .plan-pro .price-symbol,
  252. .plan-pro .price {
  253. color: #a78bfa;
  254. }
  255. .per {
  256. font-size: 14px;
  257. color: rgba(255,255,255,0.4);
  258. }
  259. .price-daily {
  260. display: block;
  261. font-size: 12px;
  262. color: rgba(255,255,255,0.3);
  263. margin-top: 4px;
  264. }
  265. .seed-badge {
  266. display: inline-block;
  267. margin-top: 6px;
  268. font-size: 11px;
  269. color: #fbbf24;
  270. background: rgba(251,191,36,0.12);
  271. padding: 2px 12px;
  272. border-radius: 4px;
  273. }
  274. .plan-features {
  275. margin-top: 16px;
  276. }
  277. .feature {
  278. display: block;
  279. padding: 5px 0;
  280. font-size: 13px;
  281. color: rgba(255,255,255,0.6);
  282. line-height: 1.5;
  283. }
  284. .plan-card.selected .feature {
  285. color: rgba(255,255,255,0.8);
  286. }
  287. /* Compare table */
  288. .compare-card {
  289. background: rgba(255,255,255,0.03);
  290. border: 1px solid rgba(255,255,255,0.06);
  291. border-radius: 14px;
  292. overflow: hidden;
  293. margin-bottom: 20px;
  294. }
  295. .compare-header,
  296. .compare-row {
  297. display: flex;
  298. padding: 12px 10px;
  299. font-size: 12px;
  300. }
  301. .compare-header {
  302. background: rgba(251,191,36,0.06);
  303. font-weight: 600;
  304. color: rgba(255,255,255,0.7);
  305. }
  306. .compare-row {
  307. border-bottom: 1px solid rgba(255,255,255,0.04);
  308. color: rgba(255,255,255,0.5);
  309. }
  310. .compare-row:last-child { border-bottom: none; }
  311. .col {
  312. flex: 1;
  313. text-align: center;
  314. }
  315. .col:first-child {
  316. text-align: left;
  317. flex: 1.3;
  318. color: rgba(255,255,255,0.7);
  319. }
  320. .col.c { color: #fbbf24; }
  321. .col.pro { color: #a78bfa; }
  322. .pay-btn {
  323. width: 100%;
  324. padding: 16px;
  325. background: linear-gradient(135deg, #f59e0b, #d97706);
  326. color: #fff;
  327. border: none;
  328. border-radius: 14px;
  329. font-size: 17px;
  330. font-weight: 600;
  331. letter-spacing: 2px;
  332. margin-bottom: 8px;
  333. }
  334. .pay-btn[disabled] { opacity: 0.4; }
  335. .note {
  336. display: block;
  337. text-align: center;
  338. font-size: 12px;
  339. color: rgba(255,255,255,0.25);
  340. }
  341. .already-vip {
  342. display: flex;
  343. flex-direction: column;
  344. align-items: center;
  345. justify-content: center;
  346. padding: 60px 20px;
  347. text-align: center;
  348. background: rgba(255,255,255,0.03);
  349. border: 1px solid rgba(255,255,255,0.08);
  350. border-radius: 16px;
  351. margin-top: 20px;
  352. }
  353. .already-vip__icon {
  354. font-size: 40px;
  355. margin-bottom: 16px;
  356. color: #22c55e;
  357. }
  358. .already-vip__title {
  359. font-size: 18px;
  360. font-weight: 600;
  361. color: rgba(255,255,255,0.85);
  362. margin-bottom: 6px;
  363. }
  364. .already-vip__desc {
  365. font-size: 13px;
  366. color: rgba(255,255,255,0.4);
  367. line-height: 1.5;
  368. }
  369. </style>