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
+140
View File
@@ -0,0 +1,140 @@
<template>
<main class="yxg-page">
<div class="yxg-hero">
<BackButton />
<PageTitle feature="rhythm" title="身心节律" :sub="needBirth && !report ? '填写生日,生成身心节律生活建议' : undefined" />
</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">开始</button>
<p v-if="error" class="yxg-err">{{ error }}</p>
</div>
</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="load">重试</button>
</p>
<template v-else-if="report">
<p class="yxg-meta head">{{ headline }}</p>
<p v-if="oneLiner" class="yxg-lead">{{ oneLiner }}</p>
<ReportRich :summary="summary" :detail="detail" :deep="!!report.has_deep_access" />
<div v-if="!report.has_deep_access" class="yxg-card lock">
<p>完整节律建议与练习可在深度版或成长会员中查看</p>
<button class="yxg-btn" type="button" :disabled="paying" @click="buyDeep">查看深度版模拟支付</button>
</div>
<router-link class="yxg-link-plain report-link" to="/companion">查看今日节气陪伴 </router-link>
<router-link v-if="report.id" class="yxg-link-plain report-link" :to="`/reports/${report.id}`">在报告页打开 </router-link>
</template>
<p class="yxg-disc">本内容为自我探索与生活方式参考不构成医疗建议亦非占卜预测</p>
</div>
</main>
</template>
<script setup lang="ts">
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 PageTitle from '../components/PageTitle.vue'
import ReportRich from '../components/ReportRich.vue'
import { AnalyticsEvent, track } from '../lib/analytics'
const route = useRoute()
const loading = ref(false)
const needBirth = ref(true)
const error = ref('')
const report = ref<GrowthReport | null>(null)
const paying = 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 || ''))
async function generate(y: number, m: number, d: number) {
loading.value = true
error.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.createRhythm(profile.id)
track(AnalyticsEvent.PortraitCompleted, { source: 'rhythm' })
} 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) {
error.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
try {
report.value = await api.getReport(reportId)
} catch (e) {
error.value = e instanceof Error ? e.message : '加载失败'
} 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)) {
await generate(y, m, d)
}
}
async function buyDeep() {
if (!report.value) return
paying.value = true
error.value = ''
track(AnalyticsEvent.DeepAccessClicked, { surface: 'rhythm' })
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)
</script>
<style scoped>
.yxg-card{margin:8px 0 12px}
.mt{margin-top:14px}
.head{margin:4px 0 8px;font-size:13px}
.lock .yxg-btn{margin-top:12px;width:100%}
.report-link{display:block;margin-top:12px}
</style>