Files
digital-psychology/apps/user-h5/src/pages/ReportsPage.vue
T
jackyu66gitandCursor 53eb4577b3 feat: 按测测功能重构 H5 首页与各子页,并修正顶栏「+」添加档案菜单
对齐测测交互:首页「+」支持邀请填档案/添加档案/邀请合盘;各 Tab 与工具页按同一视觉与功能标准落地;本地对照截图目录显式忽略。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-05 14:32:35 +08:00

300 lines
7.8 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<PageShell>
<template #hero>
<div class="top-row">
<BackButton />
<span class="who-static">我的成长报告</span>
<span class="spacer" />
</div>
</template>
<div class="chips">
<button
v-for="f in filters"
:key="f.key"
type="button"
class="chip"
:class="{ on: filter === f.key }"
@click="filter = f.key"
>
{{ f.label }}
</button>
</div>
<div class="body">
<router-link class="vip-strip" to="/membership">
<span class="vip-ico" aria-hidden="true"></span>
<span class="vip-copy">
<strong>成长会员</strong>
<em>解锁全部深度报告与节气陪伴</em>
</span>
<span class="vip-go">开通 </span>
</router-link>
<p v-if="loading" class="hint">加载中</p>
<p v-else-if="error" class="err">
{{ error }}
<button type="button" class="retry" @click="load">重试</button>
</p>
<template v-else>
<div v-if="!filtered.length" class="empty">
<HomeToolIcon name="reports" :size="56" />
<p>还没有{{ filter === 'all' ? '' : '该类' }}成长报告</p>
<router-link class="cta" to="/explore">去探索</router-link>
</div>
<div v-else class="list">
<router-link v-for="r in filtered" :key="r.id" :to="`/reports/${r.id}`" class="report-card">
<span class="thumb" aria-hidden="true">
<HomeToolIcon :name="toolIconFor(r.type)" :size="44" />
</span>
<span class="card-body">
<span class="card-title">
{{ typeLabel(r.type) }}
<span v-if="badgeFor(r)" class="badge" :class="'b-' + badgeFor(r)">{{ badgeLabel(badgeFor(r)!) }}</span>
</span>
<span class="card-desc">{{ headlineOf(r) }}</span>
<span class="card-date">{{ formatDate(r.created_at) }}</span>
</span>
<span class="card-cta">查看报告</span>
</router-link>
</div>
</template>
</div>
</PageShell>
</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 HomeToolIcon from '../components/HomeToolIcon.vue'
import PageShell from '../components/PageShell.vue'
import { toolIconFor } from '../lib/toolIconMap'
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).toLocaleDateString('zh-CN')
} catch {
return v
}
}
function badgeFor(r: GrowthReport): 'new' | 'hot' | null {
const created = new Date(r.created_at).getTime()
const weekAgo = Date.now() - 7 * 86400000
if (created > weekAgo) return 'new'
if (r.type === 'portrait' || r.type === 'star') return 'hot'
return null
}
function badgeLabel(b: 'new' | 'hot') {
return b === 'new' ? 'NEW' : 'HOT'
}
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>
.top-row {
display: flex;
align-items: center;
gap: 10px;
padding-bottom: 4px;
}
.who-static { flex: 1; font-size: 16px; font-weight: 700; text-align: center; }
.spacer { width: 36px; }
.chips {
display: flex;
gap: 8px;
padding: 8px 16px 0;
overflow-x: auto;
scrollbar-width: none;
}
.chips::-webkit-scrollbar { display: none; }
.chip {
flex-shrink: 0;
padding: 7px 14px;
border-radius: var(--radius-pill);
border: 1px solid #f0ebe8;
background: #fafafa;
font-size: 12px;
color: #666;
}
.chip.on {
background: #fff;
color: var(--color-primary);
font-weight: 600;
border-color: #f5c4bc;
box-shadow: 0 2px 8px rgba(229, 77, 66, 0.1);
}
.body { padding: 12px 16px 24px; }
.vip-strip {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 14px;
padding: 12px 14px;
border-radius: var(--radius-lg);
background: linear-gradient(145deg, #fff4e0, #ffe8b8);
box-shadow: 0 4px 14px rgba(200, 146, 58, 0.12);
color: inherit;
}
.vip-ico {
width: 32px;
height: 32px;
border-radius: 10px;
background: linear-gradient(135deg, #f0c86a, #e8a830);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
flex-shrink: 0;
}
.vip-copy { flex: 1; min-width: 0; }
.vip-copy strong { display: block; font-size: 14px; color: #222; }
.vip-copy em { display: block; font-style: normal; font-size: 11px; color: #8a7040; margin-top: 2px; }
.vip-go { font-size: 13px; font-weight: 600; color: #b07a28; flex-shrink: 0; }
.list { display: flex; flex-direction: column; gap: 10px; }
.report-card {
display: flex;
align-items: center;
gap: 12px;
padding: 14px;
border-radius: var(--radius-xl);
background: #fff;
box-shadow: var(--shadow-card);
border: 1px solid rgba(0, 0, 0, 0.03);
color: inherit;
transition: transform var(--duration-fast) ease;
}
.report-card:active { transform: scale(0.985); }
.thumb {
flex-shrink: 0;
width: 52px;
height: 52px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 14px;
background: linear-gradient(155deg, #ffe9e2, #ffd4c8);
}
.card-body { flex: 1; min-width: 0; }
.card-title {
display: flex;
align-items: center;
gap: 6px;
font-size: 15px;
font-weight: 700;
color: #222;
}
.badge {
font-size: 9px;
font-weight: 700;
color: #fff;
padding: 2px 6px;
border-radius: 6px;
}
.badge.b-hot { background: linear-gradient(135deg, #ff7a6e, var(--color-primary)); }
.badge.b-new { background: linear-gradient(135deg, #6eb6ff, #4a90e2); }
.card-desc {
display: block;
font-size: 12px;
color: #888;
margin-top: 4px;
line-height: 1.4;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.card-date { display: block; font-size: 11px; color: #c4c4c4; margin-top: 4px; }
.card-cta {
flex-shrink: 0;
font-size: 12px;
font-weight: 600;
color: var(--color-primary);
padding: 8px 12px;
border-radius: var(--radius-pill);
background: #fff5f4;
border: 1px solid #ffe0dc;
}
.empty {
text-align: center;
padding: 32px 16px;
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
color: #888;
}
.cta {
display: inline-flex;
padding: 11px 20px;
border-radius: 22px;
background: linear-gradient(135deg, #ff7a6e, var(--color-primary));
color: #fff;
font-weight: 600;
font-size: 14px;
}
.hint, .err { font-size: 13px; color: #999; }
.err { color: var(--color-primary); }
.retry {
border: none;
background: transparent;
color: var(--color-accent-blue);
text-decoration: underline;
margin-left: 6px;
}
</style>