落地埋点 ingest/数据看板、首页宫格 CMS 与测评上下架;含账号引导、问答流式与免责声明去重,以及 review P1 审计同事务修复。 Co-authored-by: Cursor <cursoragent@cursor.com>
170 lines
4.9 KiB
TypeScript
170 lines
4.9 KiB
TypeScript
import { computed, onMounted, ref } from 'vue'
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
import type { GrowthReport } from '@yuxingu/types'
|
|
import { validateBirth } from '@yuxingu/utils'
|
|
import { api } from '../api/client'
|
|
import { AnalyticsEvent, track } from '../lib/analytics'
|
|
import { ensureAccount, loadSelfLatest } from '../lib/authSession'
|
|
import type { PortraitSharePayload } from '../lib/shareLink'
|
|
|
|
export function usePortraitPage() {
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
const loading = ref(false)
|
|
const needBirth = ref(true)
|
|
const error = ref('')
|
|
const formError = ref('')
|
|
const report = ref<GrowthReport | null>(null)
|
|
const paying = ref(false)
|
|
const shareOpen = ref(false)
|
|
const year = ref(String(route.query.y || ''))
|
|
const month = ref(String(route.query.m || ''))
|
|
const day = ref(String(route.query.d || ''))
|
|
|
|
const summary = computed(() => (report.value?.summary || {}) as Record<string, unknown>)
|
|
const detail = computed(() => (report.value?.detail || null) as Record<string, unknown> | null)
|
|
const headline = computed(() => String(summary.value.headline || ''))
|
|
const oneLiner = computed(() => String(summary.value.one_liner || ''))
|
|
const keywords = computed(() =>
|
|
Array.isArray(summary.value.keywords) ? (summary.value.keywords as string[]) : [],
|
|
)
|
|
const sharePayload = computed<PortraitSharePayload | null>(() => {
|
|
if (!report.value) return null
|
|
return {
|
|
type: 'portrait',
|
|
title: headline.value || String(summary.value.person_title || '愈心解码'),
|
|
line: oneLiner.value,
|
|
keywords: keywords.value,
|
|
}
|
|
})
|
|
|
|
async function generate(y: number, m: number, d: number) {
|
|
loading.value = true
|
|
error.value = ''
|
|
formError.value = ''
|
|
needBirth.value = false
|
|
try {
|
|
const birth = `${y}-${String(m).padStart(2, '0')}-${String(d).padStart(2, '0')}`
|
|
const profile = await api.createProfile({ relation: 'self', birth_date: birth, display_name: '我' })
|
|
report.value = await api.getLatestReport(profile.id, 'portrait')
|
|
track(AnalyticsEvent.PortraitCompleted, { source: 'form' })
|
|
} catch (e) {
|
|
error.value = e instanceof Error ? e.message : '加载失败'
|
|
needBirth.value = true
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
async function start() {
|
|
const y = Number(year.value)
|
|
const m = Number(month.value)
|
|
const d = Number(day.value)
|
|
const msg = validateBirth(y, m, d)
|
|
if (msg) {
|
|
formError.value = msg
|
|
return
|
|
}
|
|
await generate(y, m, d)
|
|
}
|
|
|
|
async function load() {
|
|
if (!(await ensureAccount(router, route.fullPath))) return
|
|
const reportId = String(route.query.report_id || '')
|
|
if (reportId) {
|
|
loading.value = true
|
|
needBirth.value = false
|
|
error.value = ''
|
|
try {
|
|
report.value = await api.getReport(reportId)
|
|
track(AnalyticsEvent.PortraitCompleted, { source: 'report_id' })
|
|
} catch (e) {
|
|
error.value = e instanceof Error ? e.message : '加载失败'
|
|
needBirth.value = true
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
return
|
|
}
|
|
loading.value = true
|
|
error.value = ''
|
|
try {
|
|
const cached = await loadSelfLatest('portrait')
|
|
if (cached.report) {
|
|
report.value = cached.report
|
|
needBirth.value = false
|
|
track(AnalyticsEvent.PortraitCompleted, { source: 'cache' })
|
|
return
|
|
}
|
|
if (cached.profile) {
|
|
report.value = await api.createPortrait(cached.profile.id)
|
|
needBirth.value = false
|
|
return
|
|
}
|
|
const y = Number(route.query.y)
|
|
const m = Number(route.query.m)
|
|
const d = Number(route.query.d)
|
|
if (y && m && d && !validateBirth(y, m, d)) {
|
|
year.value = String(y)
|
|
month.value = String(m)
|
|
day.value = String(d)
|
|
await generate(y, m, d)
|
|
return
|
|
}
|
|
needBirth.value = true
|
|
} catch (e) {
|
|
error.value = e instanceof Error ? e.message : '加载失败'
|
|
needBirth.value = true
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
function retry() {
|
|
if (report.value) {
|
|
load()
|
|
return
|
|
}
|
|
needBirth.value = true
|
|
error.value = ''
|
|
}
|
|
|
|
async function buyDeep() {
|
|
if (!report.value) return
|
|
paying.value = true
|
|
error.value = ''
|
|
track(AnalyticsEvent.DeepAccessClicked, { surface: 'portrait' })
|
|
try {
|
|
const { order_id } = await api.createOrder({ kind: 'deep_access', report_id: report.value.id })
|
|
await api.payMock(order_id)
|
|
report.value = await api.getReport(report.value.id)
|
|
track(AnalyticsEvent.PurchaseCompleted, { kind: 'deep_access' })
|
|
} catch (e) {
|
|
error.value = e instanceof Error ? e.message : '支付失败'
|
|
} finally {
|
|
paying.value = false
|
|
}
|
|
}
|
|
|
|
onMounted(load)
|
|
|
|
return {
|
|
loading,
|
|
needBirth,
|
|
error,
|
|
formError,
|
|
report,
|
|
paying,
|
|
shareOpen,
|
|
year,
|
|
month,
|
|
day,
|
|
summary,
|
|
detail,
|
|
sharePayload,
|
|
start,
|
|
retry,
|
|
buyDeep,
|
|
}
|
|
}
|