Files
digital-psychology/apps/user-h5/src/pages/MembershipPage.vue
T
jackyu66gitandCursor 7ab9add5dd
ci / h5 (push) Canceled after 0s
ci / api (push) Canceled after 0s
ci / ess-docs (push) Canceled after 0s
feat(ops): ECR-007 行为分析与 ECR-008 内容运营后台
落地埋点 ingest/数据看板、首页宫格 CMS 与测评上下架;含账号引导、问答流式与免责声明去重,以及 review P1 审计同事务修复。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-07 02:26:16 +08:00

205 lines
5.8 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<PageShell>
<template #hero>
<BackButton />
<div class="head">
<HomeToolIcon name="growth" :size="48" />
<div>
<h1 class="title">成长会员</h1>
<p class="sub">完整分析 · AI 更多次数 · 专属内容</p>
</div>
</div>
</template>
<div class="body">
<p v-if="loading" class="hint">加载中</p>
<p v-else-if="error" class="err">
{{ error }}
<button type="button" class="retry" @click="load">重试</button>
</p>
<template v-else>
<div v-if="me?.active" class="status-card on">
<p class="ok-title">会员有效 · {{ planLabel }}</p>
<p v-if="me.expires_at" class="meta">到期{{ formatDate(me.expires_at) }}</p>
<p class="meta">问答额度剩余{{ me.ask_quota_left ?? 0 }}</p>
</div>
<div v-else class="status-card">
<p class="lead">开通后可查看画像与关系理解的完整分析并获得更多 AI 问答次数</p>
</div>
<p class="sec-title">问答额度包</p>
<AskQuotaBuy lead="免费次数不够用时,可单独加购问答次数(不含深度报告)" @purchased="load" />
<p class="sec-title">成长会员</p>
<div class="plans">
<button
v-for="p in plans"
:key="p.key"
type="button"
class="plan"
:class="{ featured: p.featured }"
:disabled="paying"
@click="subscribe(p.key)"
>
<span v-if="p.tag" class="tag">{{ p.tag }}</span>
<strong>{{ p.label }}</strong>
<span class="price">{{ p.price }}</span>
<span class="desc">{{ p.desc }}</span>
</button>
</div>
<p class="note">当前为模拟支付便于本地验收</p>
</template>
</div>
</PageShell>
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import type { MembershipMe } from '@yuxingu/types'
import { api } from '../api/client'
import AskQuotaBuy from '../components/ask/AskQuotaBuy.vue'
import BackButton from '../components/BackButton.vue'
import HomeToolIcon from '../components/HomeToolIcon.vue'
import PageShell from '../components/PageShell.vue'
import { AnalyticsEvent, track } from '../lib/analytics'
const plans = [
{ key: 'month', label: '月卡', price: '模拟开通', desc: '按月灵活体验', featured: false, tag: '' },
{ key: 'quarter', label: '季卡', price: '模拟开通', desc: '三个月持续成长', featured: true, tag: '推荐' },
{ key: 'year', label: '年卡', price: '模拟开通', desc: '全年陪伴与报告', featured: false, tag: '' },
]
const loading = ref(true)
const paying = ref(false)
const error = ref('')
const me = ref<MembershipMe | null>(null)
const planLabel = computed(() => {
const p = me.value?.plan
if (p === 'quarter') return '季卡'
if (p === 'year') return '年卡'
if (p === 'month') return '月卡'
return p || '成长会员'
})
function formatDate(iso: string) {
try {
return new Date(iso).toLocaleDateString('zh-CN')
} catch {
return iso
}
}
async function load() {
loading.value = true
error.value = ''
try {
me.value = await api.getMembership()
} catch (e) {
error.value = e instanceof Error ? e.message : '加载失败'
} finally {
loading.value = false
}
}
async function subscribe(plan: string) {
paying.value = true
error.value = ''
try {
const { order_id } = await api.createOrder({ kind: 'membership', plan })
await api.payMock(order_id)
me.value = await api.getMembership()
track(AnalyticsEvent.PurchaseCompleted, { kind: 'membership' })
} catch (e) {
error.value = e instanceof Error ? e.message : '支付失败'
} finally {
paying.value = false
}
}
onMounted(load)
</script>
<style scoped>
.head {
display: flex;
align-items: center;
gap: 12px;
margin-top: 8px;
padding-bottom: 8px;
}
.title {
font-family: var(--font-display);
font-size: 22px;
font-weight: 700;
letter-spacing: 0.06em;
}
.sub {
font-size: 12px;
color: rgba(0, 0, 0, 0.38);
margin-top: 2px;
}
.body { padding: 8px 16px 0; }
.status-card {
padding: 16px;
border-radius: var(--radius-xl);
background: #fafafa;
margin-bottom: 14px;
}
.status-card.on {
background: linear-gradient(145deg, #fff8e8, #ffeec8);
box-shadow: var(--shadow-card);
}
.ok-title { font-size: 16px; font-weight: 700; color: #222; }
.meta { font-size: 12px; color: #888; margin-top: 4px; }
.lead { font-size: 14px; color: #555; line-height: 1.6; }
.sec-title {
font-size: 13px;
font-weight: 650;
color: #6a615c;
margin: 4px 0 10px;
}
.plans { display: flex; flex-direction: column; gap: 10px; }
.plan {
position: relative;
text-align: left;
padding: 16px;
border-radius: var(--radius-lg);
border: 1.5px solid #f0ebe8;
background: #fff;
display: flex;
flex-direction: column;
gap: 4px;
}
.plan.featured {
border-color: #f5c4bc;
background: linear-gradient(145deg, #fff7f4, #ffe9e2);
box-shadow: var(--shadow-hero);
}
.plan strong { font-size: 16px; color: #222; }
.price { font-size: 13px; font-weight: 650; color: var(--color-primary); }
.desc { font-size: 12px; color: #999; }
.tag {
position: absolute;
top: 10px;
right: 10px;
font-size: 9px;
font-weight: 700;
color: #fff;
background: linear-gradient(135deg, #ff7a6e, var(--color-primary));
padding: 2px 6px;
border-radius: 6px;
}
.plan:disabled { opacity: 0.55; }
.note { margin-top: 12px; font-size: 11px; color: #ccc; text-align: center; }
.hint, .err { font-size: 13px; color: #999; padding: 8px 0; }
.err { color: var(--color-primary); }
.retry {
border: none;
background: transparent;
color: var(--color-accent-blue);
text-decoration: underline;
margin-left: 6px;
}
</style>