174 lines
7.3 KiB
TypeScript
174 lines
7.3 KiB
TypeScript
import { Trophy, Volume2 } from "lucide-react";
|
|
import { cn } from "@/lib/utils";
|
|
import { pkPageCursorClass } from "../constants";
|
|
import type { AnswerState, AppSettings, Grade, Team, WordItem } from "../types";
|
|
import { formatWordDisplay } from "../utils";
|
|
import { AdaptiveWordText } from "./AdaptiveWordText";
|
|
import { TeamCard } from "./TeamCard";
|
|
import { WordImage } from "./WordImage";
|
|
|
|
type GameScreenProps = {
|
|
selectedGrade: Grade;
|
|
selectedClassTypeName?: string;
|
|
showCurrentWord: boolean;
|
|
roundWords: WordItem[];
|
|
currentWord: WordItem;
|
|
options: WordItem[];
|
|
optionCount: number;
|
|
questionIndex: number;
|
|
currentTeam: Team;
|
|
scoreRed: number;
|
|
scoreBlue: number;
|
|
answerState: AnswerState;
|
|
selectedWord: number | null;
|
|
speechTip: string;
|
|
settings: AppSettings;
|
|
onSpeakWord: (word: WordItem) => void;
|
|
onCheckAnswer: (word: WordItem) => void;
|
|
onNextQuestion: () => void;
|
|
};
|
|
|
|
export function GameScreen({
|
|
selectedGrade,
|
|
selectedClassTypeName,
|
|
showCurrentWord,
|
|
roundWords,
|
|
currentWord,
|
|
options,
|
|
optionCount,
|
|
questionIndex,
|
|
currentTeam,
|
|
scoreRed,
|
|
scoreBlue,
|
|
answerState,
|
|
selectedWord,
|
|
speechTip,
|
|
settings,
|
|
onSpeakWord,
|
|
onCheckAnswer,
|
|
onNextQuestion,
|
|
}: GameScreenProps) {
|
|
const progress = roundWords.length > 0 ? (questionIndex / roundWords.length) * 100 : 0;
|
|
|
|
return (
|
|
<main className={cn(pkPageCursorClass, "pk-game-stage mx-auto min-h-[calc(100vh-60px)] max-w-7xl px-4 py-4 sm:px-6 sm:py-4")}>
|
|
<section className="pk-game-card relative overflow-hidden rounded-[28px] bg-white p-[clamp(18px,3vw,34px)] shadow-soft">
|
|
{answerState !== "idle" && (
|
|
<div
|
|
className={cn(
|
|
"pk-answer-feedback z-10 mb-4 flex flex-col items-center gap-3 rounded-lg border bg-white/95 px-4 py-3 text-center text-[clamp(16px,1.8vw,22px)] font-black shadow-lg backdrop-blur md:absolute md:right-[clamp(18px,3vw,34px)] md:top-[clamp(18px,3vw,34px)] md:mb-0 md:max-w-[360px]",
|
|
answerState === "correct" && "border-emerald-100 text-emerald-600",
|
|
answerState === "wrong" && "border-red-100 text-red-500",
|
|
)}
|
|
>
|
|
<div>
|
|
{answerState === "correct" && `🎉 太棒了!答对啦!+${settings.scorePerCorrect}分`}
|
|
{answerState === "wrong" &&
|
|
(settings.scorePerWrong > 0
|
|
? `答错啦,扣${settings.scorePerWrong}分,轮到下一队!`
|
|
: "答错啦,轮到下一队!")}
|
|
</div>
|
|
<button
|
|
onClick={onNextQuestion}
|
|
className="inline-flex items-center gap-2 rounded-full bg-slate-900 px-7 py-2.5 text-base font-bold text-white shadow-lg transition hover:bg-slate-700"
|
|
>
|
|
下一题
|
|
<Trophy className="size-5" />
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
<div className="pk-game-header mb-[clamp(12px,2vw,22px)] text-center">
|
|
<h1 className="pk-game-title text-[clamp(26px,3vw,38px)] font-black text-rose-500">单词PK大作战</h1>
|
|
<p className="pk-game-meta mt-2 text-sm font-medium text-slate-500">
|
|
{selectedGrade} / {selectedClassTypeName ?? "班型"} / {roundWords.length} 道题
|
|
</p>
|
|
</div>
|
|
|
|
<div
|
|
className={cn(
|
|
"pk-game-grid grid items-center gap-[clamp(18px,3vw,34px)] lg:grid-cols-[minmax(300px,0.9fr)_minmax(420px,1.25fr)]",
|
|
!showCurrentWord && "pk-game-grid-word-hidden",
|
|
)}
|
|
>
|
|
<div className="flex min-w-0 flex-col justify-center">
|
|
<div className="flex items-center gap-[clamp(10px,1.8vw,18px)]">
|
|
<TeamCard team="red" active={currentTeam === "red"} score={scoreRed} />
|
|
<div className="text-[clamp(20px,2.8vw,30px)] font-black text-amber-500">VS</div>
|
|
<TeamCard team="blue" active={currentTeam === "blue"} score={scoreBlue} />
|
|
</div>
|
|
|
|
<div
|
|
className={cn(
|
|
"pk-current-turn mt-4 rounded-lg px-4 py-3 text-center text-[clamp(18px,2vw,23px)] font-black",
|
|
currentTeam === "red" ? "bg-red-50 text-red-700" : "bg-blue-50 text-blue-700",
|
|
)}
|
|
>
|
|
{currentTeam === "red" ? "🔴 红队请作答" : "🔵 蓝队请作答"}
|
|
</div>
|
|
|
|
<div className="mt-4 h-3 overflow-hidden rounded-full bg-slate-100">
|
|
<div className="h-full rounded-full bg-emerald-500 transition-all" style={{ width: `${progress}%` }} />
|
|
</div>
|
|
|
|
<div
|
|
className={cn(
|
|
"pk-listen-area flex flex-col items-center justify-center text-center",
|
|
showCurrentWord
|
|
? "py-[clamp(24px,5vw,70px)]"
|
|
: "min-h-[clamp(150px,24vh,230px)] py-[clamp(16px,3vw,34px)]",
|
|
)}
|
|
>
|
|
<p className="text-[clamp(16px,1.7vw,20px)] text-slate-600">听一听,这是哪个单词?</p>
|
|
<button
|
|
onClick={() => onSpeakWord(currentWord)}
|
|
className="pk-speak-button mt-4 grid size-[clamp(62px,6vw,80px)] place-items-center rounded-full bg-rose-400 text-white shadow-lg transition hover:scale-105 hover:bg-rose-500"
|
|
aria-label="重新朗读当前单词"
|
|
>
|
|
<Volume2 className="size-[clamp(28px,3vw,36px)]" />
|
|
</button>
|
|
{showCurrentWord && (
|
|
<>
|
|
<AdaptiveWordText text={formatWordDisplay(currentWord.english, settings.wordDisplayMode)} variant="hero" />
|
|
<p className="mt-2 min-h-6 text-[clamp(13px,1.4vw,16px)] text-slate-500">{speechTip}</p>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex min-w-0 flex-col justify-center">
|
|
<div
|
|
className={cn(
|
|
"pk-answer-grid grid grid-cols-1 gap-[clamp(14px,2vw,22px)] sm:grid-cols-2",
|
|
optionCount >= 6 && "xl:grid-cols-3",
|
|
)}
|
|
>
|
|
{options.map((option) => {
|
|
const correct = option.id === currentWord.id;
|
|
const selected = selectedWord === option.id;
|
|
const revealCorrect = answerState !== "idle" && correct;
|
|
|
|
return (
|
|
<button
|
|
key={option.id}
|
|
onClick={() => onCheckAnswer(option)}
|
|
className={cn(
|
|
"pk-option-button flex min-h-[clamp(104px,18vh,188px)] flex-col items-center justify-center rounded-[20px] border-4 bg-white p-4 text-[clamp(22px,3vw,34px)] font-black transition hover:-translate-y-1 hover:border-sky-300 hover:shadow-lg",
|
|
answerState === "idle" && "border-slate-200",
|
|
revealCorrect && "border-emerald-400 bg-emerald-50 text-emerald-700",
|
|
selected && answerState === "wrong" && "border-red-400 bg-red-50 text-red-700",
|
|
)}
|
|
>
|
|
<WordImage value={option.image} label={option.english} />
|
|
<AdaptiveWordText text={formatWordDisplay(option.english, settings.wordDisplayMode)} variant="option" />
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
);
|
|
}
|