feat: add RelationInsight API and wire H5 relation/profile pages
Second P1 growth engine: compare two profiles, gate full tips behind deep-access mock payment, and list personal archives on /profile. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3,18 +3,49 @@
|
||||
<button class="back" type="button" @click="$router.back()">← 返回</button>
|
||||
<h1>个人档案</h1>
|
||||
<p class="sub">管理我的信息与重要的人</p>
|
||||
<div class="card">
|
||||
支持创建我的档案、添加伴侣/家人/朋友,并在问答中切换解读对象。数据接口:/api/v1/profiles。
|
||||
</div>
|
||||
<router-link class="link" to="/relation">去完善关系理解 →</router-link>
|
||||
<p v-if="error" class="err">{{ error }}</p>
|
||||
<ul v-if="items.length" class="list">
|
||||
<li v-for="p in items" :key="p.id">
|
||||
<strong>{{ p.display_name || (p.relation === 'self' ? '我' : 'TA') }}</strong>
|
||||
· {{ p.relation === 'self' ? '我' : '关系对象' }}
|
||||
· {{ formatDate(p.birth_date) }}
|
||||
</li>
|
||||
</ul>
|
||||
<div v-else class="card">暂无档案。请先去首页完成性格探索。</div>
|
||||
<router-link class="link" to="/">去性格探索 →</router-link>
|
||||
<router-link class="link" to="/relation">去关系理解 →</router-link>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import type { Profile } from '@yuxingu/types'
|
||||
import { api } from '../api/client'
|
||||
|
||||
const items = ref<Profile[]>([])
|
||||
const error = ref('')
|
||||
|
||||
function formatDate(v: string) {
|
||||
return (v || '').slice(0, 10)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const res = await api.listProfiles()
|
||||
items.value = res.items || []
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : '加载失败'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page{padding:16px}
|
||||
.back{border:none;background:#fff;border-radius:10px;padding:8px 12px;margin-bottom:12px}
|
||||
h1{font-size:22px}
|
||||
.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}
|
||||
.link{display:inline-block;margin-top:16px;font-size:14px;color:var(--yxg-pri)}
|
||||
.err{color:var(--yxg-pri);font-size:13px}
|
||||
.card{background:#fff;border-radius:16px;padding:18px;font-size:14px;color:#555}
|
||||
.list{background:#fff;border-radius:16px;padding:8px 16px;line-height:2.1;font-size:14px;color:#555;margin-bottom:12px}
|
||||
.link{display:block;margin-top:12px;font-size:14px;color:var(--yxg-pri)}
|
||||
</style>
|
||||
|
||||
@@ -3,16 +3,143 @@
|
||||
<button class="back" type="button" @click="$router.back()">← 返回</button>
|
||||
<h1>关系理解</h1>
|
||||
<p class="sub">添加重要的人,了解双方差异与相处建议</p>
|
||||
|
||||
<div class="card">
|
||||
双人分析与深度相处建议将在 P1 接入 API。概览免费可见,完整建议为深度版或会员权益。
|
||||
<label>TA 的称呼</label>
|
||||
<input v-model="otherName" placeholder="例如:伴侣" />
|
||||
<label>TA 的生日</label>
|
||||
<div class="row">
|
||||
<input v-model.number="oy" type="number" placeholder="年" />
|
||||
<input v-model.number="om" type="number" placeholder="月" />
|
||||
<input v-model.number="od" type="number" placeholder="日" />
|
||||
</div>
|
||||
<p class="hint">将使用你最近一份「我」的档案进行对比;若没有会先引导去首页创建个人画像。</p>
|
||||
<button type="button" :disabled="loading" @click="run">生成关系理解</button>
|
||||
<p v-if="error" class="err">{{ error }}</p>
|
||||
</div>
|
||||
|
||||
<template v-if="report">
|
||||
<div class="card">
|
||||
<p><strong>我</strong>:{{ meStyle }}</p>
|
||||
<p><strong>TA</strong>:{{ otherStyle }}</p>
|
||||
<p class="line">{{ diff }}</p>
|
||||
<div class="tags">
|
||||
<span v-for="k in meKeys" :key="'a'+k">我·{{ k }}</span>
|
||||
<span v-for="k in otherKeys" :key="'b'+k">TA·{{ k }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="report.has_deep_access && detail" class="card">
|
||||
<h2>相处建议</h2>
|
||||
<ul>
|
||||
<li v-for="(t, i) in tips" :key="i">{{ t }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-else class="card lock">
|
||||
<p>完整相处建议可在深度版或成长会员中查看。</p>
|
||||
<button type="button" :disabled="paying" @click="buyDeep">查看深度版(模拟支付)</button>
|
||||
</div>
|
||||
</template>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import type { GrowthReport, Profile } from '@yuxingu/types'
|
||||
import { api } from '../api/client'
|
||||
|
||||
const otherName = ref('TA')
|
||||
const oy = ref<number | null>(1992)
|
||||
const om = ref<number | null>(6)
|
||||
const od = ref<number | null>(8)
|
||||
const loading = ref(false)
|
||||
const paying = ref(false)
|
||||
const error = ref('')
|
||||
const report = ref<GrowthReport | null>(null)
|
||||
|
||||
const summary = computed(() => (report.value?.summary || {}) as Record<string, unknown>)
|
||||
const detail = computed(() => (report.value?.detail || null) as Record<string, unknown> | null)
|
||||
const meStyle = computed(() => String(summary.value.me_style || ''))
|
||||
const otherStyle = computed(() => String(summary.value.other_style || ''))
|
||||
const diff = computed(() => String(summary.value.diff_one_liner || ''))
|
||||
const meKeys = computed(() => (Array.isArray(summary.value.me_keywords) ? summary.value.me_keywords as string[] : []))
|
||||
const otherKeys = computed(() => (Array.isArray(summary.value.other_keywords) ? summary.value.other_keywords as string[] : []))
|
||||
const tips = computed(() => {
|
||||
const d = detail.value
|
||||
if (!d) return [] as string[]
|
||||
const a = Array.isArray(d.communication) ? d.communication as string[] : []
|
||||
const b = Array.isArray(d.interaction) ? d.interaction as string[] : []
|
||||
const c = Array.isArray(d.maintenance) ? d.maintenance as string[] : []
|
||||
return [...a, ...b, ...c]
|
||||
})
|
||||
|
||||
async function ensureSelf(): Promise<Profile> {
|
||||
const { items } = await api.listProfiles()
|
||||
const self = (items || []).find((p) => p.relation === 'self')
|
||||
if (self) return self
|
||||
throw new Error('请先在首页完成性格探索,创建「我」的个人档案')
|
||||
}
|
||||
|
||||
async function run() {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
report.value = null
|
||||
try {
|
||||
if (!oy.value || !om.value || !od.value) {
|
||||
throw new Error('请填写 TA 的完整生日')
|
||||
}
|
||||
const self = await ensureSelf()
|
||||
const birth = `${oy.value}-${String(om.value).padStart(2, '0')}-${String(od.value).padStart(2, '0')}`
|
||||
const other = await api.createProfile({
|
||||
relation: 'other',
|
||||
birth_date: birth,
|
||||
display_name: otherName.value || 'TA',
|
||||
relation_type: 'partner',
|
||||
})
|
||||
const out = await api.createRelationInsight(self.id, other.id)
|
||||
report.value = out.report
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : '生成失败'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function buyDeep() {
|
||||
if (!report.value) return
|
||||
paying.value = true
|
||||
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
|
||||
}
|
||||
}
|
||||
</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}
|
||||
.card{background:#fff;border-radius:16px;padding:18px;font-size:14px;line-height:1.7;color:#555;margin-bottom:12px}
|
||||
label{display:block;font-size:12px;color:#999;margin:8px 0 4px}
|
||||
input{width:100%;box-sizing:border-box;padding:10px;border:1.5px solid #eee;border-radius:12px;margin-bottom:4px}
|
||||
.row{display:flex;gap:8px}
|
||||
.row input{flex:1}
|
||||
.hint{font-size:12px;color:#aaa;margin:8px 0}
|
||||
button{
|
||||
margin-top:10px;padding:10px 16px;border:none;border-radius:22px;color:#fff;font-weight:600;
|
||||
background:linear-gradient(135deg,#ff7a6e,var(--yxg-pri));
|
||||
}
|
||||
button:disabled{opacity:.6}
|
||||
.err{color:var(--yxg-pri);font-size:13px;margin-top:8px}
|
||||
.line{margin-top:8px;color:#333}
|
||||
.tags{display:flex;flex-wrap:wrap;gap:8px;margin-top:10px}
|
||||
.tags span{background:#fff3d6;color:#c8923a;padding:4px 10px;border-radius:999px;font-size:12px}
|
||||
ul{padding-left:18px;margin:0}
|
||||
li{margin:6px 0}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user