落地埋点 ingest/数据看板、首页宫格 CMS 与测评上下架;含账号引导、问答流式与免责声明去重,以及 review P1 审计同事务修复。 Co-authored-by: Cursor <cursoragent@cursor.com>
262 lines
6.5 KiB
Vue
262 lines
6.5 KiB
Vue
<template>
|
||
<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">{{ accountLabel }}</p>
|
||
<p class="meta">档案 {{ profileCount }} 份</p>
|
||
</div>
|
||
<router-link v-if="!loggedIn" class="edit" to="/login">登录 ›</router-link>
|
||
<button v-else type="button" class="edit linkish" @click="logout">退出</button>
|
||
</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 { useRouter } from 'vue-router'
|
||
import type { AuthUser, MembershipMe } from '@yuxingu/types'
|
||
import { api } from '../api/client'
|
||
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'
|
||
import { clearAuthToken } from '../lib/authSession'
|
||
|
||
const router = useRouter()
|
||
|
||
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)
|
||
const error = ref('')
|
||
const membership = ref<MembershipMe | null>(null)
|
||
const profileCount = ref(0)
|
||
const me = ref<AuthUser | null>(null)
|
||
|
||
const loggedIn = computed(() => !!me.value)
|
||
const accountLabel = computed(() => me.value?.nickname || me.value?.phone || '未登录')
|
||
|
||
const planLabel = computed(() => {
|
||
const p = membership.value?.plan
|
||
if (p === 'quarter') return '季卡'
|
||
if (p === 'year') return '年卡'
|
||
if (p === 'month') return '月卡'
|
||
return '会员中'
|
||
})
|
||
|
||
async function load() {
|
||
loading.value = true
|
||
error.value = ''
|
||
try {
|
||
me.value = await api.authMe()
|
||
const [m, profiles] = await Promise.all([api.getMembership(), api.listProfiles()])
|
||
membership.value = m
|
||
profileCount.value = (profiles.items || []).length
|
||
} catch (e) {
|
||
me.value = null
|
||
membership.value = null
|
||
profileCount.value = 0
|
||
error.value = e instanceof Error ? e.message : '请先登录'
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
async function logout() {
|
||
try {
|
||
await api.authLogout()
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
clearAuthToken()
|
||
me.value = null
|
||
await router.push('/login')
|
||
}
|
||
|
||
onMounted(load)
|
||
</script>
|
||
|
||
<style scoped>
|
||
.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;
|
||
}
|
||
.linkish {
|
||
border: 0;
|
||
background: none;
|
||
padding: 0;
|
||
cursor: pointer;
|
||
}
|
||
.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>
|