落地埋点 ingest/数据看板、首页宫格 CMS 与测评上下架;含账号引导、问答流式与免责声明去重,以及 review P1 审计同事务修复。 Co-authored-by: Cursor <cursoragent@cursor.com>
205 lines
5.8 KiB
Vue
205 lines
5.8 KiB
Vue
<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>
|