feat(ECR-012–016): 合规、题库、时辰刷新、头像、MBTI OEJTS 与埋点

落地输入合规、探索题库、报告日/时辰刷新、账号头像、OEJTS 量表,并补齐 H5 埋点与 Admin 漏斗;同步 ESS 工件、切至自建 Git、清理 GitHub Actions。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jackyu66git
2026-08-13 01:27:58 +08:00
co-authored by Cursor
parent 13860bf1ef
commit 89756f65b4
227 changed files with 7405 additions and 1407 deletions
@@ -10,21 +10,45 @@
<div v-if="currentQuestion" class="q-card yxg-card">
<p class="prompt">{{ currentQuestion.body.prompt }}</p>
<label
v-for="opt in currentQuestion.body.options"
:key="opt.key"
class="opt"
:class="{ picked: answers[currentQuestion.id] === opt.key }"
>
<input
type="radio"
:name="`q-${currentQuestion.id}`"
:value="opt.key"
:checked="answers[currentQuestion.id] === opt.key"
@change="onPick(opt.key)"
/>
<span class="opt-text">{{ opt.text }}</span>
</label>
<template v-if="isLikert">
<div class="poles">
<span class="pole left">{{ currentQuestion.body.left }}</span>
<span class="pole right">{{ currentQuestion.body.right }}</span>
</div>
<div class="likert">
<button
v-for="opt in currentQuestion.body.options"
:key="opt.key"
type="button"
class="likert-btn"
:class="{ picked: answers[currentQuestion.id] === opt.key }"
:aria-label="opt.text"
@click="onPick(opt.key)"
>
{{ opt.key }}
</button>
</div>
<p class="likert-hint">1 更接近左侧 · 5 更接近右侧</p>
</template>
<template v-else>
<label
v-for="opt in currentQuestion.body.options"
:key="opt.key"
class="opt"
:class="{ picked: answers[currentQuestion.id] === opt.key }"
>
<input
type="radio"
:name="`q-${currentQuestion.id}`"
:value="opt.key"
:checked="answers[currentQuestion.id] === opt.key"
@change="onPick(opt.key)"
/>
<span class="opt-text">{{ opt.text }}</span>
</label>
</template>
</div>
<p v-if="submitError" class="yxg-err">{{ submitError }}</p>
@@ -36,12 +60,23 @@
</template>
<script setup lang="ts">
import { computed } from 'vue'
const props = defineProps<{
draftRestored: boolean
currentIdx: number
progressPct: number
totalQuestions: number
currentQuestion: { id: string; body: { prompt: string; options: { key: string; text: string }[] } } | null
currentQuestion: {
id: string
body: {
prompt: string
format?: string
left?: string
right?: string
options: { key: string; text: string }[]
}
} | null
answers: Record<string, string>
submitError: string
needProfile: boolean
@@ -51,6 +86,8 @@ const emit = defineEmits<{
answer: [questionId: string, key: string]
}>()
const isLikert = computed(() => props.currentQuestion?.body.format === 'likert5')
function onPick(key: string) {
if (!props.currentQuestion) return
emit('answer', props.currentQuestion.id, key)
@@ -97,6 +134,49 @@ function onPick(key: string) {
margin-bottom: 16px;
line-height: 1.5;
}
.poles {
display: flex;
justify-content: space-between;
gap: 12px;
margin-bottom: 14px;
}
.pole {
flex: 1;
font-size: 13px;
line-height: 1.4;
color: #5a4a44;
font-weight: 600;
}
.pole.right {
text-align: right;
}
.likert {
display: flex;
justify-content: space-between;
gap: 8px;
}
.likert-btn {
flex: 1;
height: 44px;
border-radius: 12px;
border: 1.5px solid #eee;
background: #fdfaf8;
font-size: 16px;
font-weight: 700;
color: #333;
cursor: pointer;
}
.likert-btn.picked {
border-color: var(--color-primary);
background: #fff5f3;
color: var(--color-primary);
}
.likert-hint {
margin: 10px 0 0;
font-size: 11px;
color: var(--color-text-tertiary);
text-align: center;
}
.opt {
display: flex;
align-items: flex-start;
@@ -4,9 +4,31 @@
<HomeToolIcon name="mbti" :size="64" />
<h2 class="intro-title">{{ title || '探索测试' }}</h2>
<p v-if="description" class="intro-desc">{{ description }}</p>
<p v-if="questionCount" class="intro-count"> {{ questionCount }} · {{ estMinutes }} 分钟</p>
<p v-if="questionCount && !showMbtiVersions" class="intro-count">
{{ questionCount }} · {{ estMinutes }} 分钟
</p>
<p v-if="draftRestored" class="draft-hint">检测到上次未完成的作答开始后将继续</p>
<button class="yxg-btn yxg-btn-block intro-start" type="button" :disabled="!questionCount" @click="$emit('start')">
<template v-if="showMbtiVersions">
<button class="yxg-btn yxg-btn-block intro-start" type="button" :disabled="!questionCount" @click="$emit('start')">
标准版 · 32 题免费
</button>
<button class="yxg-btn yxg-btn-ghost yxg-btn-block intro-full" type="button" @click="$emit('start-full')">
完整版 · 60 成长会员
</button>
<p class="credit">题库基于开源 OEJTS · 非官方 MBTI 版权量表</p>
</template>
<template v-else-if="locked">
<p class="lock-hint">完整版需开通成长会员后作答</p>
<router-link class="yxg-btn yxg-btn-block intro-start" to="/membership">去开通会员</router-link>
</template>
<button
v-else
class="yxg-btn yxg-btn-block intro-start"
type="button"
:disabled="!questionCount"
@click="$emit('start')"
>
开始
</button>
</div>
@@ -22,9 +44,11 @@ defineProps<{
questionCount: number
estMinutes: number
draftRestored: boolean
showMbtiVersions?: boolean
locked?: boolean
}>()
defineEmits<{ start: [] }>()
defineEmits<{ start: []; 'start-full': [] }>()
</script>
<style scoped>
@@ -61,7 +85,8 @@ defineEmits<{ start: [] }>()
color: var(--color-text-tertiary);
margin: 0 0 20px;
}
.draft-hint {
.draft-hint,
.lock-hint {
font-size: 12px;
color: var(--color-accent-gold);
margin: 0 0 12px;
@@ -73,4 +98,14 @@ defineEmits<{ start: [] }>()
background: linear-gradient(135deg, #ff7a6e, var(--color-primary));
box-shadow: 0 6px 18px rgba(229, 77, 66, 0.28);
}
.intro-full {
margin-top: 10px;
font-weight: 650;
}
.credit {
margin: 14px 0 0;
font-size: 11px;
color: var(--color-text-tertiary);
line-height: 1.4;
}
</style>
@@ -5,23 +5,38 @@
<div class="yxg-card">
<p v-if="shareLine" class="share">{{ shareLine }}</p>
<button type="button" class="yxg-btn yxg-btn-ghost" @click="$emit('share')">生成分享卡</button>
<router-link class="yxg-link-plain link" to="/relation">用结果去做关系理解 </router-link>
<router-link class="yxg-link-plain link" to="/ask">去问答聊聊 </router-link>
<button type="button" class="yxg-btn retake" @click="$emit('retake')">重新测试</button>
<router-link class="yxg-link-plain link" to="/relation" @click="onCtaRelation">
用结果去做关系理解
</router-link>
<router-link class="yxg-link-plain link" to="/ask" @click="onCtaAsk">
去问答聊聊
</router-link>
</div>
</div>
</template>
<script setup lang="ts">
import ReportRich from '../ReportRich.vue'
import { AnalyticsEvent, track } from '../../lib/analytics'
defineProps<{
const props = defineProps<{
slug: string
resultLabel: string
shareLine: string
resultSummaryObj: Record<string, unknown>
resultDetailObj: Record<string, unknown>
}>()
defineEmits<{ share: [] }>()
defineEmits<{ share: []; retake: [] }>()
function onCtaRelation() {
track(AnalyticsEvent.ScaleCtaRelation, { slug: props.slug })
}
function onCtaAsk() {
track(AnalyticsEvent.ScaleCtaAsk, { slug: props.slug })
}
</script>
<style scoped>
@@ -37,6 +52,11 @@ defineEmits<{ share: [] }>()
margin: 0 0 10px;
color: var(--yxg-gold);
}
.retake {
display: block;
width: 100%;
margin-top: 10px;
}
.link {
display: block;
margin-top: 12px;