refactor(ECR-001): Phase F types/sdk 对齐并拆分超标 H5 页

抽出 OpenAPI DTO;将 Ask/Report/Profile/Star 等 8 页拆到 ≤400 行,并修 ScaleSummary、fortuneKey、Scale 选答与单测。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jackyu66git
2026-08-05 18:19:34 +08:00
co-authored by Cursor
parent 19d3cd5945
commit e735aa43e3
64 changed files with 6198 additions and 4157 deletions
@@ -0,0 +1,138 @@
<template>
<div class="answer-screen">
<p v-if="draftRestored && currentIdx === 0" class="yxg-hint draft">已恢复上次未提交的作答</p>
<div class="progress-wrap">
<div class="progress-bar">
<i class="progress-fill" :style="{ width: progressPct + '%' }" />
</div>
<span class="progress-label">{{ currentIdx + 1 }} / {{ totalQuestions }}</span>
</div>
<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>
</div>
<p v-if="submitError" class="yxg-err">{{ submitError }}</p>
<div v-if="needProfile" class="need">
<p>提交结果需要先有的个人档案</p>
<router-link class="yxg-btn" to="/">去首页创建档案</router-link>
</div>
</div>
</template>
<script setup lang="ts">
const props = defineProps<{
draftRestored: boolean
currentIdx: number
progressPct: number
totalQuestions: number
currentQuestion: { id: string; body: { prompt: string; options: { key: string; text: string }[] } } | null
answers: Record<string, string>
submitError: string
needProfile: boolean
}>()
const emit = defineEmits<{
answer: [questionId: string, key: string]
}>()
function onPick(key: string) {
if (!props.currentQuestion) return
emit('answer', props.currentQuestion.id, key)
}
</script>
<style scoped>
.answer-screen {
padding-bottom: 8px;
}
.progress-wrap {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 16px;
}
.progress-bar {
flex: 1;
height: 6px;
border-radius: 999px;
background: #f0ebe7;
overflow: hidden;
}
.progress-fill {
display: block;
height: 100%;
border-radius: 999px;
background: linear-gradient(90deg, #ff7a6e, var(--color-primary));
transition: width 0.3s ease;
}
.progress-label {
font-size: 12px;
font-weight: 600;
color: var(--color-text-tertiary);
flex-shrink: 0;
}
.q-card {
margin: 0 0 12px;
}
.prompt {
font-weight: 650;
font-size: 17px;
color: #222;
margin-bottom: 16px;
line-height: 1.5;
}
.opt {
display: flex;
align-items: flex-start;
gap: 10px;
margin: 10px 0;
padding: 14px 12px;
border-radius: 14px;
border: 1.5px solid #eee;
background: #fdfaf8;
cursor: pointer;
line-height: 1.45;
transition: border-color 0.15s ease, background 0.15s ease;
}
.opt.picked {
border-color: var(--color-primary);
background: #fff5f3;
}
.opt input {
margin-top: 3px;
accent-color: var(--color-primary);
}
.opt-text {
flex: 1;
font-size: 14px;
}
.need {
margin-top: 10px;
padding: 12px;
background: #fff8f7;
border-radius: 12px;
}
.need .yxg-btn {
margin-top: 10px;
}
.draft {
color: var(--yxg-gold);
margin-bottom: 8px;
}
</style>
@@ -0,0 +1,76 @@
<template>
<div class="intro-screen">
<div class="intro-card">
<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="draftRestored" class="draft-hint">检测到上次未完成的作答开始后将继续</p>
<button class="yxg-btn yxg-btn-block intro-start" type="button" :disabled="!questionCount" @click="$emit('start')">
开始
</button>
</div>
</div>
</template>
<script setup lang="ts">
import HomeToolIcon from '../HomeToolIcon.vue'
defineProps<{
title: string
description: string
questionCount: number
estMinutes: number
draftRestored: boolean
}>()
defineEmits<{ start: [] }>()
</script>
<style scoped>
.intro-screen {
display: flex;
justify-content: center;
padding: 24px 0 32px;
}
.intro-card {
width: 100%;
max-width: 340px;
text-align: center;
padding: 32px 24px;
background: var(--color-surface);
border-radius: var(--radius-xl);
box-shadow: var(--shadow-hero);
}
.intro-title {
font-family: var(--font-display);
font-size: 22px;
font-weight: 700;
color: var(--color-text-primary);
margin: 16px 0 8px;
letter-spacing: 0.04em;
}
.intro-desc {
font-size: 14px;
color: var(--color-text-secondary);
line-height: 1.55;
margin: 0 0 12px;
}
.intro-count {
font-size: 12px;
color: var(--color-text-tertiary);
margin: 0 0 20px;
}
.draft-hint {
font-size: 12px;
color: var(--color-accent-gold);
margin: 0 0 12px;
}
.intro-start {
font-size: 16px;
font-weight: 700;
padding: 14px;
background: linear-gradient(135deg, #ff7a6e, var(--color-primary));
box-shadow: 0 6px 18px rgba(229, 77, 66, 0.28);
}
</style>
@@ -0,0 +1,68 @@
<template>
<div class="nav-bar">
<button type="button" class="nav-btn prev" :disabled="currentIdx === 0" @click="$emit('prev')">上一题</button>
<button
type="button"
class="nav-btn next"
:disabled="!hasAnswer || submitting"
@click="$emit('next')"
>
{{ isLast ? (submitting ? '提交中…' : '查看探索结果') : '下一题' }}
</button>
</div>
</template>
<script setup lang="ts">
defineProps<{
currentIdx: number
hasAnswer: boolean
isLast: boolean
submitting: boolean
}>()
defineEmits<{ prev: []; next: [] }>()
</script>
<style scoped>
.nav-bar {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 20;
display: flex;
gap: 10px;
padding: 10px 16px 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);
}
.nav-btn {
flex: 1;
padding: 13px;
border-radius: 22px;
font-size: 15px;
font-weight: 600;
cursor: pointer;
border: none;
}
.nav-btn.prev {
background: #f3eeea;
color: #555;
}
.nav-btn.prev:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.nav-btn.next {
background: linear-gradient(135deg, #ff7a6e, var(--color-primary));
color: #fff;
box-shadow: 0 4px 14px rgba(229, 77, 66, 0.25);
}
.nav-btn.next:disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>
@@ -0,0 +1,44 @@
<template>
<div>
<p class="result-lead">{{ resultLabel }}</p>
<ReportRich :summary="resultSummaryObj" :detail="resultDetailObj" :deep="true" />
<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>
</div>
</div>
</template>
<script setup lang="ts">
import ReportRich from '../ReportRich.vue'
defineProps<{
resultLabel: string
shareLine: string
resultSummaryObj: Record<string, unknown>
resultDetailObj: Record<string, unknown>
}>()
defineEmits<{ share: [] }>()
</script>
<style scoped>
.result-lead {
font-family: var(--font-display);
font-size: 20px;
font-weight: 700;
color: #222;
margin: 4px 0 12px;
letter-spacing: 0.04em;
}
.share {
margin: 0 0 10px;
color: var(--yxg-gold);
}
.link {
display: block;
margin-top: 12px;
}
</style>