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
+189 -56
View File
@@ -1,61 +1,97 @@
<template>
<main class="yxg-page">
<div class="yxg-hero">
<PageTitle feature="profile" title="我的" sub="个人档案 · 成长报告 · 成长会员" />
</div>
<div class="yxg-sheet">
<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 class="yxg-card member">
<p class="yxg-chip-ico">
<span class="yxg-mark icon-purple tiny" aria-hidden="true"></span>
<span class="member-text">
<strong v-if="membership?.active">成长会员有效 · {{ planLabel }}</strong>
<strong v-else>尚未开通成长会员</strong>
<span class="yxg-meta">档案 {{ profileCount }} </span>
</span>
</p>
<router-link class="yxg-btn" to="/membership">
{{ membership?.active ? '查看会员' : '开通成长会员' }}
</router-link>
<PageShell>
<template #hero>
<div class="profile-head">
<span class="av" aria-hidden="true">
<BrandLogo size="card" class="av-logo" />
</span>
<div class="who">
<p class="name">愈心谷用户</p>
<p class="meta">档案 {{ profileCount }} </p>
</div>
<ul class="yxg-list">
<li v-for="item in links" :key="item.to">
<router-link :to="item.to">
<span class="yxg-mark" :class="'icon-' + item.tone" aria-hidden="true">{{ item.icon }}</span>
<span class="yxg-body"><strong>{{ item.label }}</strong></span>
<span class="chev" aria-hidden="true"></span>
</router-link>
</li>
</ul>
</template>
</div>
</main>
<router-link class="edit" to="/profile">编辑 </router-link>
</div>
</template>
<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>
<!-- 资产条 -->
<section class="assets">
<router-link to="/membership" class="asset">
<strong>{{ membership?.active ? planLabel : '未开通' }}</strong>
<span>成长会员</span>
</router-link>
<router-link to="/ask" class="asset">
<strong>{{ profileCount }}</strong>
<span>档案</span>
</router-link>
<router-link to="/reports" class="asset">
<strong>报告</strong>
<span>成长记录</span>
</router-link>
<router-link to="/membership" class="asset accent">
<strong>{{ membership?.active ? '续费' : '开通' }}</strong>
<span>会员中心</span>
</router-link>
</section>
<section class="sec">
<p class="sec-title">档案与内容</p>
<div class="group">
<ListRow
v-for="item in groupArchive"
:key="item.to"
:to="item.to"
:title="item.label"
:icon="item.icon"
/>
</div>
</section>
<section class="sec">
<p class="sec-title">成长与会员</p>
<div class="group">
<ListRow
v-for="item in groupGrowth"
:key="item.to"
:to="item.to"
:title="item.label"
:icon="item.icon"
/>
</div>
</section>
</template>
</PageShell>
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import type { MembershipMe } from '@yuxingu/types'
import { api } from '../api/client'
import PageTitle from '../components/PageTitle.vue'
import type { IconTone } from '../lib/featureIcons'
import BrandLogo from '../components/BrandLogo.vue'
import ListRow from '../components/ListRow.vue'
import PageShell from '../components/PageShell.vue'
import type { HomeToolIconName } from '../components/HomeToolIcon.vue'
const links: { to: string; icon: string; label: string; tone: IconTone }[] = [
{ to: '/profile', icon: '◍', label: '个人档案', tone: 'mute' },
{ to: '/reports', icon: '▣', label: '我的成长报告', tone: 'orange' },
{ to: '/', icon: '△', label: '个人画像', tone: 'gold' },
{ to: '/star', icon: '✦', label: '星象性格', tone: 'night' },
{ to: '/rhythm', icon: '☯', label: '身心节律', tone: 'green' },
{ to: '/cards', icon: '◈', label: '意象卡片', tone: 'teal' },
{ to: '/relation', icon: '♡', label: '关系理解', tone: 'rose' },
{ to: '/ask', icon: '◎', label: 'AI 成长助手', tone: 'gold' },
{ to: '/explore', icon: '◆', label: '探索测试', tone: 'blue' },
{ to: '/membership', icon: '⑨', label: '成长会员', tone: 'purple' },
const groupArchive: { to: string; label: string; icon: HomeToolIconName }[] = [
{ to: '/profile', label: '个人档案', icon: 'portrait' },
{ to: '/reports', label: '我的成长报告', icon: 'reports' },
{ to: '/portrait', label: '愈心解码', icon: 'portrait' },
{ to: '/star', label: '星象性格', icon: 'star' },
{ to: '/rhythm', label: '身心节律', icon: 'rhythm' },
{ to: '/cards', label: '意象卡片', icon: 'cards' },
]
const groupGrowth: { to: string; label: string; icon: HomeToolIconName }[] = [
{ to: '/relation', label: '人格匹配', icon: 'relation' },
{ to: '/ask', label: 'AI 成长助手', icon: 'ask' },
{ to: '/explore', label: '探索测试', icon: 'mbti' },
{ to: '/growth-plan', label: '成长计划', icon: 'growth' },
{ to: '/membership', label: '成长会员', icon: 'growth' },
]
const loading = ref(true)
@@ -68,7 +104,7 @@ const planLabel = computed(() => {
if (p === 'quarter') return '季卡'
if (p === 'year') return '年卡'
if (p === 'month') return '月卡'
return '成长会员'
return '会员'
})
async function load() {
@@ -89,10 +125,107 @@ onMounted(load)
</script>
<style scoped>
.pad{padding:8px 16px}
.member{display:flex;flex-direction:column;gap:12px;align-items:flex-start}
.member-text{display:flex;flex-direction:column;gap:2px}
.member-text strong{font-size:15px;color:#222;font-weight:650}
.tiny{width:36px!important;height:36px!important;font-size:16px!important;border-radius:12px!important}
.yxg-btn{margin-top:2px}
.profile-head {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 0 12px;
}
.av {
width: 56px;
height: 56px;
border-radius: 50%;
background: linear-gradient(145deg, #ffe8e0, #ffd0c4);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.7);
flex-shrink: 0;
}
.av-logo {
width: 34px !important;
height: auto !important;
max-height: none !important;
}
.who { flex: 1; min-width: 0; }
.name {
font-size: 18px;
font-weight: 700;
color: var(--color-text-primary);
}
.meta {
font-size: 12px;
color: #999;
margin-top: 2px;
}
.edit {
font-size: 12px;
color: #bbb;
flex-shrink: 0;
}
.assets {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
padding: 0 16px;
margin-top: 4px;
}
.asset {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
padding: 12px 4px;
background: #fafafa;
border-radius: var(--radius-md);
text-align: center;
}
.asset strong {
font-size: 13px;
font-weight: 700;
color: #222;
}
.asset span {
font-size: 10px;
color: #999;
}
.asset.accent strong {
color: var(--color-primary);
}
.sec {
padding: 0 16px;
margin-top: 20px;
}
.sec-title {
font-size: 13px;
font-weight: 650;
color: #999;
margin-bottom: 8px;
letter-spacing: 0.04em;
}
.group {
background: #fafafa;
border-radius: var(--radius-lg);
padding: 2px 10px;
}
.group :deep(.lr) {
border-bottom: 1px solid #f0ebe8;
}
.group :deep(.lr:last-child) {
border-bottom: none;
}
.hint, .err {
padding: 16px;
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>