You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
14 KiB
14 KiB
05-体质辨识页面(原型)
目标
实现 Web 端中医体质辨识问卷和结果展示,使用本地模拟数据计算结果。
页面组成
- 体质首页 - 介绍页面,引导用户开始测试
- 问卷页面 - 60道题目,逐题作答
- 结果页面 - 显示体质类型、雷达图、调养建议
前置要求
- 路由配置完成
- 模拟数据服务已创建
实施步骤
步骤 1:体质首页
创建 src/views/constitution/ConstitutionView.vue:
<template>
<div class="constitution-page">
<!-- 已有结果提示 -->
<el-alert
v-if="constitutionStore.result"
type="success"
:closable="false"
class="result-alert"
>
<template #title>
<div class="alert-content">
<span>您已完成体质测评,当前体质:<strong>{{ constitutionNames[constitutionStore.result.primaryType] }}</strong></span>
<el-button type="primary" link @click="router.push('/constitution/result')">
查看详细报告 →
</el-button>
</div>
</template>
</el-alert>
<!-- 介绍卡片 -->
<el-card class="intro-card">
<div class="intro-header">
<h2>中医体质自测</h2>
<p>通过科学的问卷调查,分析您的体质类型,为您提供个性化的健康建议。</p>
</div>
</el-card>
<!-- 步骤说明 -->
<el-card class="steps-card">
<h3>测试说明</h3>
<el-steps :active="0" direction="vertical">
<el-step title="回答65个问题" description="根据您的真实情况选择最符合的答案" />
<el-step title="获取分析报告" description="系统将为您分析体质类型并提供建议" />
<el-step title="个性化建议" description="根据结果提供针对性的健康建议" />
</el-steps>
</el-card>
<!-- 开始按钮 -->
<el-button
type="primary"
size="large"
class="start-btn"
@click="router.push('/constitution/test')"
>
{{ constitutionStore.result ? '重新测评' : '开始测试' }}
</el-button>
<p class="note">建议每3-6个月重新测评一次,以跟踪体质变化</p>
</div>
</template>
<script setup lang="ts">
import { useRouter } from 'vue-router'
import { useConstitutionStore } from '@/stores/constitution'
import { constitutionNames } from '@/mock/constitution'
const router = useRouter()
const constitutionStore = useConstitutionStore()
</script>
<style scoped lang="scss">
.constitution-page {
max-width: 600px;
margin: 0 auto;
}
.result-alert {
margin-bottom: 20px;
.alert-content {
display: flex;
justify-content: space-between;
align-items: center;
}
}
.intro-card {
margin-bottom: 20px;
background: linear-gradient(135deg, #10B981 0%, #2EC4B6 100%);
color: #fff;
.intro-header {
text-align: center;
padding: 20px;
h2 {
font-size: 24px;
margin-bottom: 8px;
}
p {
opacity: 0.9;
}
}
}
.steps-card {
margin-bottom: 20px;
h3 {
margin-bottom: 20px;
}
}
.start-btn {
width: 100%;
height: 50px;
border-radius: 25px;
font-size: 16px;
}
.note {
text-align: center;
color: #9CA3AF;
font-size: 13px;
margin-top: 16px;
}
</style>
步骤 2:问卷页面
创建 src/views/constitution/ConstitutionTestView.vue:
<template>
<div class="test-page">
<!-- 进度条 -->
<el-card class="progress-card">
<div class="progress-header">
<span>第 {{ currentIndex + 1 }} 题 / 共 {{ questions.length }} 题</span>
</div>
<el-progress
:percentage="progress"
:stroke-width="8"
:show-text="false"
color="#10B981"
/>
</el-card>
<!-- 问题卡片 -->
<el-card class="question-card">
<div class="question-label">
<el-tag type="success">问题{{ currentIndex + 1 }}</el-tag>
</div>
<h3 class="question-text">{{ currentQuestion.question }}</h3>
<div class="options">
<div
v-for="option in currentQuestion.options"
:key="option.value"
class="option-item"
:class="{ active: answers[currentQuestion.id] === option.value }"
@click="selectOption(option.value)"
>
{{ option.label }}
</div>
</div>
</el-card>
<!-- 提示 -->
<el-alert type="info" :closable="false" class="tip-alert">
请根据您最近三个月的实际感受如实回答,系统将根据您的回答生成专属的中医体质报告。
</el-alert>
<!-- 导航按钮 -->
<div class="nav-buttons">
<el-button size="large" :disabled="currentIndex === 0" @click="handlePrev">
上一题
</el-button>
<el-button
v-if="!isLastQuestion"
type="primary"
size="large"
:disabled="!answers[currentQuestion.id]"
@click="handleNext"
>
下一题
</el-button>
<el-button
v-else
type="primary"
size="large"
:loading="submitting"
@click="handleSubmit"
>
提交
</el-button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { useConstitutionStore } from '@/stores/constitution'
import { constitutionQuestions, calculateConstitution } from '@/mock/constitution'
const router = useRouter()
const constitutionStore = useConstitutionStore()
const questions = constitutionQuestions
const currentIndex = ref(0)
const answers = ref<Record<number, number>>({})
const submitting = ref(false)
const currentQuestion = computed(() => questions[currentIndex.value])
const progress = computed(() => ((currentIndex.value + 1) / questions.length) * 100)
const isLastQuestion = computed(() => currentIndex.value === questions.length - 1)
const selectOption = (value: number) => {
answers.value[currentQuestion.value.id] = value
}
const handlePrev = () => {
if (currentIndex.value > 0) {
currentIndex.value--
}
}
const handleNext = () => {
if (!answers.value[currentQuestion.value.id]) {
ElMessage.warning('请选择一个选项')
return
}
if (currentIndex.value < questions.length - 1) {
currentIndex.value++
}
}
const handleSubmit = () => {
if (Object.keys(answers.value).length < questions.length) {
ElMessage.warning('请完成所有题目')
return
}
submitting.value = true
// 模拟提交延迟
setTimeout(() => {
const result = calculateConstitution(answers.value)
constitutionStore.setResult(result)
submitting.value = false
router.push('/constitution/result')
}, 1000)
}
</script>
<style scoped lang="scss">
.test-page {
max-width: 600px;
margin: 0 auto;
}
.progress-card {
margin-bottom: 20px;
.progress-header {
text-align: center;
margin-bottom: 12px;
color: #6B7280;
}
}
.question-card {
margin-bottom: 20px;
.question-label {
margin-bottom: 16px;
}
.question-text {
font-size: 18px;
line-height: 1.6;
margin-bottom: 24px;
}
.options {
display: flex;
flex-direction: column;
gap: 12px;
.option-item {
padding: 16px 20px;
border: 1px solid #E5E7EB;
border-radius: 12px;
cursor: pointer;
transition: all 0.2s;
&:hover {
border-color: #10B981;
background: #ECFDF5;
}
&.active {
border-color: #10B981;
background: #10B981;
color: #fff;
}
}
}
}
.tip-alert {
margin-bottom: 20px;
}
.nav-buttons {
display: flex;
gap: 16px;
.el-button {
flex: 1;
height: 48px;
}
}
</style>
步骤 3:结果页面
创建 src/views/constitution/ConstitutionResultView.vue:
<template>
<div class="result-page" v-if="constitutionStore.result">
<!-- 主体质卡片 -->
<el-card class="primary-card">
<div class="primary-content">
<h2>体质分析报告</h2>
<p class="sub-title">您的主体质倾向</p>
<div class="primary-type">
<span class="type-name">{{ constitutionNames[constitutionStore.result.primaryType] }}</span>
<el-tag type="success" size="large">{{ constitutionStore.result.scores[constitutionStore.result.primaryType] }}分</el-tag>
</div>
<p class="status-text">体质状态良好,请继续保持</p>
</div>
</el-card>
<!-- 体质雷达图 -->
<el-card class="chart-card">
<template #header>
<div class="card-header">
<el-icon><TrendCharts /></el-icon>
<span>体质雷达图</span>
</div>
</template>
<div class="chart-container" ref="chartRef"></div>
</el-card>
<!-- 体质特征 -->
<el-card class="features-card">
<template #header>
<div class="card-header">
<el-icon><Document /></el-icon>
<span>体质特征</span>
</div>
</template>
<p class="features-text">{{ info.description }}</p>
<div class="features-tags">
<el-tag v-for="(feature, index) in info.features" :key="index" type="info">
{{ feature }}
</el-tag>
</div>
</el-card>
<!-- 调理建议 -->
<el-card class="suggestions-card">
<template #header>调理建议</template>
<el-row :gutter="16">
<el-col :span="12" v-for="(suggestion, index) in info.suggestions" :key="index">
<div class="suggestion-item">
<el-icon size="20" :color="suggestionIcons[index % 4].color">
<component :is="suggestionIcons[index % 4].icon" />
</el-icon>
<p>{{ suggestion }}</p>
</div>
</el-col>
</el-row>
</el-card>
<!-- 操作按钮 -->
<div class="actions">
<el-button type="primary" size="large" @click="router.push('/chat')">
咨询AI助手
</el-button>
<el-button size="large" @click="router.push('/constitution/test')">
重新测评
</el-button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue'
import { useRouter } from 'vue-router'
import * as echarts from 'echarts'
import { TrendCharts, Document, Sunny, Bowl, Football, Heart } from '@element-plus/icons-vue'
import { useConstitutionStore } from '@/stores/constitution'
import { constitutionNames, constitutionDescriptions } from '@/mock/constitution'
const router = useRouter()
const constitutionStore = useConstitutionStore()
const chartRef = ref<HTMLElement>()
const info = computed(() => {
if (!constitutionStore.result) return { description: '', features: [], suggestions: [] }
return constitutionDescriptions[constitutionStore.result.primaryType]
})
const suggestionIcons = [
{ icon: Sunny, color: '#8B5CF6' },
{ icon: Bowl, color: '#14B8A6' },
{ icon: Football, color: '#8B5CF6' },
{ icon: Heart, color: '#EC4899' }
]
onMounted(() => {
initChart()
})
watch(() => constitutionStore.result, () => {
initChart()
})
const initChart = () => {
if (!chartRef.value || !constitutionStore.result) return
const chart = echarts.init(chartRef.value)
const scores = constitutionStore.result.scores
const data = Object.entries(scores).map(([type, score]) => ({
name: constitutionNames[type as keyof typeof constitutionNames],
value: score
}))
chart.setOption({
radar: {
indicator: data.map(d => ({ name: d.name, max: 100 })),
shape: 'polygon',
splitNumber: 4,
axisName: {
color: '#6B7280'
}
},
series: [{
type: 'radar',
data: [{
value: data.map(d => d.value),
areaStyle: {
color: 'rgba(16, 185, 129, 0.2)'
},
lineStyle: {
color: '#10B981'
},
itemStyle: {
color: '#10B981'
}
}]
}]
})
}
</script>
<style scoped lang="scss">
.result-page {
max-width: 800px;
margin: 0 auto;
}
.primary-card {
margin-bottom: 20px;
background: linear-gradient(135deg, #10B981 0%, #2EC4B6 100%);
color: #fff;
.primary-content {
text-align: center;
padding: 20px;
h2 {
font-size: 20px;
margin-bottom: 8px;
}
.sub-title {
opacity: 0.8;
margin-bottom: 16px;
}
.primary-type {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
margin-bottom: 12px;
.type-name {
font-size: 36px;
font-weight: bold;
}
}
.status-text {
background: rgba(255, 255, 255, 0.2);
padding: 8px 16px;
border-radius: 20px;
display: inline-block;
}
}
}
.card-header {
display: flex;
align-items: center;
gap: 8px;
}
.chart-card {
margin-bottom: 20px;
.chart-container {
height: 300px;
}
}
.features-card {
margin-bottom: 20px;
.features-text {
margin-bottom: 16px;
line-height: 1.8;
color: #4B5563;
}
.features-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
}
.suggestions-card {
margin-bottom: 20px;
.suggestion-item {
background: #F9FAFB;
border-radius: 12px;
padding: 16px;
margin-bottom: 16px;
display: flex;
align-items: flex-start;
gap: 12px;
p {
flex: 1;
color: #4B5563;
line-height: 1.6;
}
}
}
.actions {
display: flex;
gap: 16px;
.el-button {
flex: 1;
height: 48px;
}
}
</style>
验收标准
- 体质首页正常显示
- 问卷60题可完整答题
- 进度条显示正确
- 提交后本地计算结果
- 雷达图显示正常
- 调理建议完整显示
预计耗时
45-55 分钟
下一步
完成后进入 03-Web原型开发/06-AI对话页面.md