import { Ionicons } from '@expo/vector-icons'; import { useLocalSearchParams, useRouter } from 'expo-router'; import { ActivityIndicator, Pressable, ScrollView, StyleSheet, Text, TextInput, View, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useMemo, useState } from 'react'; import { useGameData } from '../../src/hooks'; import { colors } from '../../src/theme'; import type { GameAnswer, GameQuestion } from '../../src/types'; /* ===================== 子组件:顶部导航 ===================== */ function Header({ title, onBack }: { title: string; onBack: () => void }) { return ( {title} ); } /* ===================== 子组件:题目卡片 ===================== */ function QuestionCard({ index, question, answer, onSelect, onInput, submitted, }: { index: number; question: GameQuestion; answer: GameAnswer | undefined; onSelect: (questionId: string, optionId: string, type: GameQuestion['type']) => void; onInput: (questionId: string, value: string) => void; submitted: boolean; }) { return ( 题目 {index + 1} {question.title} {question.type === 'input' ? ( onInput(question.id, text)} editable={!submitted} /> ) : ( {question.options?.map((option) => { const selected = answer?.selectedIds.includes(option.id) ?? false; const showCorrect = submitted && option.isCorrect; const showWrong = submitted && selected && !option.isCorrect; return ( onSelect(question.id, option.id, question.type)} disabled={submitted} > {option.text} {showCorrect && } {showWrong && } ); })} )} {submitted && ( 解析 {question.explanation} )} ); } /* ===================== 页面 ===================== */ export default function GameLevelScreen() { const router = useRouter(); const { id } = useLocalSearchParams<{ id: string }>(); const { levels, progress, loading, error, refresh, submitLevel, submitting } = useGameData(); const level = useMemo(() => levels.find((l) => l.id === id), [levels, id]); const result = useMemo( () => progress?.results.find((r) => r.levelId === id), [progress, id], ); const [answers, setAnswers] = useState([]); const [submitted, setSubmitted] = useState(false); // 已有历史结果时进入结果态 const hasResult = result?.completed === true; const showResult = submitted || hasResult; const handleSelect = ( questionId: string, optionId: string, type: GameQuestion['type'], ) => { setAnswers((prev) => { const next = prev.filter((a) => a.questionId !== questionId); const existing = prev.find((a) => a.questionId === questionId); let selectedIds: string[]; if (type === 'multiple') { const current = existing?.selectedIds ?? []; selectedIds = current.includes(optionId) ? current.filter((sid) => sid !== optionId) : [...current, optionId]; } else { selectedIds = [optionId]; } return [...next, { questionId, selectedIds, isCorrect: false }]; }); }; const handleInput = (questionId: string, value: string) => { setAnswers((prev) => { const next = prev.filter((a) => a.questionId !== questionId); return [...next, { questionId, selectedIds: [], inputValue: value, isCorrect: false }]; }); }; const handleSubmit = async () => { if (!level) return; const payload = level.questions.map((q) => { const answer = answers.find((a) => a.questionId === q.id); return { questionId: q.id, selectedIds: answer?.selectedIds ?? [], inputValue: answer?.inputValue, isCorrect: false, }; }); await submitLevel({ levelId: level.id, answers: payload }); setSubmitted(true); }; const handleNext = () => { if (!level?.nextLevel) { router.replace('/game'); return; } router.replace(`/game/level?id=${level.nextLevel}`); }; if (loading) { return (
router.back()} /> ); } if (error || !level) { return (
router.back()} /> {error ? '加载失败' : '关卡不存在'} 重试 ); } return (
router.back()} /> {level.description} {level.questions.map((q, idx) => ( a.questionId === q.id)} onSelect={handleSelect} onInput={handleInput} submitted={showResult} /> ))} {showResult && result && ( 本关得分:{result.score} 分 {result.completed ? '恭喜通关!' : '未达 60 分,可重新挑战'} )} {!showResult ? ( {submitting ? '提交中…' : '提交答案'} ) : ( {level.nextLevel ? '进入下一关' : '返回通关地图'} )} ); } const styles = StyleSheet.create({ safeArea: { flex: 1, backgroundColor: colors.pageBg, }, header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 8, height: 48, backgroundColor: colors.white, borderBottomWidth: 1, borderBottomColor: colors.borderLight, }, headerBtn: { width: 40, height: 40, alignItems: 'center', justifyContent: 'center', }, headerTitle: { fontSize: 17, fontWeight: '700', color: colors.textPrimary, }, scroll: { flex: 1, }, content: { padding: 8, paddingBottom: 24, }, levelDesc: { fontSize: 14, lineHeight: 20, color: colors.textSecondary, backgroundColor: colors.white, borderRadius: 8, padding: 8, marginBottom: 8, }, questionCard: { backgroundColor: colors.white, borderRadius: 8, padding: 8, marginBottom: 8, }, questionIndex: { fontSize: 12, fontWeight: '600', color: colors.primary, marginBottom: 6, }, questionTitle: { fontSize: 15, fontWeight: '700', color: colors.textPrimary, lineHeight: 22, marginBottom: 12, }, options: { gap: 8, }, option: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 8, paddingVertical: 8, borderRadius: 8, borderWidth: 1, borderColor: colors.border, backgroundColor: colors.white, }, optionSelected: { borderColor: colors.primary, backgroundColor: colors.primaryLight, }, optionCorrect: { borderColor: colors.success, backgroundColor: colors.successLight, }, optionWrong: { borderColor: colors.danger, backgroundColor: colors.dangerLight, }, optionText: { flex: 1, fontSize: 14, color: colors.textPrimary, }, optionTextSelected: { color: colors.primaryText, fontWeight: '600', }, optionTextCorrect: { color: colors.successText, fontWeight: '600', }, optionTextWrong: { color: colors.dangerText, fontWeight: '600', }, input: { height: 44, borderRadius: 6, borderWidth: 1, borderColor: colors.border, paddingHorizontal: 12, fontSize: 14, color: colors.textPrimary, }, explanation: { marginTop: 8, padding: 8, borderRadius: 8, backgroundColor: colors.infoLight, }, explanationLabel: { fontSize: 12, fontWeight: '700', color: colors.infoText, marginBottom: 4, }, explanationText: { fontSize: 13, lineHeight: 18, color: colors.textSecondary, }, resultCard: { alignItems: 'center', backgroundColor: colors.white, borderRadius: 8, padding: 8, marginBottom: 8, }, resultTitle: { fontSize: 16, fontWeight: '700', color: colors.textPrimary, }, resultScore: { color: colors.primary, }, resultHint: { marginTop: 6, fontSize: 13, color: colors.textSecondary, }, submitBtn: { marginTop: 8, height: 48, borderRadius: 8, alignItems: 'center', justifyContent: 'center', backgroundColor: colors.primaryBtn, }, submitBtnDisabled: { opacity: 0.6, }, submitText: { fontSize: 15, fontWeight: '700', color: colors.white, }, nextBtn: { marginTop: 8, height: 48, borderRadius: 8, alignItems: 'center', justifyContent: 'center', backgroundColor: colors.successBtn, }, nextText: { fontSize: 15, fontWeight: '700', color: colors.white, }, stateBox: { flex: 1, alignItems: 'center', justifyContent: 'center', }, stateText: { marginTop: 12, fontSize: 14, color: colors.textSecondary, }, retryBtn: { marginTop: 16, paddingHorizontal: 24, paddingVertical: 8, borderRadius: 18, backgroundColor: colors.primaryBtn, }, retryText: { fontSize: 13, fontWeight: '600', color: colors.white, }, });