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
+118
View File
@@ -0,0 +1,118 @@
<template>
<main class="yxg-page">
<div class="yxg-hero">
<BackButton />
<PageTitle feature="reports" title="我的成长报告" sub="按类型筛选 · 愈心解码 / 星座 / 节律 / 匹配 / 意象" />
</div>
<div class="yxg-sheet">
<div class="yxg-chips">
<button
v-for="f in filters"
:key="f.key"
type="button"
class="yxg-chip yxg-chip-solid"
:class="{ on: filter === f.key }"
@click="filter = f.key"
>{{ f.label }}</button>
</div>
<p v-if="loading" class="yxg-hint pad">加载中</p>
<p v-else-if="error" class="yxg-err pad">
{{ error }}
<button type="button" class="yxg-link" @click="load">重试</button>
</p>
<template v-else>
<div v-if="!filtered.length" class="yxg-card empty">
<p>还没有{{ filter === 'all' ? '' : '该类' }}成长报告</p>
<router-link class="yxg-btn" to="/explore">去探索</router-link>
</div>
<ul v-else class="yxg-list">
<li v-for="r in filtered" :key="r.id">
<router-link :to="`/reports/${r.id}`">
<span class="yxg-body">
<strong>{{ typeLabel(r.type) }}</strong>
<span class="desc">{{ headlineOf(r) }}</span>
<span class="meta">{{ formatDate(r.created_at) }}</span>
</span>
<span class="chev" aria-hidden="true"></span>
</router-link>
</li>
</ul>
</template>
</div>
</main>
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import type { GrowthReport } from '@yuxingu/types'
import { api } from '../api/client'
import BackButton from '../components/BackButton.vue'
import PageTitle from '../components/PageTitle.vue'
const items = ref<GrowthReport[]>([])
const loading = ref(true)
const error = ref('')
const filter = ref('all')
const filters = [
{ key: 'all', label: '全部' },
{ key: 'portrait', label: '解码' },
{ key: 'star', label: '星座' },
{ key: 'synastry', label: '合盘' },
{ key: 'rhythm', label: '节律' },
{ key: 'relation', label: '匹配' },
{ key: 'image_card', label: '意象' },
]
const filtered = computed(() => {
if (filter.value === 'all') return items.value
return items.value.filter((r) => r.type === filter.value)
})
function typeLabel(t: string) {
const map: Record<string, string> = {
portrait: '△ 愈心解码',
relation: '♡ 人格匹配',
synastry: '✧ 合盘',
star: '✦ 星座',
rhythm: '☯ 身心节律',
image_card: '◈ 意象卡片',
}
return map[t] || '▣ 成长报告'
}
function headlineOf(r: GrowthReport) {
const s = (r.summary || {}) as Record<string, unknown>
return String(s.headline || s.diff_one_liner || s.one_liner || '查看报告')
}
function formatDate(v: string) {
try {
return new Date(v).toLocaleString('zh-CN')
} catch {
return v
}
}
async function load() {
loading.value = true
error.value = ''
try {
const res = await api.listReports()
items.value = res.items || []
} catch (e) {
error.value = e instanceof Error ? e.message : '加载失败'
} finally {
loading.value = false
}
}
onMounted(load)
</script>
<style scoped>
.pad{padding:0 16px}
.empty .yxg-btn{margin-top:12px}
</style>