Files
digital-psychology/apps/user-h5/src/pages/MembershipPage.vue
T
jackyu66gitandCursor 89756f65b4 feat(ECR-012–016): 合规、题库、时辰刷新、头像、MBTI OEJTS 与埋点
落地输入合规、探索题库、报告日/时辰刷新、账号头像、OEJTS 量表,并补齐 H5 埋点与 Admin 漏斗;同步 ESS 工件、切至自建 Git、清理 GitHub Actions。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-13 01:27:58 +08:00

212 lines
5.9 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>
<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 { useRoute, useRouter } from 'vue-router'
import type { MembershipMe } from '@yuxingu/types'
import { api } from '../api/client'
import AskQuotaBuy from '../components/ask/AskQuotaBuy.vue'
import HomeToolIcon from '../components/HomeToolIcon.vue'
import PageShell from '../components/PageShell.vue'
import { AnalyticsEvent, track } from '../lib/analytics'
import { ensureAccount } from '../lib/authSession'
const router = useRouter()
const route = useRoute()
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 = ''
if (!(await ensureAccount(router, route.fullPath))) {
loading.value = false
return
}
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>