Files
digital-psychology/apps/user-h5/src/pages/ExplorePage.vue
T
jackyu66gitandCursor 7ab9add5dd
ci / h5 (push) Canceled after 0s
ci / api (push) Canceled after 0s
ci / ess-docs (push) Canceled after 0s
feat(ops): ECR-007 行为分析与 ECR-008 内容运营后台
落地埋点 ingest/数据看板、首页宫格 CMS 与测评上下架;含账号引导、问答流式与免责声明去重,以及 review P1 审计同事务修复。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-07 02:26:16 +08:00

200 lines
5.2 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="head">
<BackButton />
<h1 class="title">探索</h1>
<p class="sub">测评 · 工具 · 自我理解</p>
</div>
</template>
<!-- 推荐宫格对齐首页 12 + 截图更多 -->
<section class="sec">
<div class="sec-head">
<span class="sec-title">推荐工具</span>
</div>
<div class="grid">
<router-link
v-for="t in featured"
:key="t.to + t.label"
:to="t.to"
class="g-item"
>
<HomeToolIcon :name="t.icon" :size="52" />
<span class="g-label">{{ t.label }}</span>
<span v-if="t.badge" class="g-badge" :class="'b-' + (t.badgeTone || 'hot')">{{ t.badge }}</span>
</router-link>
</div>
</section>
<section class="sec">
<div class="sec-head">
<span class="sec-title">全部分类</span>
</div>
<p v-if="loading" class="hint">加载中</p>
<p v-else-if="error" class="err">
{{ error }}
<button type="button" class="retry" @click="load">重试</button>
</p>
<div v-else class="cat-list">
<ListRow
v-for="c in categories"
:key="c.key"
:to="`/explore/${c.key}`"
:title="c.title"
:desc="`${c.description} · ${c.items?.length || 0} 项`"
:icon="toolIconFor(c.key)"
:badge="hotBadge(c)"
/>
</div>
</section>
</PageShell>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { api } from '../api/client'
import BackButton from '../components/BackButton.vue'
import HomeToolIcon, { type HomeToolIconName } from '../components/HomeToolIcon.vue'
import ListRow from '../components/ListRow.vue'
import PageShell from '../components/PageShell.vue'
import { toolIconFor } from '../lib/toolIconMap'
type Cat = {
key: string
title: string
description: string
icon: string
tone: string
items?: { badge?: string }[]
}
const featured: { to: string; label: string; icon: HomeToolIconName; badge?: string; badgeTone?: 'hot' | 'new' }[] = [
{ to: '/scales/mbti-lite', label: '人格测试', icon: 'mbti' },
{ to: '/star', label: '星座', icon: 'star' },
{ to: '/portrait', label: '愈心解码', icon: 'portrait', badge: '热', badgeTone: 'hot' },
{ to: '/rhythm', label: '身心节律', icon: 'rhythm' },
{ to: '/synastry', label: '合盘', icon: 'synastry', badge: '新', badgeTone: 'new' },
{ to: '/ask', label: 'AI问答', icon: 'ask' },
{ to: '/companion', label: '节气陪伴', icon: 'companion' },
{ to: '/cards', label: '意象卡片', icon: 'cards' },
{ to: '/reports', label: '成长报告', icon: 'reports', badge: '新', badgeTone: 'new' },
{ to: '/growth-plan', label: '成长计划', icon: 'growth' },
{ to: '/relation', label: '人格匹配', icon: 'relation' },
{ to: '/membership', label: '成长会员', icon: 'growth' },
]
const categories = ref<Cat[]>([])
const loading = ref(true)
const error = ref('')
function hotBadge(c: Cat) {
const b = c.items?.find((i) => i.badge)?.badge
return b || undefined
}
async function load() {
loading.value = true
error.value = ''
try {
const res = await api.getExploreCatalog()
categories.value = res.categories || []
} catch (e) {
error.value = e instanceof Error ? e.message : '加载失败'
} finally {
loading.value = false
}
}
onMounted(load)
</script>
<style scoped>
.head { padding: 4px 0 8px; }
.title {
font-family: var(--font-display);
font-size: 26px;
font-weight: 700;
color: var(--color-text-primary);
letter-spacing: 0.08em;
}
.sub {
margin-top: 4px;
font-size: 12px;
color: rgba(0, 0, 0, 0.38);
letter-spacing: 0.06em;
}
.sec { padding: 0 16px; margin-top: 18px; }
.sec:first-child { margin-top: 12px; }
.sec-head { margin-bottom: 12px; }
.sec-title {
font-size: 17px;
font-weight: 700;
color: var(--color-text-primary);
}
.grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 14px 6px;
}
.g-item {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
color: inherit;
transition: transform var(--duration-fast) ease;
}
.g-item:active { transform: scale(0.92); }
.g-label {
font-size: 11px;
font-weight: 500;
color: #555;
text-align: center;
line-height: 1.2;
}
.g-badge {
position: absolute;
top: -4px;
right: 4px;
font-size: 9px;
font-weight: 650;
color: #fff;
padding: 2px 5px;
border-radius: 7px;
line-height: 1.3;
}
.g-badge.b-hot {
background: linear-gradient(135deg, #ff7a6e, var(--color-primary));
}
.g-badge.b-new {
background: linear-gradient(135deg, #6eb6ff, #4a90e2);
}
.cat-list {
background: #fafafa;
border-radius: var(--radius-lg);
padding: 4px 10px;
}
.cat-list :deep(.lr) {
border-bottom: 1px solid #f0ebe8;
}
.cat-list :deep(.lr:last-child) {
border-bottom: none;
}
.hint, .err {
font-size: 13px;
color: var(--color-text-secondary);
padding: 8px 0;
}
.err { color: var(--color-primary); }
.retry {
margin-left: 8px;
border: none;
background: transparent;
color: var(--color-accent-blue);
font-size: 13px;
text-decoration: underline;
}
</style>