feat: 按测测功能重构 H5 首页与各子页,并修正顶栏「+」添加档案菜单
对齐测测交互:首页「+」支持邀请填档案/添加档案/邀请合盘;各 Tab 与工具页按同一视觉与功能标准落地;本地对照截图目录显式忽略。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,47 +1,66 @@
|
||||
<template>
|
||||
<main class="yxg-page">
|
||||
<div class="yxg-hero">
|
||||
<BackButton />
|
||||
<PageTitle feature="reports" title="我的成长报告" sub="按类型筛选 · 愈心解码 / 星座 / 节律 / 匹配 / 意象" />
|
||||
<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="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>
|
||||
<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="yxg-hint pad">加载中…</p>
|
||||
<p v-else-if="error" class="yxg-err pad">
|
||||
<p v-if="loading" class="hint">加载中…</p>
|
||||
<p v-else-if="error" class="err">
|
||||
{{ error }}
|
||||
<button type="button" class="yxg-link" @click="load">重试</button>
|
||||
<button type="button" class="retry" @click="load">重试</button>
|
||||
</p>
|
||||
<template v-else>
|
||||
<div v-if="!filtered.length" class="yxg-card empty">
|
||||
<div v-if="!filtered.length" class="empty">
|
||||
<HomeToolIcon name="reports" :size="56" />
|
||||
<p>还没有{{ filter === 'all' ? '' : '该类' }}成长报告。</p>
|
||||
<router-link class="yxg-btn" to="/explore">去探索</router-link>
|
||||
<router-link class="cta" 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>
|
||||
<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="chev" aria-hidden="true">›</span>
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
<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>
|
||||
</main>
|
||||
</PageShell>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -49,7 +68,9 @@ 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'
|
||||
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)
|
||||
@@ -73,14 +94,14 @@ const filtered = computed(() => {
|
||||
|
||||
function typeLabel(t: string) {
|
||||
const map: Record<string, string> = {
|
||||
portrait: '△ 愈心解码',
|
||||
relation: '♡ 人格匹配',
|
||||
synastry: '✧ 合盘',
|
||||
star: '✦ 星座',
|
||||
rhythm: '☯ 身心节律',
|
||||
image_card: '◈ 意象卡片',
|
||||
portrait: '愈心解码',
|
||||
relation: '人格匹配',
|
||||
synastry: '合盘',
|
||||
star: '星座',
|
||||
rhythm: '身心节律',
|
||||
image_card: '意象卡片',
|
||||
}
|
||||
return map[t] || '▣ 成长报告'
|
||||
return map[t] || '成长报告'
|
||||
}
|
||||
|
||||
function headlineOf(r: GrowthReport) {
|
||||
@@ -90,12 +111,24 @@ function headlineOf(r: GrowthReport) {
|
||||
|
||||
function formatDate(v: string) {
|
||||
try {
|
||||
return new Date(v).toLocaleString('zh-CN')
|
||||
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 = ''
|
||||
@@ -113,6 +146,154 @@ onMounted(load)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.pad{padding:0 16px}
|
||||
.empty .yxg-btn{margin-top:12px}
|
||||
.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>
|
||||
|
||||
Reference in New Issue
Block a user