新增记忆字母表功能

This commit is contained in:
2025-10-09 11:23:56 +08:00
parent 5b7cac029b
commit 4829ab9531
16 changed files with 1135 additions and 9 deletions

View File

@@ -0,0 +1,46 @@
import Button from "@/components/Button";
import IconClick from "@/components/IconClick";
import IMAGES from "@/config/images";
import { Letter, SupportedAlphabets } from "@/interfaces";
import { Dispatch, SetStateAction, useRef, useState } from "react";
export default function MemoryCard(
{
alphabet,
language,
setChosenAlphabet
}: {
alphabet: Letter[],
language: string,
setChosenAlphabet: Dispatch<SetStateAction<SupportedAlphabets | null>>
}
) {
const [index, setIndex] = useState(Math.floor(Math.random() * alphabet.length));
const [more, setMore] = useState(false);
const [ipaDisplay, setIPADisplay] = useState(true);
const [letterDisplay, setLetterDisplay] = useState(true);
const letter = alphabet[index];
return (
<div className="w-full flex justify-center items-center">
<div className="m-4 p-4 w-full md:w-[60dvw] flex-col rounded-2xl shadow border-gray-200 border flex justify-center items-center">
<div className="w-full flex justify-end items-center">
<IconClick size={32} alt="close" src={IMAGES.close} onClick={() => setChosenAlphabet(null)}></IconClick>
</div>
<div className="flex flex-col gap-12 justify-center items-center">
<span className="text-7xl md:text-9xl">{letterDisplay ? letter.letter : ''}</span>
<span className="text-5xl md:text-7xl text-gray-400">{ipaDisplay ? letter.letter_sound_ipa : ''}</span>
</div>
<div className="flex flex-row mt-32 items-center justify-center gap-2">
<IconClick size={48} alt="refresh" src={IMAGES.refresh} onClick={() => setIndex(Math.floor(Math.random() * alphabet.length))}></IconClick>
<IconClick size={48} alt="more" src={IMAGES.more_horiz} onClick={() => setMore(!more)}></IconClick>
{
more ? (<>
<Button className="w-20" label={letterDisplay ? '隐藏字母' : '显示字母'} onClick={() => { setLetterDisplay(!letterDisplay) }}></Button>
<Button className="w-20" label={ipaDisplay ? '隐藏IPA' : '显示IPA'} onClick={() => { setIPADisplay(!ipaDisplay) }}></Button>
</>) : (<></>)
}
</div>
</div>
</div>
);
}

70
src/app/alphabet/page.tsx Normal file
View File

@@ -0,0 +1,70 @@
'use client';
import Button from "@/components/Button";
import { Letter, SupportedAlphabets } from "@/interfaces";
import { useEffect, useState } from "react";
import MemoryCard from "./MemoryCard";
export default function Home() {
const [chosenAlphabet, setChosenAlphabet] = useState<SupportedAlphabets | null>(null);
const [alphabetData, setAlphabetData] = useState<Record<SupportedAlphabets, Letter[] | null>>({
japanese: null,
english: null,
esperanto: null,
uyghur: null
});
const [loadingState, setLoadingState] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
useEffect(() => {
if (chosenAlphabet && !alphabetData[chosenAlphabet]) {
setLoadingState('loading');
fetch('/alphabets/' + chosenAlphabet + '.json')
.then(res => {
if (!res.ok) throw new Error('Network response was not ok');
return res.json();
}).then((obj) => {
setAlphabetData(prev => ({ ...prev, [chosenAlphabet]: obj as Letter[] }));
setLoadingState('success');
}).catch(() => {
setLoadingState('error');
});
}
}, [chosenAlphabet, alphabetData]);
useEffect(() => {
if (loadingState === 'error') {
const timer = setTimeout(() => {
setLoadingState('idle');
setChosenAlphabet(null);
}, 2000);
return () => clearTimeout(timer);
}
}, [loadingState]);
if (!chosenAlphabet) return (
<div className="border border-gray-200 m-4 mt-4 flex flex-col justify-center items-center p-4 rounded-2xl gap-2">
<span className="text-2xl md:text-3xl"></span>
<div className="flex gap-1 flex-wrap">
<Button label="日语假名" onClick={() => setChosenAlphabet('japanese')}></Button>
<Button label="英文字母" onClick={() => setChosenAlphabet('english')}></Button>
<Button label="维吾尔字母" onClick={() => setChosenAlphabet('uyghur')}></Button>
<Button label="世界语字母" onClick={() => setChosenAlphabet('esperanto')}></Button>
</div>
</div>);
if (loadingState === 'loading') {
return '加载中...';
}
if (loadingState === 'error') {
return '加载失败,请重试';
}
if (loadingState === 'success' && alphabetData[chosenAlphabet]) {
return (<MemoryCard
language={chosenAlphabet}
alphabet={alphabetData[chosenAlphabet]}
setChosenAlphabet={setChosenAlphabet}>
</MemoryCard>);
}
return null;
}

View File

@@ -56,6 +56,11 @@ function LinkGrid() {
name="逐句视频播放器"
description="基于SRT字幕文件逐句播放视频以模仿母语者的发音"
color="#3c988d"></LinkArea>
<LinkArea
href="/alphabet"
name="记忆字母表"
description="从字母表开始新语言的学习"
color="#dd7486"></LinkArea>
<LinkArea
href="#"
name="更多功能"

View File

@@ -1,7 +1,7 @@
'use client';
import { BOARD_WIDTH, TEXT_WIDTH, BOARD_HEIGHT, TEXT_SIZE } from "@/constants";
import Word from "@/interfaces/Word";
import { Word } from "@/interfaces";
import { Dispatch, SetStateAction, useEffect } from "react";
export default function WordBoard(

View File

@@ -2,7 +2,7 @@
import WordBoard from "@/app/word-board/WordBoard";
import Button from "../../components/Button";
import { KeyboardEvent, useRef, useState } from "react";
import Word from "@/interfaces/Word";
import { Word } from "@/interfaces";
import { BOARD_WIDTH, TEXT_WIDTH, BOARD_HEIGHT, TEXT_SIZE } from "@/constants";
import { inspect } from "@/utils";

View File

@@ -8,7 +8,10 @@ const IMAGES = {
autoplay: '/images/autoplay_24dp_1F1F1F_FILL0_wght400_GRAD0_opsz24.svg',
autopause: '/images/autopause_24dp_1F1F1F_FILL0_wght400_GRAD0_opsz24.svg',
speed_1x: '/images/1x_mobiledata_24dp_1F1F1F_FILL0_wght400_GRAD0_opsz24.svg',
play_arrow: '/images/play_arrow_24dp_1F1F1F_FILL0_wght400_GRAD0_opsz24.svg'
play_arrow: '/images/play_arrow_24dp_1F1F1F_FILL0_wght400_GRAD0_opsz24.svg',
close: '/images/close_24dp_1F1F1F_FILL0_wght400_GRAD0_opsz24.svg',
refresh: '/images/refresh_24dp_1F1F1F_FILL0_wght400_GRAD0_opsz24.svg',
more_horiz: '/images/more_horiz_24dp_1F1F1F_FILL0_wght400_GRAD0_opsz24.svg',
}
export default IMAGES;

13
src/interfaces.ts Normal file
View File

@@ -0,0 +1,13 @@
export interface Word {
word: string;
x: number;
y: number;
}export interface Letter {
letter: string;
letter_name_ipa: string;
letter_sound_ipa: string;
roman_letter?: string;
}
export type SupportedAlphabets = 'japanese' | 'english' | 'esperanto' | 'uyghur';

View File

@@ -1,5 +0,0 @@
export default interface Word {
word: string,
x: number,
y: number
}