feat: 按测测功能重构 H5 首页与各子页,并修正顶栏「+」添加档案菜单

对齐测测交互:首页「+」支持邀请填档案/添加档案/邀请合盘;各 Tab 与工具页按同一视觉与功能标准落地;本地对照截图目录显式忽略。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jackyu66git
2026-08-05 14:32:35 +08:00
co-authored by Cursor
parent bd22d9dddd
commit 53eb4577b3
35 changed files with 8900 additions and 1356 deletions
+167 -30
View File
@@ -1,28 +1,44 @@
<template>
<main class="yxg-page decode-page">
<div class="yxg-hero">
<PageShell>
<template #hero>
<BackButton />
<PageTitle
feature="portrait"
title="愈心解码"
:sub="needBirth && !report ? '填写生日,生成身心解码报告' : '一个生日,读懂身与心'"
/>
</div>
<div class="yxg-sheet sheet-pad">
<div v-if="!report" class="head">
<HomeToolIcon name="portrait" :size="48" />
<div>
<h1 class="title">愈心解码</h1>
<p class="sub">一个生日读懂身与心</p>
</div>
</div>
<div v-else class="head compact">
<HomeToolIcon name="portrait" :size="40" />
<h1 class="title sm">愈心解码</h1>
</div>
</template>
<div class="body" :class="{ 'has-sticky': !!report }">
<template v-if="needBirth && !report">
<div class="yxg-card">
<BirthDateInputs v-model:year="year" v-model:month="month" v-model:day="day" />
<button class="yxg-btn yxg-btn-block mt" type="button" :disabled="loading" @click="start">
{{ loading ? '解码中' : '开始解码' }}
</button>
<p v-if="formError" class="yxg-err">{{ formError }}</p>
<div class="decode-cta">
<div class="decode-inner">
<HomeToolIcon name="portrait" :size="56" />
<h2 class="decode-title">愈心解码</h2>
<p class="decode-sub">一个生日读懂性格与身心节奏</p>
<div class="decode-inputs">
<BirthDateInputs v-model:year="year" v-model:month="month" v-model:day="day" />
<button class="decode-btn" type="button" :disabled="loading" @click="start">
{{ loading ? '解码中' : '开始解码' }}
</button>
</div>
<p v-if="formError" class="yxg-err">{{ formError }}</p>
</div>
</div>
</template>
<p v-else-if="loading" class="yxg-hint">正在解码</p>
<p v-else-if="error" class="yxg-err">
<p v-else-if="error && !report" class="yxg-err">
{{ error }}
<button type="button" class="yxg-link" @click="retry">重试</button>
</p>
<template v-else-if="report">
<DecodePanel
:summary="summary"
@@ -32,16 +48,23 @@
@unlock="buyDeep"
/>
<p v-if="error" class="yxg-err">{{ error }}</p>
<div class="links">
<router-link class="yxg-link-plain" to="/relation">去做人格匹配 </router-link>
<router-link v-if="report.id" class="yxg-link-plain" :to="`/reports/${report.id}`">在报告页打开 </router-link>
</div>
<button type="button" class="yxg-btn yxg-btn-ghost share-btn" @click="shareOpen = true">生成分享卡</button>
<router-link v-if="report.id" class="yxg-link-plain report-link" :to="`/reports/${report.id}`">
在报告页打开
</router-link>
</template>
<p class="yxg-disc">自我探索与生活方式参考不是占卜或算命体质内容非医疗建议</p>
</div>
<!-- Sticky bottom CTAs -->
<div v-if="report" class="sticky-bar">
<router-link class="sticky-btn" to="/ask">去问答</router-link>
<router-link class="sticky-btn primary" to="/relation">去做匹配</router-link>
<button type="button" class="sticky-btn ghost" @click="shareOpen = true">分享</button>
</div>
<ShareSheet :open="shareOpen" :payload="sharePayload" @close="shareOpen = false" />
</main>
</PageShell>
</template>
<script setup lang="ts">
@@ -53,7 +76,8 @@ import { api } from '../api/client'
import BackButton from '../components/BackButton.vue'
import BirthDateInputs from '../components/BirthDateInputs.vue'
import DecodePanel from '../components/DecodePanel.vue'
import PageTitle from '../components/PageTitle.vue'
import HomeToolIcon from '../components/HomeToolIcon.vue'
import PageShell from '../components/PageShell.vue'
import ShareSheet from '../components/ShareSheet.vue'
import { AnalyticsEvent, track } from '../lib/analytics'
import type { PortraitSharePayload } from '../lib/shareLink'
@@ -176,17 +200,130 @@ onMounted(load)
</script>
<style scoped>
.mt {
margin-top: 14px;
}
.share-btn {
.head {
display: flex;
align-items: center;
gap: 12px;
margin-top: 8px;
width: 100%;
padding-bottom: 8px;
}
.links {
.head.compact {
padding-bottom: 4px;
}
.title {
font-family: var(--font-display);
font-size: 22px;
font-weight: 700;
letter-spacing: 0.06em;
}
.title.sm {
font-size: 18px;
}
.sub {
font-size: 12px;
color: rgba(0, 0, 0, 0.38);
margin-top: 2px;
}
.body {
padding: 8px 16px 0;
}
.body.has-sticky {
padding-bottom: 72px;
}
.decode-cta {
margin: 8px 0 16px;
padding: 28px 20px 24px;
background: var(--color-surface);
border-radius: var(--radius-xl);
box-shadow: var(--shadow-hero);
text-align: center;
}
.decode-inner {
display: flex;
flex-direction: column;
align-items: center;
}
.decode-title {
font-family: var(--font-display);
font-size: 20px;
font-weight: 700;
color: var(--color-accent-gold);
letter-spacing: 0.08em;
margin: 14px 0 4px;
}
.decode-sub {
font-size: 13px;
color: var(--color-text-tertiary);
margin: 0 0 18px;
}
.decode-inputs {
width: 100%;
display: flex;
flex-direction: column;
gap: 14px;
align-items: center;
}
.decode-inputs :deep(.bd) {
justify-content: center;
}
.decode-btn {
width: 100%;
max-width: 280px;
padding: 14px 24px;
border: none;
border-radius: 24px;
color: #fff;
font-size: 16px;
font-weight: 700;
background: linear-gradient(135deg, #ff7a6e, var(--color-primary));
box-shadow: 0 6px 18px rgba(229, 77, 66, 0.28);
cursor: pointer;
}
.decode-btn:disabled {
opacity: 0.65;
}
.report-link {
display: block;
margin: 12px 0;
}
.sticky-bar {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 20;
display: flex;
gap: 8px;
margin: 4px 0 12px;
padding: 10px 14px calc(10px + env(safe-area-inset-bottom, 0));
background: rgba(255, 255, 255, 0.94);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border-top: 1px solid rgba(0, 0, 0, 0.06);
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.06);
}
.sticky-btn {
flex: 1;
padding: 12px 8px;
border-radius: 20px;
font-size: 14px;
font-weight: 600;
text-align: center;
text-decoration: none;
border: 1.5px solid #eee;
background: #fff;
color: var(--color-text-primary);
cursor: pointer;
}
.sticky-btn.primary {
background: linear-gradient(135deg, #ff7a6e, var(--color-primary));
color: #fff;
border-color: transparent;
box-shadow: 0 4px 12px rgba(229, 77, 66, 0.22);
}
.sticky-btn.ghost {
flex: 0.8;
}
</style>