feat: P1 profile/portrait API and freeze local-dev environment
Add host-first environment contracts (Local vs CI vs Prod), deps-only compose, and the Profile → Portrait → deep-access mock payment slice with device identity and auto-migrate on API startup. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2,31 +2,113 @@
|
||||
<main class="page">
|
||||
<button class="back" type="button" @click="$router.back()">← 返回</button>
|
||||
<h1>个人画像</h1>
|
||||
<p class="sub" v-if="y">生日:{{ y }}-{{ m }}-{{ d }}</p>
|
||||
<div class="card">
|
||||
基础画像将由 API 生成。免费可见基础结论;完整分析为「深度版」或会员权益(见 PRD)。
|
||||
</div>
|
||||
<p class="disc">
|
||||
本内容为自我探索与生活方式参考,不构成医疗建议,亦非占卜预测。
|
||||
</p>
|
||||
<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>
|
||||
</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>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import type { GrowthReport } from '@yuxingu/types'
|
||||
import { api } from '../api/client'
|
||||
|
||||
const route = useRoute()
|
||||
const y = computed(() => String(route.query.y || ''))
|
||||
const m = computed(() => String(route.query.m || ''))
|
||||
const d = computed(() => String(route.query.d || ''))
|
||||
const loading = ref(true)
|
||||
const error = ref('')
|
||||
const report = ref<GrowthReport | null>(null)
|
||||
const paying = ref(false)
|
||||
|
||||
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[] : []))
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
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)
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : '加载失败'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function buyDeep() {
|
||||
if (!report.value) return
|
||||
paying.value = true
|
||||
error.value = ''
|
||||
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)
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : '支付失败'
|
||||
} finally {
|
||||
paying.value = false
|
||||
}
|
||||
}
|
||||
|
||||
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}
|
||||
.card{background:#fff;border-radius:16px;padding:18px;font-size:14px;line-height:1.7;color:#555}
|
||||
.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));
|
||||
}
|
||||
.lock button:disabled{opacity:.6}
|
||||
.disc{font-size:11px;color:#bbb;margin-top:16px;line-height:1.5}
|
||||
.deep p{margin-top:8px}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user