feat: P1 合盘/星座/问答与测测完整设计包及模拟器取证工具

落地 synastry/star/ask API 与 H5 页面,补齐 cece-frontend-re complete-design 证据文档,并加入 Android 模拟器截图抓取脚本。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jackyu66git
2026-08-03 11:37:53 +08:00
co-authored by Cursor
parent 15a9db374a
commit bd22d9dddd
248 changed files with 26309 additions and 842 deletions
+137 -59
View File
@@ -1,31 +1,46 @@
<template>
<main class="page">
<button class="back" type="button" @click="$router.back()"> 返回</button>
<h1>个人画像</h1>
<p v-if="loading" class="sub">正在生成</p>
<p v-else-if="error" class="err">{{ error }}</p>
<template v-else-if="report">
<p class="sub">{{ headline }}</p>
<div class="card">
<p class="line">{{ oneLiner }}</p>
<p class="tip">生活建议{{ lifeTip }}</p>
<div v-if="keywords.length" class="tags">
<span v-for="k in keywords" :key="k">{{ k }}</span>
<main class="yxg-page decode-page">
<div class="yxg-hero">
<BackButton />
<PageTitle
feature="portrait"
title="愈心解码"
:sub="needBirth && !report ? '填写生日,生成身心解码报告' : '一个生日,读懂身与心'"
/>
</div>
<div class="yxg-sheet sheet-pad">
<template v-if="needBirth && !report">
<div class="yxg-card">
<BirthDateInputs v-model:year="year" v-model:month="month" v-model:day="day" />
<button class="yxg-btn yxg-btn-block mt" type="button" :disabled="loading" @click="start">
{{ loading ? '解码中' : '开始解码' }}
</button>
<p v-if="formError" class="yxg-err">{{ formError }}</p>
</div>
</div>
<div v-if="report.has_deep_access && detail" class="card deep">
<h2>完整分析</h2>
<p><strong>行为模式</strong> {{ detail.behavior_pattern }}</p>
<p><strong>关系特点</strong> {{ detail.relation_style }}</p>
<p><strong>成长方向</strong> {{ detail.growth_direction }}</p>
</div>
<div v-else class="card lock">
<p>完整分析行为模式与成长方向可在深度版或成长会员中查看</p>
<button type="button" :disabled="paying" @click="buyDeep">查看深度版模拟支付</button>
</div>
</template>
<p class="disc">本内容为自我探索与生活方式参考不构成医疗建议亦非占卜预测</p>
</template>
<p v-else-if="loading" class="yxg-hint">正在解码</p>
<p v-else-if="error" class="yxg-err">
{{ error }}
<button type="button" class="yxg-link" @click="retry">重试</button>
</p>
<template v-else-if="report">
<DecodePanel
:summary="summary"
:detail="detail"
:deep="!!report.has_deep_access"
:paying="paying"
@unlock="buyDeep"
/>
<p v-if="error" class="yxg-err">{{ error }}</p>
<div class="links">
<router-link class="yxg-link-plain" to="/relation">去做人格匹配 </router-link>
<router-link v-if="report.id" class="yxg-link-plain" :to="`/reports/${report.id}`">在报告页打开 </router-link>
</div>
<button type="button" class="yxg-btn yxg-btn-ghost share-btn" @click="shareOpen = true">生成分享卡</button>
</template>
<p class="yxg-disc">自我探索与生活方式参考不是占卜或算命体质内容非医疗建议</p>
</div>
<ShareSheet :open="shareOpen" :payload="sharePayload" @close="shareOpen = false" />
</main>
</template>
@@ -33,55 +48,123 @@
import { computed, onMounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import type { GrowthReport } from '@yuxingu/types'
import { validateBirth } from '@yuxingu/utils'
import { api } from '../api/client'
import BackButton from '../components/BackButton.vue'
import BirthDateInputs from '../components/BirthDateInputs.vue'
import DecodePanel from '../components/DecodePanel.vue'
import PageTitle from '../components/PageTitle.vue'
import ShareSheet from '../components/ShareSheet.vue'
import { AnalyticsEvent, track } from '../lib/analytics'
import type { PortraitSharePayload } from '../lib/shareLink'
const route = useRoute()
const loading = ref(true)
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 lifeTip = computed(() => String(summary.value.life_tip || ''))
const keywords = computed(() => (Array.isArray(summary.value.keywords) ? summary.value.keywords as string[] : []))
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 load() {
async function generate(y: number, m: number, d: number) {
loading.value = true
error.value = ''
formError.value = ''
needBirth.value = false
try {
const reportId = String(route.query.report_id || '')
if (reportId) {
report.value = await api.getReport(reportId)
return
}
const y = Number(route.query.y)
const m = Number(route.query.m)
const d = Number(route.query.d)
if (!y || !m || !d) {
error.value = '请从首页填写生日后进入'
return
}
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.createPortrait(profile.id)
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() {
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
}
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
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 {
@@ -93,22 +176,17 @@ onMounted(load)
</script>
<style scoped>
.page{padding:16px}
.back{border:none;background:#fff;border-radius:10px;padding:8px 12px;margin-bottom:12px}
h1{font-size:22px}
h2{font-size:16px;margin-bottom:8px}
.sub{color:var(--yxg-sub);margin:8px 0 14px;font-size:13px}
.err{color:var(--yxg-pri);font-size:13px;margin:8px 0}
.card{background:#fff;border-radius:16px;padding:18px;font-size:14px;line-height:1.7;color:#555;margin-bottom:12px}
.line{font-size:15px;color:#333}
.tip{margin-top:8px;color:#666}
.tags{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px}
.tags span{background:var(--yxg-bg-start,#ffe4e4);color:var(--yxg-pri);padding:4px 10px;border-radius:999px;font-size:12px}
.lock button{
margin-top:12px;padding:10px 16px;border:none;border-radius:22px;color:#fff;font-weight:600;
background:linear-gradient(135deg,#ff7a6e,var(--yxg-pri));
.mt {
margin-top: 14px;
}
.share-btn {
margin-top: 8px;
width: 100%;
}
.links {
display: flex;
flex-direction: column;
gap: 8px;
margin: 4px 0 12px;
}
.lock button:disabled{opacity:.6}
.disc{font-size:11px;color:#bbb;margin-top:16px;line-height:1.5}
.deep p{margin-top:8px}
</style>