轻松聘
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.
 
 
 

314 lines
9.2 KiB

import { Ionicons } from '@expo/vector-icons';
import { useRouter } from 'expo-router';
import { useState } from 'react';
import {
ActivityIndicator,
Pressable,
ScrollView,
StyleSheet,
Text,
View,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useHollandAssessment } from '../../src/hooks';
import { colors, hollandTypeColors } from '../../src/theme';
import type { HollandDimension, HollandOption } from '../../src/types';
/* ===================== 子组件:顶部导航 ===================== */
function Header({ progress, onBack }: { progress: string; onBack: () => void }) {
return (
<View style={styles.header}>
<Pressable style={styles.headerBtn} onPress={onBack} hitSlop={8}>
<Ionicons name="chevron-back" size={24} color={colors.textPrimary} />
</Pressable>
<View style={styles.headerCenter}>
<Text style={styles.headerTitle}>霍兰德测评</Text>
<Text style={styles.headerSub}>{progress}</Text>
</View>
<View style={styles.headerBtn} />
</View>
);
}
/* ===================== 子组件:六维分布条 ===================== */
function DimensionBars({ dimensions }: { dimensions: HollandDimension[] }) {
return (
<View style={styles.dimCard}>
<Text style={styles.dimTitle}>当前六维分布</Text>
<View style={styles.dimGrid}>
{dimensions.map((d) => (
<View key={d.type} style={styles.dimRow}>
<Text style={styles.dimType}>{d.type}</Text>
<Text style={styles.dimName}>{d.name}</Text>
<View style={styles.dimTrack}>
<View
style={[
styles.dimFill,
{ width: `${d.percent}%`, backgroundColor: hollandTypeColors[d.type] },
]}
/>
</View>
<Text style={styles.dimScore}>{d.score}</Text>
</View>
))}
</View>
</View>
);
}
/* ===================== 子组件:选项 ===================== */
function OptionButton({
option,
selected,
onPress,
}: {
option: HollandOption;
selected: boolean;
onPress: () => void;
}) {
return (
<Pressable style={[styles.option, selected && styles.optionActive]} onPress={onPress}>
<Text style={[styles.optionText, selected && styles.optionTextActive]}>{option.label}</Text>
{selected && <Ionicons name="checkmark-circle" size={20} color={colors.primary} />}
</Pressable>
);
}
/* ===================== 子组件:四态 ===================== */
function Loading() {
return (
<View style={styles.center}>
<ActivityIndicator size="large" color={colors.primary} />
<Text style={styles.tip}>正在加载测评…</Text>
</View>
);
}
function ErrorState({ onRetry }: { onRetry: () => void }) {
return (
<View style={styles.center}>
<Ionicons name="help-circle-outline" size={40} color={colors.textHint} />
<Text style={styles.tip}>测评加载失败</Text>
<Pressable style={styles.retryBtn} onPress={onRetry} hitSlop={6}>
<Text style={styles.retryText}>点击重试</Text>
</Pressable>
</View>
);
}
/* ===================== 页面 ===================== */
export default function HollandQuestionScreen() {
const router = useRouter();
const { assessment, loading, error, refresh, submit } = useHollandAssessment();
const [selectedValue, setSelectedValue] = useState<number | null>(null);
const [localIndex, setLocalIndex] = useState(0);
if (loading) {
return (
<SafeAreaView style={styles.container} edges={['top']}>
<Header progress="加载中…" onBack={() => router.back()} />
<Loading />
</SafeAreaView>
);
}
if (error || !assessment) {
return (
<SafeAreaView style={styles.container} edges={['top']}>
<Header progress="" onBack={() => router.back()} />
<ErrorState onRetry={refresh} />
</SafeAreaView>
);
}
const currentQuestion = Math.min(
assessment.currentQuestionIndex + 1 + localIndex,
assessment.totalQuestions,
);
const progressText = `第 ${currentQuestion}/${assessment.totalQuestions} 题`;
const handleNext = async () => {
if (selectedValue == null) return;
await submit(assessment.question.id, selectedValue);
setSelectedValue(null);
const nextIndex = localIndex + 1;
setLocalIndex(nextIndex);
if (nextIndex >= 3 || assessment.currentQuestionIndex + 1 >= assessment.totalQuestions) {
router.push('/holland-result');
}
};
return (
<SafeAreaView style={styles.container} edges={['top']}>
<Header progress={progressText} onBack={() => router.back()} />
<ScrollView contentContainerStyle={styles.body} showsVerticalScrollIndicator={false}>
<DimensionBars dimensions={assessment.dimensions} />
<View style={styles.questionCard}>
<Text style={styles.questionTitle}>请根据你的兴趣程度选择</Text>
<Text style={styles.questionText}>{assessment.question.text}</Text>
</View>
<View style={styles.options}>
{assessment.options.map((option) => (
<OptionButton
key={option.value}
option={option}
selected={selectedValue === option.value}
onPress={() => setSelectedValue(option.value)}
/>
))}
</View>
<View style={styles.footer}>
<Pressable style={[styles.navBtn, styles.navBtnPrev]} onPress={() => router.back()}>
<Text style={styles.navBtnPrevText}>上一题</Text>
</Pressable>
<Pressable
style={[
styles.navBtn,
styles.navBtnNext,
selectedValue == null && styles.navBtnNextDisabled,
]}
onPress={handleNext}
disabled={selectedValue == null}
>
<Text style={styles.navBtnNextText}>下一题</Text>
</Pressable>
</View>
</ScrollView>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: colors.pageBg },
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 12,
paddingVertical: 12,
backgroundColor: colors.white,
},
headerBtn: { width: 40, alignItems: 'center' },
headerCenter: { alignItems: 'center' },
headerTitle: { fontSize: 17, fontWeight: '700', color: colors.textPrimary },
headerSub: { fontSize: 12, color: colors.textHint, marginTop: 2 },
body: { padding: 8, paddingBottom: 32 },
dimCard: {
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
marginBottom: 8,
borderWidth: 1,
borderColor: colors.borderLight,
},
dimTitle: { fontSize: 15, fontWeight: '700', color: colors.textPrimary, marginBottom: 14 },
dimGrid: { flexDirection: 'row', flexWrap: 'wrap', gap: 10 },
dimRow: {
width: '47%',
flexDirection: 'row',
alignItems: 'center',
},
dimType: {
width: 18,
fontSize: 13,
fontWeight: '800',
color: colors.textPrimary,
},
dimName: {
width: 36,
fontSize: 12,
color: colors.textSecondary,
},
dimTrack: {
flex: 1,
height: 6,
borderRadius: 3,
backgroundColor: colors.borderLight,
overflow: 'hidden',
},
dimFill: { height: 6, borderRadius: 3 },
dimScore: {
width: 28,
fontSize: 12,
fontWeight: '700',
color: colors.textPrimary,
textAlign: 'right',
},
questionCard: {
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
marginBottom: 8,
borderWidth: 1,
borderColor: colors.borderLight,
alignItems: 'center',
},
questionTitle: { fontSize: 13, color: colors.textHint, marginBottom: 16 },
questionText: {
fontSize: 20,
fontWeight: '700',
color: colors.textPrimary,
textAlign: 'center',
lineHeight: 28,
},
options: { gap: 10, marginBottom: 20 },
option: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
backgroundColor: colors.white,
borderRadius: 8,
paddingVertical: 8,
paddingHorizontal: 8,
borderWidth: 1,
borderColor: colors.borderLight,
},
optionActive: {
backgroundColor: colors.primaryLight,
borderColor: colors.primary,
},
optionText: { fontSize: 15, color: colors.textPrimary },
optionTextActive: { fontWeight: '700', color: colors.primaryText },
footer: {
flexDirection: 'row',
gap: 12,
},
navBtn: {
flex: 1,
borderRadius: 12,
paddingVertical: 14,
alignItems: 'center',
},
navBtnPrev: {
backgroundColor: colors.white,
borderWidth: 1,
borderColor: colors.border,
},
navBtnPrevText: { fontSize: 15, fontWeight: '600', color: colors.textPrimary },
navBtnNext: { backgroundColor: colors.primaryBtn },
navBtnNextDisabled: { backgroundColor: colors.textDisabled },
navBtnNextText: { fontSize: 15, fontWeight: '700', color: colors.white },
center: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 40,
gap: 12,
},
tip: { fontSize: 14, color: colors.textSecondary },
retryBtn: {
paddingHorizontal: 18,
paddingVertical: 8,
borderRadius: 16,
backgroundColor: colors.primaryLight,
},
retryText: { fontSize: 13, fontWeight: '600', color: colors.primaryText },
});