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.
16 KiB
16 KiB
05-体质辨识页面(原型)
目标
实现 APP 端中医体质辨识问卷和结果展示,使用本地模拟数据计算结果。
UI 设计参考
参考设计稿:
files/ui/体质页.png、files/ui/体质检测.png、files/ui/体质分析.png
页面组成
- 体质首页 - 介绍页面,引导用户开始测试
- 问卷页面 - 60道题目,逐题作答
- 结果页面 - 显示体质类型、雷达图、调养建议
前置要求
- 导航配置完成
- 模拟数据服务已创建(
src/mock/constitution.ts)
实施步骤
步骤 1:体质首页
创建 src/screens/constitution/ConstitutionHomeScreen.tsx:
import React from 'react'
import { View, ScrollView, StyleSheet } from 'react-native'
import { Text, Card, Button } from 'react-native-paper'
import Icon from 'react-native-vector-icons/MaterialCommunityIcons'
import { useNavigation } from '@react-navigation/native'
import { useConstitutionStore } from '../../stores/useConstitutionStore'
import { constitutionNames, constitutionDescriptions } from '../../mock/constitution'
const ConstitutionHomeScreen = () => {
const navigation = useNavigation<any>()
const { result } = useConstitutionStore()
const steps = [
{ icon: 'clipboard-text', title: '回答问卷', desc: '60道题目,约10分钟' },
{ icon: 'calculator', title: '智能分析', desc: '根据答案计算体质' },
{ icon: 'file-document', title: '获取报告', desc: '体质类型和调养建议' },
]
return (
<ScrollView style={styles.container}>
{/* 已有结果时显示 */}
{result && (
<Card style={styles.resultCard}>
<Card.Content>
<View style={styles.resultHeader}>
<Icon name="check-circle" size={24} color="#10B981" />
<Text style={styles.resultTitle}>您已完成体质测评</Text>
</View>
<View style={styles.resultBody}>
<Text style={styles.resultType}>
{constitutionNames[result.primaryType]}
</Text>
<Text style={styles.resultDesc}>
{constitutionDescriptions[result.primaryType].description}
</Text>
</View>
<Button
mode="contained"
onPress={() => navigation.navigate('ConstitutionResult')}
buttonColor="#10B981"
style={styles.resultButton}
>
查看详细报告
</Button>
</Card.Content>
</Card>
)}
{/* 介绍卡片 */}
<Card style={styles.introCard}>
<Card.Content>
<Text style={styles.introTitle}>中医体质自测</Text>
<Text style={styles.introDesc}>
中医体质辨识是以中医理论为指导,根据人体生理特点分为9种基本体质类型。
了解自己的体质类型,有助于选择适合的养生方法。
</Text>
</Card.Content>
</Card>
{/* 步骤说明 */}
<View style={styles.steps}>
{steps.map((step, index) => (
<View key={index} style={styles.stepItem}>
<View style={styles.stepIcon}>
<Icon name={step.icon} size={24} color="#10B981" />
</View>
<View style={styles.stepContent}>
<Text style={styles.stepTitle}>{step.title}</Text>
<Text style={styles.stepDesc}>{step.desc}</Text>
</View>
{index < steps.length - 1 && <View style={styles.stepLine} />}
</View>
))}
</View>
{/* 开始按钮 */}
<Button
mode="contained"
onPress={() => navigation.navigate('ConstitutionQuestions')}
buttonColor="#10B981"
style={styles.startButton}
contentStyle={styles.startButtonContent}
>
{result ? '重新测评' : '开始测评'}
</Button>
<Text style={styles.note}>
建议每3-6个月重新测评一次,以跟踪体质变化
</Text>
</ScrollView>
)
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#F3F4F6', padding: 16 },
resultCard: { marginBottom: 16, borderRadius: 12, borderLeftWidth: 4, borderLeftColor: '#10B981' },
resultHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 12 },
resultTitle: { fontSize: 16, fontWeight: '600', marginLeft: 8, color: '#10B981' },
resultBody: { marginBottom: 12 },
resultType: { fontSize: 24, fontWeight: 'bold', color: '#1F2937' },
resultDesc: { fontSize: 14, color: '#6B7280', marginTop: 4 },
resultButton: { borderRadius: 8 },
introCard: { marginBottom: 16, borderRadius: 12 },
introTitle: { fontSize: 20, fontWeight: 'bold', color: '#1F2937', marginBottom: 8 },
introDesc: { fontSize: 14, color: '#6B7280', lineHeight: 22 },
steps: { backgroundColor: '#fff', borderRadius: 12, padding: 16, marginBottom: 16 },
stepItem: { flexDirection: 'row', alignItems: 'flex-start', marginBottom: 16 },
stepIcon: { width: 48, height: 48, borderRadius: 24, backgroundColor: '#ECFDF5', justifyContent: 'center', alignItems: 'center' },
stepContent: { flex: 1, marginLeft: 12 },
stepTitle: { fontSize: 16, fontWeight: '600', color: '#1F2937' },
stepDesc: { fontSize: 13, color: '#6B7280', marginTop: 2 },
stepLine: { position: 'absolute', left: 24, top: 48, width: 1, height: 16, backgroundColor: '#E5E7EB' },
startButton: { borderRadius: 24, marginBottom: 12 },
startButtonContent: { paddingVertical: 8 },
note: { fontSize: 12, color: '#9CA3AF', textAlign: 'center' },
})
export default ConstitutionHomeScreen
步骤 2:问卷页面
创建 src/screens/constitution/ConstitutionQuestionsScreen.tsx:
import React, { useState } from 'react'
import { View, ScrollView, StyleSheet, Alert } from 'react-native'
import { Text, Button, ProgressBar, Card } from 'react-native-paper'
import { useNavigation } from '@react-navigation/native'
import { useConstitutionStore } from '../../stores/useConstitutionStore'
import { constitutionQuestions, calculateConstitution } from '../../mock/constitution'
const ConstitutionQuestionsScreen = () => {
const navigation = useNavigation<any>()
const { setResult } = useConstitutionStore()
const [currentIndex, setCurrentIndex] = useState(0)
const [answers, setAnswers] = useState<Record<number, number>>({})
const questions = constitutionQuestions
const currentQuestion = questions[currentIndex]
const progress = (currentIndex + 1) / questions.length
const isLastQuestion = currentIndex === questions.length - 1
const selectOption = (value: number) => {
setAnswers({ ...answers, [currentQuestion.id]: value })
}
const handleNext = () => {
if (!answers[currentQuestion.id]) {
Alert.alert('提示', '请选择一个选项')
return
}
if (currentIndex < questions.length - 1) {
setCurrentIndex(currentIndex + 1)
}
}
const handlePrev = () => {
if (currentIndex > 0) {
setCurrentIndex(currentIndex - 1)
}
}
const handleSubmit = () => {
if (Object.keys(answers).length < questions.length) {
Alert.alert('提示', '请完成所有题目')
return
}
// 本地计算结果
const result = calculateConstitution(answers)
setResult(result)
navigation.navigate('ConstitutionResult')
}
return (
<View style={styles.container}>
{/* 进度条 */}
<View style={styles.progressContainer}>
<Text style={styles.progressText}>
第 {currentIndex + 1} 题 / 共 {questions.length} 题
</Text>
<ProgressBar progress={progress} color="#10B981" style={styles.progressBar} />
</View>
{/* 问题卡片 */}
<ScrollView style={styles.content}>
<Card style={styles.questionCard}>
<Card.Content>
<Text style={styles.questionText}>{currentQuestion.question}</Text>
<View style={styles.options}>
{currentQuestion.options.map((option) => (
<Button
key={option.value}
mode={answers[currentQuestion.id] === option.value ? 'contained' : 'outlined'}
onPress={() => selectOption(option.value)}
style={styles.optionButton}
buttonColor={answers[currentQuestion.id] === option.value ? '#10B981' : undefined}
>
{option.label}
</Button>
))}
</View>
</Card.Content>
</Card>
</ScrollView>
{/* 导航按钮 */}
<View style={styles.navButtons}>
<Button
mode="outlined"
onPress={handlePrev}
disabled={currentIndex === 0}
style={styles.navButton}
>
上一题
</Button>
{isLastQuestion ? (
<Button
mode="contained"
onPress={handleSubmit}
buttonColor="#10B981"
style={styles.navButton}
>
提交
</Button>
) : (
<Button
mode="contained"
onPress={handleNext}
buttonColor="#10B981"
style={styles.navButton}
>
下一题
</Button>
)}
</View>
</View>
)
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#F3F4F6' },
progressContainer: { padding: 16, backgroundColor: '#fff' },
progressText: { textAlign: 'center', marginBottom: 8, color: '#6B7280' },
progressBar: { height: 6, borderRadius: 3 },
content: { flex: 1, padding: 16 },
questionCard: { borderRadius: 12 },
questionText: { fontSize: 18, lineHeight: 28, color: '#1F2937', marginBottom: 20 },
options: { gap: 12 },
optionButton: { marginBottom: 8, borderRadius: 8 },
navButtons: { flexDirection: 'row', justifyContent: 'space-between', padding: 16, backgroundColor: '#fff', borderTopWidth: 1, borderTopColor: '#E5E7EB' },
navButton: { flex: 1, marginHorizontal: 8, borderRadius: 8 },
})
export default ConstitutionQuestionsScreen
步骤 3:结果页面
创建 src/screens/constitution/ConstitutionResultScreen.tsx:
import React from 'react'
import { View, ScrollView, StyleSheet } from 'react-native'
import { Text, Card, Chip, Button } from 'react-native-paper'
import Icon from 'react-native-vector-icons/MaterialCommunityIcons'
import { useNavigation } from '@react-navigation/native'
import { useConstitutionStore } from '../../stores/useConstitutionStore'
import { constitutionNames, constitutionDescriptions } from '../../mock/constitution'
import { getProductsByConstitution } from '../../mock/products'
const ConstitutionResultScreen = () => {
const navigation = useNavigation<any>()
const { result } = useConstitutionStore()
if (!result) {
return (
<View style={styles.emptyContainer}>
<Text>暂无测评结果</Text>
<Button mode="contained" onPress={() => navigation.navigate('ConstitutionQuestions')}>
开始测评
</Button>
</View>
)
}
const info = constitutionDescriptions[result.primaryType]
const products = getProductsByConstitution(result.primaryType)
// 计算所有体质得分用于显示
const allScores = Object.entries(result.scores)
.map(([type, score]) => ({
type,
name: constitutionNames[type as keyof typeof constitutionNames],
score,
}))
.sort((a, b) => b.score - a.score)
return (
<ScrollView style={styles.container}>
{/* 主体质卡片 */}
<Card style={styles.primaryCard}>
<Card.Content style={styles.primaryContent}>
<Icon name="heart-pulse" size={48} color="#10B981" />
<Text style={styles.primaryType}>{constitutionNames[result.primaryType]}</Text>
<Text style={styles.primaryScore}>{result.scores[result.primaryType]}分</Text>
<Text style={styles.primaryDesc}>{info.description}</Text>
</Card.Content>
</Card>
{/* 体质得分 */}
<Card style={styles.card}>
<Card.Title title="体质得分分布" />
<Card.Content>
{allScores.map((item) => (
<View key={item.type} style={styles.scoreItem}>
<Text style={styles.scoreName}>{item.name}</Text>
<View style={styles.scoreBar}>
<View style={[styles.scoreBarFill, { width: `${item.score}%` }]} />
</View>
<Text style={styles.scoreValue}>{item.score}</Text>
</View>
))}
</Card.Content>
</Card>
{/* 体质特征 */}
<Card style={styles.card}>
<Card.Title title="体质特征" />
<Card.Content>
<View style={styles.tagList}>
{info.features.map((feature, index) => (
<Chip key={index} style={styles.tag}>{feature}</Chip>
))}
</View>
</Card.Content>
</Card>
{/* 调养建议 */}
<Card style={styles.card}>
<Card.Title title="调养建议" />
<Card.Content>
{info.suggestions.map((suggestion, index) => (
<View key={index} style={styles.suggestionItem}>
<Icon name="check-circle" size={20} color="#10B981" />
<Text style={styles.suggestionText}>{suggestion}</Text>
</View>
))}
</Card.Content>
</Card>
{/* 推荐产品 */}
<Card style={styles.card}>
<Card.Title title="推荐调养产品" />
<Card.Content>
{products.map((product) => (
<View key={product.id} style={styles.productItem}>
<Text style={styles.productName}>{product.name}</Text>
<Text style={styles.productPrice}>¥{product.price}</Text>
</View>
))}
</Card.Content>
</Card>
{/* 操作按钮 */}
<View style={styles.actions}>
<Button
mode="contained"
onPress={() => navigation.navigate('ChatTab')}
buttonColor="#10B981"
style={styles.actionButton}
>
咨询AI助手
</Button>
</View>
</ScrollView>
)
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#F3F4F6', padding: 16 },
emptyContainer: { flex: 1, justifyContent: 'center', alignItems: 'center' },
primaryCard: { borderRadius: 16, marginBottom: 16 },
primaryContent: { alignItems: 'center', paddingVertical: 24 },
primaryType: { fontSize: 28, fontWeight: 'bold', color: '#1F2937', marginTop: 12 },
primaryScore: { fontSize: 18, color: '#10B981', marginTop: 4 },
primaryDesc: { fontSize: 14, color: '#6B7280', marginTop: 12, textAlign: 'center', lineHeight: 22 },
card: { borderRadius: 12, marginBottom: 16 },
scoreItem: { flexDirection: 'row', alignItems: 'center', marginBottom: 12 },
scoreName: { width: 60, fontSize: 13 },
scoreBar: { flex: 1, height: 8, backgroundColor: '#E5E7EB', borderRadius: 4, marginHorizontal: 8 },
scoreBarFill: { height: '100%', backgroundColor: '#10B981', borderRadius: 4 },
scoreValue: { width: 30, textAlign: 'right', fontSize: 13 },
tagList: { flexDirection: 'row', flexWrap: 'wrap', gap: 8 },
tag: { backgroundColor: '#ECFDF5' },
suggestionItem: { flexDirection: 'row', alignItems: 'flex-start', marginBottom: 12 },
suggestionText: { flex: 1, marginLeft: 8, fontSize: 14, color: '#4B5563', lineHeight: 20 },
productItem: { flexDirection: 'row', justifyContent: 'space-between', paddingVertical: 12, borderBottomWidth: 1, borderBottomColor: '#E5E7EB' },
productName: { fontSize: 14, color: '#1F2937' },
productPrice: { fontSize: 14, color: '#EF4444', fontWeight: '600' },
actions: { padding: 16 },
actionButton: { borderRadius: 24 },
})
export default ConstitutionResultScreen
验收标准
- 体质首页正常显示
- 问卷60题可完整答题
- 进度条显示正确
- 提交后本地计算结果
- 结果页显示体质类型和建议
- 体质得分分布正确
预计耗时
40-50 分钟
下一步
完成后进入 02-APP原型开发/06-AI对话页面.md