Переглянути джерело

feat: 订阅页VIP方案过滤 + 支付页定价统一

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
liaoxg 3 місяців тому
батько
коміт
ee9f136c83
2 змінених файлів з 152 додано та 89 видалено
  1. 22 11
      client/pages/payment/index.vue
  2. 130 78
      client/pages/subscribe/index.vue

+ 22 - 11
client/pages/payment/index.vue

@@ -144,23 +144,32 @@ onMounted(async () => {
   // Determine upgrade scenario: annual member upgrading to practitioner
   if (product.value === 'practitioner' && userStore.vipType === 'annual') {
     isUpgrade.value = true
-    try {
-      const res = await pricingApi.upgrade()
-      if (res) pricing.value = { ...pricing.value, ...res }
-    } catch (e) {
-      // Fallback to normal pricing on error
-      isUpgrade.value = false
-    }
   }
 
-  if (!isUpgrade.value) {
+  // Always get base pricing first (isSeedPrice + correct practitioner price)
+  try {
+    const res = await pricingApi.current()
+    if (res) pricing.value = res
+  } catch (e) {
+    // Use defaults if API fails
+  }
+
+  // If upgrade, overlay upgrade-specific fields (upgradePrice, remainingValue, etc.)
+  if (isUpgrade.value) {
     try {
-      const res = await pricingApi.current()
-      if (res) pricing.value = res
+      const upgradeRes = await pricingApi.upgrade()
+      if (upgradeRes) {
+        pricing.value = {
+          ...pricing.value,    // base: isSeedPrice, seedPrice, standardPrice, annualFee
+          ...upgradeRes         // overlay: upgradePrice, remainingValue, originalPaidAmount, usedDays
+        }
+      }
     } catch (e) {
-      // Use defaults if API fails
+      // Fallback: treat as non-upgrade
+      isUpgrade.value = false
     }
   }
+
   loading.value = false
 })
 
@@ -175,6 +184,8 @@ async function onPay() {
       productType,
       isUpgrade: isUpgrade.value
     })
+    // Refresh user profile to reflect VIP status change
+    await userStore.fetchProfile()
     uni.showToast({ title: '开通成功!', icon: 'success' })
     setTimeout(() => uni.navigateBack(), 1500)
   } catch (e) {

+ 130 - 78
client/pages/subscribe/index.vue

@@ -10,87 +10,96 @@
     </template>
 
     <template v-else>
-    <!-- C端 ¥131 方案 -->
-    <view
-      class="plan-card"
-      :class="{ selected: selectedPlan === 'annual' }"
-      @click="selectedPlan = 'annual'"
-    >
-      <view class="plan-badge popular">推荐</view>
-      <view class="plan-top">
-        <text class="plan-name">C端会员</text>
-        <view class="plan-price-row">
-          <text class="price-symbol">¥</text>
-          <text class="price">{{ formatYuan(pricing.annualFee) }}</text>
-          <text class="per">/年</text>
-        </view>
-        <text class="price-daily">每天仅 ¥{{ (pricing.annualFee / 100 / 365).toFixed(2) }}</text>
-      </view>
-      <view class="plan-features">
-        <text class="feature">✦ AI 互动问答 — 无限次</text>
-        <text class="feature">✦ 获得推广权限,赚取佣金</text>
-        <text class="feature">✦ 一级 40% + 二级 5% 佣金比例</text>
-        <text class="feature">✦ VIP 标识</text>
+      <view v-if="visiblePlans.length === 0" class="already-vip">
+        <text class="already-vip__icon">✓</text>
+        <text class="already-vip__title">您已是能量师</text>
+        <text class="already-vip__desc">解锁了全部功能,可在命盘页使用 AI 解读</text>
       </view>
-    </view>
 
-    <!-- B端 ¥1,314 方案 -->
-    <view
-      class="plan-card plan-pro"
-      :class="{ selected: selectedPlan === 'practitioner' }"
-      @click="selectedPlan = 'practitioner'"
-    >
-      <view class="plan-badge pro">专业</view>
-      <view class="plan-top">
-        <text class="plan-name">能量师</text>
-        <view class="plan-price-row">
-          <text class="price-symbol">¥</text>
-          <text class="price">{{ formatYuan(currentPractitionerPrice) }}</text>
-          <text class="per">/年</text>
+      <view v-show="visiblePlans.length > 0" class="plans-section">
+        <view
+          v-if="visiblePlans.includes('annual')"
+          class="plan-card"
+          :class="{ selected: selectedPlan === 'annual' }"
+          @click="selectedPlan = 'annual'"
+        >
+          <view class="plan-badge popular">推荐</view>
+          <view class="plan-top">
+            <text class="plan-name">C端会员</text>
+            <view class="plan-price-row">
+              <text class="price-symbol">¥</text>
+              <text class="price">{{ formatYuan(pricing.annualFee) }}</text>
+              <text class="per">/年</text>
+            </view>
+            <text class="price-daily">每天仅 ¥{{ (pricing.annualFee / 100 / 365).toFixed(2) }}</text>
+          </view>
+          <view class="plan-features">
+            <text class="feature">✦ AI 互动问答 — 无限次</text>
+            <text class="feature">✦ 获得推广权限,赚取佣金</text>
+            <text class="feature">✦ 一级 40% + 二级 5% 佣金比例</text>
+            <text class="feature">✦ VIP 标识</text>
+          </view>
         </view>
-        <view v-if="pricing.isSeedPrice" class="seed-badge">
-          种子价 · 仅剩 {{ pricing.remainingSeats }}/{{ pricing.seedLimit }} 个名额
+
+        <view
+          v-if="visiblePlans.includes('practitioner')"
+          class="plan-card plan-pro"
+          :class="{ selected: selectedPlan === 'practitioner' }"
+          @click="selectedPlan = 'practitioner'"
+        >
+          <view class="plan-badge pro">专业</view>
+          <view class="plan-top">
+            <text class="plan-name">能量师</text>
+            <view class="plan-price-row">
+              <text class="price-symbol">¥</text>
+              <text class="price">{{ formatYuan(currentPractitionerPrice) }}</text>
+              <text class="per">/年</text>
+            </view>
+            <view v-if="pricing.isSeedPrice" class="seed-badge">
+              种子价 · 仅剩 {{ pricing.remainingSeats }}/{{ pricing.seedLimit }} 个名额
+            </view>
+            <text class="price-daily">每天仅 ¥{{ (currentPractitionerPrice / 100 / 365).toFixed(2) }}</text>
+          </view>
+          <view class="plan-features">
+            <text class="feature">✦ 包含 C 端全部权益</text>
+            <text class="feature">✦ 无限命盘生成</text>
+            <text class="feature">✦ AI 五区完整解读,随问随答</text>
+            <text class="feature">✦ 全部历史记录永久保存</text>
+            <text class="feature">✦ 推广佣金 — 固定 ¥500/人 + 二级 ¥100/人</text>
+          </view>
         </view>
-        <text class="price-daily">每天仅 ¥{{ (currentPractitionerPrice / 100 / 365).toFixed(2) }}</text>
-      </view>
-      <view class="plan-features">
-        <text class="feature">✦ 包含 C 端全部权益</text>
-        <text class="feature">✦ 无限命盘生成</text>
-        <text class="feature">✦ AI 五区完整解读,随问随答</text>
-        <text class="feature">✦ 全部历史记录永久保存</text>
-        <text class="feature">✦ 推广佣金 — 固定 ¥500/人 + 二级 ¥100/人</text>
-      </view>
-    </view>
 
-    <!-- Compare table -->
-    <view class="compare-card">
-      <view class="compare-header">
-        <text class="col">功能</text>
-        <text class="col">普通</text>
-        <text class="col c">C端</text>
-        <text class="col pro">能量师</text>
-      </view>
-      <view class="compare-row" v-for="row in compareData" :key="row.label">
-        <text class="col">{{ row.label }}</text>
-        <text class="col">{{ row.free }}</text>
-        <text class="col c">{{ row.c }}</text>
-        <text class="col pro">{{ row.pro }}</text>
-      </view>
-    </view>
+        <view class="compare-card">
+          <view class="compare-header">
+            <text class="col">功能</text>
+            <text class="col">普通</text>
+            <text class="col c">C端</text>
+            <text class="col pro">能量师</text>
+          </view>
+          <view class="compare-row" v-for="row in compareData" :key="row.label">
+            <text class="col">{{ row.label }}</text>
+            <text class="col">{{ row.free }}</text>
+            <text class="col c">{{ row.c }}</text>
+            <text class="col pro">{{ row.pro }}</text>
+          </view>
+        </view>
 
-    <!-- Pay button -->
-    <button class="pay-btn" @click="onPay" :disabled="paying">
-      {{ paying ? '处理中...' : payButtonText }}
-    </button>
-    <text class="note">支付即表示同意《订阅协议》</text>
+        <button class="pay-btn" @click="onPay" :disabled="paying">
+          {{ paying ? '处理中...' : payButtonText }}
+        </button>
+        <text class="note">支付即表示同意《订阅协议》</text>
+      </view>
     </template>
   </view>
 </template>
 
 <script setup>
-import { ref, computed, onMounted } from 'vue'
+import { ref, computed, onMounted, watch } from 'vue'
+import { useUserStore } from '@/stores/user'
 import { payApi, pricingApi } from '@/utils/api'
 
+const userStore = useUserStore()
+
 const paying = ref(false)
 const loading = ref(true)
 const selectedPlan = ref('annual')
@@ -131,6 +140,8 @@ function formatYuan(cents) {
 
 onMounted(async () => {
   try {
+    // Ensure user profile is loaded (for vipType check)
+    await userStore.fetchProfile()
     const res = await pricingApi.current()
     if (res) pricing.value = res
   } catch (e) {
@@ -139,18 +150,30 @@ onMounted(async () => {
   loading.value = false
 })
 
+// Show plans based on current VIP status
+const visiblePlans = computed(() => {
+  const vipType = userStore.vipType
+  if (!vipType) return ['annual', 'practitioner']
+  if (vipType === 'annual') return ['practitioner']
+  if (vipType === 'practitioner') return []
+  return ['annual', 'practitioner']
+})
+
+// Auto-select first visible plan when VIP status changes
+watch(visiblePlans, (newPlans) => {
+  if (newPlans.length === 0) return
+  if (!newPlans.includes(selectedPlan.value)) {
+    selectedPlan.value = newPlans[0]
+  }
+}, { immediate: true })
+
 async function onPay() {
-  paying.value = true
-  try {
-    const totalFee = selectedPlan.value === 'annual' ? pricing.value.annualFee : currentPractitionerPrice.value
-    const productType = selectedPlan.value
-    await payApi.create({ totalFee, payType: 'wxpay', productType })
-    uni.showToast({ title: '开通成功!', icon: 'success' })
-    setTimeout(() => uni.navigateBack(), 1500)
-  } catch (e) {
-    uni.showToast({ title: '支付失败,请重试', icon: 'none' })
+  if (selectedPlan.value === 'annual') {
+    uni.navigateTo({ url: '/pages/payment/index?product=annual' })
+    return
   }
-  paying.value = false
+  // practitioner — go to payment page
+  uni.navigateTo({ url: '/pages/payment/index?product=practitioner' })
 }
 </script>
 
@@ -342,4 +365,33 @@ async function onPay() {
   font-size: 12px;
   color: rgba(255,255,255,0.25);
 }
+
+.already-vip {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 60px 20px;
+  text-align: center;
+  background: rgba(255,255,255,0.03);
+  border: 1px solid rgba(255,255,255,0.08);
+  border-radius: 16px;
+  margin-top: 20px;
+}
+.already-vip__icon {
+  font-size: 40px;
+  margin-bottom: 16px;
+  color: #22c55e;
+}
+.already-vip__title {
+  font-size: 18px;
+  font-weight: 600;
+  color: rgba(255,255,255,0.85);
+  margin-bottom: 6px;
+}
+.already-vip__desc {
+  font-size: 13px;
+  color: rgba(255,255,255,0.4);
+  line-height: 1.5;
+}
 </style>