This commit is contained in:
2025-12-29 10:40:59 +08:00
parent af259d4691
commit 3ac17f66f2
19 changed files with 215 additions and 131 deletions

View File

@@ -99,7 +99,7 @@ export default function AlphabetCard({ alphabet, alphabetType, onBack }: Alphabe
return ( return (
<div className="min-h-[calc(100vh-64px)] bg-[#35786f] flex items-center justify-center px-4 py-8"> <div className="min-h-[calc(100vh-64px)] bg-[#35786f] flex items-center justify-center px-4 py-8">
<div className="w-full max-w-2xl"> <div className="w-full max-w-2xl">
{/* 返回按钮 */} {/* 右上角返回按钮 */}
<div className="flex justify-end mb-4"> <div className="flex justify-end mb-4">
<IconClick <IconClick
size={32} size={32}
@@ -110,13 +110,15 @@ export default function AlphabetCard({ alphabet, alphabetType, onBack }: Alphabe
/> />
</div> </div>
{/* 主卡片 */} {/* 白色主卡片容器 */}
<div className="bg-white rounded-2xl shadow-xl p-8 md:p-12"> <div className="bg-white rounded-2xl shadow-xl p-8 md:p-12">
{/* 进度指示器 */} {/* 顶部进度指示器和显示选项按钮 */}
<div className="flex justify-between items-center mb-6"> <div className="flex justify-between items-center mb-6">
{/* 当前字母进度 */}
<span className="text-sm text-gray-500"> <span className="text-sm text-gray-500">
{currentIndex + 1} / {alphabet.length} {currentIndex + 1} / {alphabet.length}
</span> </span>
{/* 显示选项切换按钮组 */}
<div className="flex gap-2 flex-wrap"> <div className="flex gap-2 flex-wrap">
<button <button
onClick={() => setShowLetter(!showLetter)} onClick={() => setShowLetter(!showLetter)}
@@ -128,6 +130,7 @@ export default function AlphabetCard({ alphabet, alphabetType, onBack }: Alphabe
> >
{t("letter")} {t("letter")}
</button> </button>
{/* IPA 音标显示切换 */}
<button <button
onClick={() => setShowIPA(!showIPA)} onClick={() => setShowIPA(!showIPA)}
className={`px-3 py-1 rounded-full text-sm transition-colors ${ className={`px-3 py-1 rounded-full text-sm transition-colors ${
@@ -138,6 +141,7 @@ export default function AlphabetCard({ alphabet, alphabetType, onBack }: Alphabe
> >
IPA IPA
</button> </button>
{/* 罗马音显示切换(仅日语显示) */}
{hasRomanization && ( {hasRomanization && (
<button <button
onClick={() => setShowRoman(!showRoman)} onClick={() => setShowRoman(!showRoman)}
@@ -150,6 +154,7 @@ export default function AlphabetCard({ alphabet, alphabetType, onBack }: Alphabe
{t("roman")} {t("roman")}
</button> </button>
)} )}
{/* 随机模式切换 */}
<button <button
onClick={() => setIsRandomMode(!isRandomMode)} onClick={() => setIsRandomMode(!isRandomMode)}
className={`px-3 py-1 rounded-full text-sm transition-colors ${ className={`px-3 py-1 rounded-full text-sm transition-colors ${
@@ -163,8 +168,9 @@ export default function AlphabetCard({ alphabet, alphabetType, onBack }: Alphabe
</div> </div>
</div> </div>
{/* 字母显示区域 */} {/* 字母主要内容显示区域 */}
<div className="text-center mb-8"> <div className="text-center mb-8">
{/* 字母本身(可隐藏) */}
{showLetter ? ( {showLetter ? (
<div className="text-6xl md:text-8xl font-bold text-gray-800 mb-4"> <div className="text-6xl md:text-8xl font-bold text-gray-800 mb-4">
{currentLetter.letter} {currentLetter.letter}
@@ -175,12 +181,14 @@ export default function AlphabetCard({ alphabet, alphabetType, onBack }: Alphabe
</div> </div>
)} )}
{/* IPA 音标显示 */}
{showIPA && ( {showIPA && (
<div className="text-2xl md:text-3xl text-gray-600 mb-2"> <div className="text-2xl md:text-3xl text-gray-600 mb-2">
{currentLetter.letter_sound_ipa} {currentLetter.letter_sound_ipa}
</div> </div>
)} )}
{/* 罗马音显示(日语) */}
{showRoman && hasRomanization && currentLetter.roman_letter && ( {showRoman && hasRomanization && currentLetter.roman_letter && (
<div className="text-lg md:text-xl text-gray-500"> <div className="text-lg md:text-xl text-gray-500">
{currentLetter.roman_letter} {currentLetter.roman_letter}
@@ -188,8 +196,9 @@ export default function AlphabetCard({ alphabet, alphabetType, onBack }: Alphabe
)} )}
</div> </div>
{/* 导航控制 */} {/* 底部导航控制区域 */}
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
{/* 上一个按钮 */}
<button <button
onClick={goToPrevious} onClick={goToPrevious}
className="p-3 rounded-full bg-gray-100 hover:bg-gray-200 transition-colors" className="p-3 rounded-full bg-gray-100 hover:bg-gray-200 transition-colors"
@@ -198,8 +207,10 @@ export default function AlphabetCard({ alphabet, alphabetType, onBack }: Alphabe
<ChevronLeft size={24} /> <ChevronLeft size={24} />
</button> </button>
{/* 中间区域:随机按钮或进度条 */}
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">
{isRandomMode ? ( {isRandomMode ? (
// 随机模式:显示随机切换按钮
<button <button
onClick={goToRandom} onClick={goToRandom}
className="px-4 py-2 rounded-full bg-[#35786f] text-white text-sm font-medium hover:bg-[#2d5f58] transition-colors" className="px-4 py-2 rounded-full bg-[#35786f] text-white text-sm font-medium hover:bg-[#2d5f58] transition-colors"
@@ -207,6 +218,7 @@ export default function AlphabetCard({ alphabet, alphabetType, onBack }: Alphabe
{t("randomNext")} {t("randomNext")}
</button> </button>
) : ( ) : (
// 顺序模式:显示进度点
<div className="flex gap-1 flex-wrap max-w-xs justify-center"> <div className="flex gap-1 flex-wrap max-w-xs justify-center">
{alphabet.slice(0, 20).map((_, index) => ( {alphabet.slice(0, 20).map((_, index) => (
<div <div
@@ -218,6 +230,7 @@ export default function AlphabetCard({ alphabet, alphabetType, onBack }: Alphabe
}`} }`}
/> />
))} ))}
{/* 超过20个字母时显示省略号 */}
{alphabet.length > 20 && ( {alphabet.length > 20 && (
<div className="text-xs text-gray-500 flex items-center">...</div> <div className="text-xs text-gray-500 flex items-center">...</div>
)} )}
@@ -225,6 +238,7 @@ export default function AlphabetCard({ alphabet, alphabetType, onBack }: Alphabe
)} )}
</div> </div>
{/* 下一个按钮 */}
<button <button
onClick={goToNext} onClick={goToNext}
className="p-3 rounded-full bg-gray-100 hover:bg-gray-200 transition-colors" className="p-3 rounded-full bg-gray-100 hover:bg-gray-200 transition-colors"
@@ -235,7 +249,7 @@ export default function AlphabetCard({ alphabet, alphabetType, onBack }: Alphabe
</div> </div>
</div> </div>
{/* 操作提示 */} {/* 底部操作提示文字 */}
<div className="text-center mt-6 text-white text-sm"> <div className="text-center mt-6 text-white text-sm">
<p> <p>
{isRandomMode {isRandomMode
@@ -246,7 +260,7 @@ export default function AlphabetCard({ alphabet, alphabetType, onBack }: Alphabe
</div> </div>
</div> </div>
{/* 触摸事件处理 */} {/* 全屏触摸事件监听层(用于滑动切换) */}
<div <div
className="absolute inset-0 pointer-events-none" className="absolute inset-0 pointer-events-none"
onTouchStart={onTouchStart} onTouchStart={onTouchStart}

View File

@@ -50,14 +50,18 @@ export default function Alphabet() {
return ( return (
<div className="min-h-[calc(100vh-64px)] bg-[#35786f] flex flex-col items-center justify-center px-4"> <div className="min-h-[calc(100vh-64px)] bg-[#35786f] flex flex-col items-center justify-center px-4">
<Container className="p-8 max-w-2xl w-full text-center"> <Container className="p-8 max-w-2xl w-full text-center">
{/* 页面标题 */}
<h1 className="text-4xl md:text-5xl font-bold text-gray-800 mb-4"> <h1 className="text-4xl md:text-5xl font-bold text-gray-800 mb-4">
{t("chooseCharacters")} {t("chooseCharacters")}
</h1> </h1>
{/* 副标题说明 */}
<p className="text-gray-600 mb-8 text-lg"> <p className="text-gray-600 mb-8 text-lg">
</p> </p>
{/* 语言选择按钮网格 */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* 日语假名选项 */}
<LightButton <LightButton
onClick={() => setChosenAlphabet("japanese")} onClick={() => setChosenAlphabet("japanese")}
className="p-6 text-lg font-medium hover:scale-105 transition-transform" className="p-6 text-lg font-medium hover:scale-105 transition-transform"
@@ -68,6 +72,7 @@ export default function Alphabet() {
</div> </div>
</LightButton> </LightButton>
{/* 英语字母选项 */}
<LightButton <LightButton
onClick={() => setChosenAlphabet("english")} onClick={() => setChosenAlphabet("english")}
className="p-6 text-lg font-medium hover:scale-105 transition-transform" className="p-6 text-lg font-medium hover:scale-105 transition-transform"
@@ -78,6 +83,7 @@ export default function Alphabet() {
</div> </div>
</LightButton> </LightButton>
{/* 维吾尔语字母选项 */}
<LightButton <LightButton
onClick={() => setChosenAlphabet("uyghur")} onClick={() => setChosenAlphabet("uyghur")}
className="p-6 text-lg font-medium hover:scale-105 transition-transform" className="p-6 text-lg font-medium hover:scale-105 transition-transform"
@@ -88,6 +94,7 @@ export default function Alphabet() {
</div> </div>
</LightButton> </LightButton>
{/* 世界语字母选项 */}
<LightButton <LightButton
onClick={() => setChosenAlphabet("esperanto")} onClick={() => setChosenAlphabet("esperanto")}
className="p-6 text-lg font-medium hover:scale-105 transition-transform" className="p-6 text-lg font-medium hover:scale-105 transition-transform"

View File

@@ -19,6 +19,7 @@ const FolderSelector: React.FC<FolderSelectorProps> = ({ folders }) => {
<div className="w-full max-w-2xl"> <div className="w-full max-w-2xl">
<div className="bg-white rounded-2xl shadow-xl p-6 md:p-8"> <div className="bg-white rounded-2xl shadow-xl p-6 md:p-8">
{folders.length === 0 ? ( {folders.length === 0 ? (
// 空状态 - 显示提示和跳转按钮
<div className="text-center"> <div className="text-center">
<h1 className="text-2xl md:text-3xl font-bold text-gray-800 mb-4"> <h1 className="text-2xl md:text-3xl font-bold text-gray-800 mb-4">
{t("noFolders")} {t("noFolders")}
@@ -32,9 +33,11 @@ const FolderSelector: React.FC<FolderSelectorProps> = ({ folders }) => {
</div> </div>
) : ( ) : (
<> <>
{/* 页面标题 */}
<h1 className="text-2xl md:text-3xl font-bold text-gray-800 mb-6"> <h1 className="text-2xl md:text-3xl font-bold text-gray-800 mb-6">
{t("selectFolder")} {t("selectFolder")}
</h1> </h1>
{/* 文件夹列表 */}
<div className="border border-gray-200 rounded-2xl max-h-96 overflow-y-auto"> <div className="border border-gray-200 rounded-2xl max-h-96 overflow-y-auto">
{folders {folders
.toSorted((a, b) => a.id - b.id) .toSorted((a, b) => a.id - b.id)
@@ -46,9 +49,11 @@ const FolderSelector: React.FC<FolderSelectorProps> = ({ folders }) => {
} }
className="flex flex-row items-center p-4 gap-3 hover:cursor-pointer hover:bg-gray-50 transition-colors border-b border-gray-100 last:border-b-0" className="flex flex-row items-center p-4 gap-3 hover:cursor-pointer hover:bg-gray-50 transition-colors border-b border-gray-100 last:border-b-0"
> >
{/* 文件夹图标 */}
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<Fd className="text-gray-600" size={24} /> <Fd className="text-gray-600" size={24} />
</div> </div>
{/* 文件夹信息 */}
<div className="flex-1"> <div className="flex-1">
<div className="font-medium text-gray-900"> <div className="font-medium text-gray-900">
{folder.name} {folder.name}
@@ -61,6 +66,7 @@ const FolderSelector: React.FC<FolderSelectorProps> = ({ folders }) => {
})} })}
</div> </div>
</div> </div>
{/* 右箭头 */}
<div className="text-gray-400"> <div className="text-gray-400">
<svg <svg
className="w-5 h-5" className="w-5 h-5"

View File

@@ -3,7 +3,7 @@
import React from "react"; import React from "react";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { ChevronLeft, ChevronRight, RotateCcw, Pause } from "lucide-react"; import { ChevronLeft, ChevronRight, RotateCcw, Pause } from "lucide-react";
import DarkButton from "@/components/ui/buttons/DarkButton"; import LightButton from "@/components/ui/buttons/LightButton";
import { ControlBarProps } from "../../types/controls"; import { ControlBarProps } from "../../types/controls";
import PlayButton from "../atoms/PlayButton"; import PlayButton from "../atoms/PlayButton";
import SpeedControl from "../atoms/SpeedControl"; import SpeedControl from "../atoms/SpeedControl";
@@ -31,32 +31,32 @@ export default function ControlBar({
disabled={disabled} disabled={disabled}
/> />
<DarkButton <LightButton
onClick={disabled ? undefined : onPrevious} onClick={disabled ? undefined : onPrevious}
disabled={disabled} disabled={disabled}
className="flex items-center px-3 py-2" className="flex items-center px-3 py-2"
> >
<ChevronLeft className="w-4 h-4 mr-2" /> <ChevronLeft className="w-4 h-4 mr-2" />
{t("previous")} {t("previous")}
</DarkButton> </LightButton>
<DarkButton <LightButton
onClick={disabled ? undefined : onNext} onClick={disabled ? undefined : onNext}
disabled={disabled} disabled={disabled}
className="flex items-center px-3 py-2" className="flex items-center px-3 py-2"
> >
{t("next")} {t("next")}
<ChevronRight className="w-4 h-4 ml-2" /> <ChevronRight className="w-4 h-4 ml-2" />
</DarkButton> </LightButton>
<DarkButton <LightButton
onClick={disabled ? undefined : onRestart} onClick={disabled ? undefined : onRestart}
disabled={disabled} disabled={disabled}
className="flex items-center px-3 py-2" className="flex items-center px-3 py-2"
> >
<RotateCcw className="w-4 h-4 mr-2" /> <RotateCcw className="w-4 h-4 mr-2" />
{t("restart")} {t("restart")}
</DarkButton> </LightButton>
<SpeedControl <SpeedControl
playbackRate={playbackRate} playbackRate={playbackRate}
@@ -64,14 +64,14 @@ export default function ControlBar({
disabled={disabled} disabled={disabled}
/> />
<DarkButton <LightButton
onClick={disabled ? undefined : onAutoPauseToggle} onClick={disabled ? undefined : onAutoPauseToggle}
disabled={disabled} disabled={disabled}
className="flex items-center px-3 py-2" className="flex items-center px-3 py-2"
> >
<Pause className="w-4 h-4 mr-2" /> <Pause className="w-4 h-4 mr-2" />
{t("autoPause", { enabled: autoPause ? t("on") : t("off") })} {t("autoPause", { enabled: autoPause ? t("on") : t("off") })}
</DarkButton> </LightButton>
</div> </div>
); );
} }

View File

@@ -4,7 +4,7 @@ import React from "react";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { toast } from "sonner"; import { toast } from "sonner";
import { Video, FileText } from "lucide-react"; import { Video, FileText } from "lucide-react";
import DarkButton from "@/components/ui/buttons/DarkButton"; import LightButton from "@/components/ui/buttons/LightButton";
import { FileUploadProps } from "../../types/controls"; import { FileUploadProps } from "../../types/controls";
import { useFileUpload } from "../../hooks/useFileUpload"; import { useFileUpload } from "../../hooks/useFileUpload";
@@ -26,21 +26,21 @@ export default function UploadZone({ onVideoUpload, onSubtitleUpload, className
return ( return (
<div className={`flex gap-3 ${className || ''}`}> <div className={`flex gap-3 ${className || ''}`}>
<DarkButton <LightButton
onClick={handleVideoUpload} onClick={handleVideoUpload}
className="flex-1 py-2 px-3 text-sm" className="flex-1 py-2 px-3 text-sm"
> >
<Video className="w-4 h-4 mr-2" /> <Video className="w-4 h-4 mr-2" />
{t("uploadVideo")} {t("uploadVideo")}
</DarkButton> </LightButton>
<DarkButton <LightButton
onClick={handleSubtitleUpload} onClick={handleSubtitleUpload}
className="flex-1 py-2 px-3 text-sm" className="flex-1 py-2 px-3 text-sm"
> >
<FileText className="w-4 h-4 mr-2" /> <FileText className="w-4 h-4 mr-2" />
{t("uploadSubtitle")} {t("uploadSubtitle")}
</DarkButton> </LightButton>
</div> </div>
); );
} }

View File

@@ -14,7 +14,7 @@ import SubtitleArea from "./components/compounds/SubtitleArea";
import ControlBar from "./components/compounds/ControlBar"; import ControlBar from "./components/compounds/ControlBar";
import UploadZone from "./components/compounds/UploadZone"; import UploadZone from "./components/compounds/UploadZone";
import SeekBar from "./components/atoms/SeekBar"; import SeekBar from "./components/atoms/SeekBar";
import DarkButton from "@/components/ui/buttons/DarkButton"; import LightButton from "@/components/ui/buttons/LightButton";
export default function SrtPlayerPage() { export default function SrtPlayerPage() {
const t = useTranslations("home"); const t = useTranslations("home");
@@ -182,13 +182,13 @@ export default function SrtPlayerPage() {
</p> </p>
</div> </div>
</div> </div>
<DarkButton <LightButton
onClick={state.video.url ? undefined : handleVideoUpload} onClick={state.video.url ? undefined : handleVideoUpload}
disabled={!!state.video.url} disabled={!!state.video.url}
className="px-2 py-1 text-xs" className="px-2 py-1 text-xs"
> >
{state.video.url ? srtT("uploaded") : srtT("upload")} {state.video.url ? srtT("uploaded") : srtT("upload")}
</DarkButton> </LightButton>
</div> </div>
</div> </div>
@@ -206,13 +206,13 @@ export default function SrtPlayerPage() {
</p> </p>
</div> </div>
</div> </div>
<DarkButton <LightButton
onClick={state.subtitle.url ? undefined : handleSubtitleUpload} onClick={state.subtitle.url ? undefined : handleSubtitleUpload}
disabled={!!state.subtitle.url} disabled={!!state.subtitle.url}
className="px-2 py-1 text-xs" className="px-2 py-1 text-xs"
> >
{state.subtitle.url ? srtT("uploaded") : srtT("upload")} {state.subtitle.url ? srtT("uploaded") : srtT("upload")}
</DarkButton> </LightButton>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -17,6 +17,7 @@ import { useTranslations } from "next-intl";
import { getLocalStorageOperator } from "@/lib/browser/localStorageOperators"; import { getLocalStorageOperator } from "@/lib/browser/localStorageOperators";
import { getTTSAudioUrl } from "@/lib/browser/tts"; import { getTTSAudioUrl } from "@/lib/browser/tts";
import { genIPA, genLocale } from "@/lib/server/translatorActions"; import { genIPA, genLocale } from "@/lib/server/translatorActions";
import PageLayout from "@/components/ui/PageLayout";
export default function TextSpeakerPage() { export default function TextSpeakerPage() {
const t = useTranslations("text_speaker"); const t = useTranslations("text_speaker");
@@ -225,24 +226,30 @@ export default function TextSpeakerPage() {
}; };
return ( return (
<> <PageLayout className="items-start py-4">
{/* 文本输入区域 */}
<div <div
className="my-4 p-4 mx-4 md:mx-32 border border-gray-200 rounded-2xl" className="border border-gray-200 rounded-2xl"
style={{ fontFamily: "Times New Roman, serif" }} style={{ fontFamily: "Times New Roman, serif" }}
> >
{/* 文本输入框 */}
<textarea <textarea
className="text-2xl resize-none focus:outline-0 min-h-64 w-full border-gray-200 border-b" className="text-2xl resize-none focus:outline-0 min-h-64 w-full border-gray-200 border-b p-4"
onChange={handleInputChange} onChange={handleInputChange}
ref={textareaRef} ref={textareaRef}
></textarea> ></textarea>
{/* IPA 显示区域 */}
{(ipa.length !== 0 && ( {(ipa.length !== 0 && (
<div className="overflow-auto text-gray-600 h-18 border-gray-200 border-b"> <div className="overflow-auto text-gray-600 h-18 border-gray-200 border-b px-4">
{ipa} {ipa}
</div> </div>
)) || <div className="h-18"></div>} )) || <div className="h-18"></div>}
<div className="mt-8 relative w-full flex flex-row flex-wrap gap-2 justify-center items-center">
{/* 控制按钮区域 */}
<div className="p-4 relative w-full flex flex-row flex-wrap gap-2 justify-center items-center">
{/* 速度调节面板 */}
{showSpeedAdjust && ( {showSpeedAdjust && (
<div className="bg-white p-6 rounded-2xl border-gray-200 border-2 shadow-2xl absolute left-1/2 -translate-x-1/2 -translate-y-full -top-4 flex flex-row flex-wrap gap-2 justify-center items-center"> <div className="bg-white p-6 rounded-2xl border-gray-200 border-2 shadow-2xl absolute left-1/2 -translate-x-1/2 -translate-y-full -top-4 flex flex-row flex-wrap gap-2 justify-center items-center z-10">
<IconClick <IconClick
size={45} size={45}
onClick={letMeSetSpeed(0.5)} onClick={letMeSetSpeed(0.5)}
@@ -280,6 +287,7 @@ export default function TextSpeakerPage() {
></IconClick> ></IconClick>
</div> </div>
)} )}
{/* 播放/暂停按钮 */}
<IconClick <IconClick
size={45} size={45}
onClick={speak} onClick={speak}
@@ -287,6 +295,7 @@ export default function TextSpeakerPage() {
alt="playorpause" alt="playorpause"
className={`${processing ? "bg-gray-200" : ""}`} className={`${processing ? "bg-gray-200" : ""}`}
></IconClick> ></IconClick>
{/* 自动暂停按钮 */}
<IconClick <IconClick
size={45} size={45}
onClick={() => { onClick={() => {
@@ -299,6 +308,7 @@ export default function TextSpeakerPage() {
src={autopause ? IMAGES.autoplay : IMAGES.autopause} src={autopause ? IMAGES.autoplay : IMAGES.autopause}
alt="autoplayorpause" alt="autoplayorpause"
></IconClick> ></IconClick>
{/* 速度调节按钮 */}
<IconClick <IconClick
size={45} size={45}
onClick={() => setShowSpeedAdjust(!showSpeedAdjust)} onClick={() => setShowSpeedAdjust(!showSpeedAdjust)}
@@ -306,6 +316,7 @@ export default function TextSpeakerPage() {
alt="speed" alt="speed"
className={`${showSpeedAdjust ? "bg-gray-200" : ""}`} className={`${showSpeedAdjust ? "bg-gray-200" : ""}`}
></IconClick> ></IconClick>
{/* 保存按钮 */}
<IconClick <IconClick
size={45} size={45}
onClick={save} onClick={save}
@@ -313,6 +324,7 @@ export default function TextSpeakerPage() {
alt="save" alt="save"
className={`${saving ? "bg-gray-200" : ""}`} className={`${saving ? "bg-gray-200" : ""}`}
></IconClick> ></IconClick>
{/* 功能开关按钮 */}
<div className="w-full flex flex-row flex-wrap gap-2 justify-center items-center"> <div className="w-full flex flex-row flex-wrap gap-2 justify-center items-center">
<LightButton <LightButton
selected={ipaEnabled} selected={ipaEnabled}
@@ -331,7 +343,12 @@ export default function TextSpeakerPage() {
</div> </div>
</div> </div>
</div> </div>
{/* 保存列表 */}
{showSaveList && (
<div className="mt-4 border border-gray-200 rounded-2xl overflow-hidden">
<SaveList show={showSaveList} handleUse={handleUseItem}></SaveList> <SaveList show={showSaveList} handleUse={handleUseItem}></SaveList>
</> </div>
)}
</PageLayout>
); );
} }

View File

@@ -6,7 +6,6 @@ import { signInAction, signUpAction, SignUpState } from "@/lib/actions/auth";
import Container from "@/components/ui/Container"; import Container from "@/components/ui/Container";
import Input from "@/components/ui/Input"; import Input from "@/components/ui/Input";
import LightButton from "@/components/ui/buttons/LightButton"; import LightButton from "@/components/ui/buttons/LightButton";
import DarkButton from "@/components/ui/buttons/DarkButton";
import { authClient } from "@/lib/auth-client"; import { authClient } from "@/lib/auth-client";
interface AuthFormProps { interface AuthFormProps {
@@ -115,17 +114,21 @@ export default function AuthForm({ redirectTo }: AuthFormProps) {
return ( return (
<div className="h-[calc(100vh-64px)] bg-[#35786f] flex items-center justify-center px-4"> <div className="h-[calc(100vh-64px)] bg-[#35786f] flex items-center justify-center px-4">
<Container className="p-8 max-w-md w-full"> <Container className="p-8 max-w-md w-full">
{/* 页面标题 */}
<div className="text-center mb-8"> <div className="text-center mb-8">
<h1 className="text-3xl font-bold text-gray-800 mb-2">{t(mode === 'signin' ? 'signIn' : 'signUp')}</h1> <h1 className="text-3xl font-bold text-gray-800 mb-2">{t(mode === 'signin' ? 'signIn' : 'signUp')}</h1>
</div> </div>
{/* 服务器端错误提示 */}
{currentError?.message && ( {currentError?.message && (
<div className="mb-4 p-3 bg-red-100 border border-red-400 text-red-700 rounded"> <div className="mb-4 p-3 bg-red-100 border border-red-400 text-red-700 rounded">
{currentError.message} {currentError.message}
</div> </div>
)} )}
{/* 登录/注册表单 */}
<form onSubmit={handleFormSubmit} className="space-y-4"> <form onSubmit={handleFormSubmit} className="space-y-4">
{/* 用户名输入(仅注册模式显示) */}
{mode === 'signup' && ( {mode === 'signup' && (
<div> <div>
<Input <Input
@@ -134,15 +137,18 @@ export default function AuthForm({ redirectTo }: AuthFormProps) {
placeholder={t("name")} placeholder={t("name")}
className="w-full px-3 py-2" className="w-full px-3 py-2"
/> />
{/* 客户端验证错误 */}
{errors.name && ( {errors.name && (
<p className="text-red-500 text-sm mt-1">{errors.name}</p> <p className="text-red-500 text-sm mt-1">{errors.name}</p>
)} )}
{/* 服务器端验证错误 */}
{currentError?.errors?.username && ( {currentError?.errors?.username && (
<p className="text-red-500 text-sm mt-1">{currentError.errors.username[0]}</p> <p className="text-red-500 text-sm mt-1">{currentError.errors.username[0]}</p>
)} )}
</div> </div>
)} )}
{/* 邮箱输入 */}
<div> <div>
<Input <Input
type="email" type="email"
@@ -158,6 +164,7 @@ export default function AuthForm({ redirectTo }: AuthFormProps) {
)} )}
</div> </div>
{/* 密码输入 */}
<div> <div>
<Input <Input
type="password" type="password"
@@ -173,6 +180,7 @@ export default function AuthForm({ redirectTo }: AuthFormProps) {
)} )}
</div> </div>
{/* 确认密码输入(仅注册模式显示) */}
{mode === 'signup' && ( {mode === 'signup' && (
<div> <div>
<Input <Input
@@ -187,7 +195,8 @@ export default function AuthForm({ redirectTo }: AuthFormProps) {
</div> </div>
)} )}
<DarkButton {/* 提交按钮 */}
<LightButton
type="submit" type="submit"
className={`w-full py-2 ${isSignInPending || isSignUpPending ? 'opacity-50 cursor-not-allowed' : ''}`} className={`w-full py-2 ${isSignInPending || isSignUpPending ? 'opacity-50 cursor-not-allowed' : ''}`}
> >
@@ -195,10 +204,12 @@ export default function AuthForm({ redirectTo }: AuthFormProps) {
? t("loading") ? t("loading")
: t(mode === 'signin' ? 'signInButton' : 'signUpButton') : t(mode === 'signin' ? 'signInButton' : 'signUpButton')
} }
</DarkButton> </LightButton>
</form> </form>
{/* 第三方登录区域 */}
<div className="mt-6"> <div className="mt-6">
{/* 分隔线 */}
<div className="relative"> <div className="relative">
<div className="absolute inset-0 flex items-center"> <div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-gray-300"></div> <div className="w-full border-t border-gray-300"></div>
@@ -208,6 +219,7 @@ export default function AuthForm({ redirectTo }: AuthFormProps) {
</div> </div>
</div> </div>
{/* GitHub 登录按钮 */}
<LightButton <LightButton
onClick={handleGitHubSignIn} onClick={handleGitHubSignIn}
className="w-full mt-4 py-2 flex items-center justify-center gap-2" className="w-full mt-4 py-2 flex items-center justify-center gap-2"
@@ -219,6 +231,7 @@ export default function AuthForm({ redirectTo }: AuthFormProps) {
</LightButton> </LightButton>
</div> </div>
{/* 模式切换链接 */}
<div className="mt-6 text-center"> <div className="mt-6 text-center">
<button <button
type="button" type="button"

View File

@@ -119,6 +119,7 @@ export default function FoldersClient({ userId }: { userId: string }) {
<PageLayout> <PageLayout>
<PageHeader title={t("title")} subtitle={t("subtitle")} /> <PageHeader title={t("title")} subtitle={t("subtitle")} />
{/* 新建文件夹按钮 */}
<button <button
onClick={async () => { onClick={async () => {
const folderName = prompt(t("enterFolderName")); const folderName = prompt(t("enterFolderName"));
@@ -141,9 +142,11 @@ export default function FoldersClient({ userId }: { userId: string }) {
<span>{loading ? t("creating") : t("newFolder")}</span> <span>{loading ? t("creating") : t("newFolder")}</span>
</button> </button>
{/* 文件夹列表 */}
<div className="mt-4"> <div className="mt-4">
<CardList> <CardList>
{folders.length === 0 ? ( {folders.length === 0 ? (
// 空状态
<div className="text-center py-12 text-gray-400"> <div className="text-center py-12 text-gray-400">
<div className="w-16 h-16 mx-auto mb-3 rounded-full bg-gray-100 flex items-center justify-center"> <div className="w-16 h-16 mx-auto mb-3 rounded-full bg-gray-100 flex items-center justify-center">
<FolderPlus size={24} className="text-gray-400" /> <FolderPlus size={24} className="text-gray-400" />
@@ -151,6 +154,7 @@ export default function FoldersClient({ userId }: { userId: string }) {
<p className="text-sm">{t("noFoldersYet")}</p> <p className="text-sm">{t("noFoldersYet")}</p>
</div> </div>
) : ( ) : (
// 文件夹卡片列表
<div className="rounded-xl border border-gray-200 overflow-hidden"> <div className="rounded-xl border border-gray-200 overflow-hidden">
{folders {folders
.toSorted((a, b) => a.id - b.id) .toSorted((a, b) => a.id - b.id)

View File

@@ -57,7 +57,9 @@ export default function InFolder({ folderId }: { folderId: number }) {
return ( return (
<PageLayout> <PageLayout>
{/* 顶部导航和标题栏 */}
<div className="mb-6"> <div className="mb-6">
{/* 返回按钮 */}
<button <button
onClick={router.back} onClick={router.back}
className="flex items-center gap-2 text-gray-500 hover:text-gray-700 transition-colors mb-4" className="flex items-center gap-2 text-gray-500 hover:text-gray-700 transition-colors mb-4"
@@ -66,7 +68,9 @@ export default function InFolder({ folderId }: { folderId: number }) {
<span className="text-sm">{t("back")}</span> <span className="text-sm">{t("back")}</span>
</button> </button>
{/* 页面标题和操作按钮 */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
{/* 标题区域 */}
<div> <div>
<h1 className="text-2xl md:text-3xl font-bold text-gray-800 mb-1"> <h1 className="text-2xl md:text-3xl font-bold text-gray-800 mb-1">
{t("textPairs")} {t("textPairs")}
@@ -76,6 +80,7 @@ export default function InFolder({ folderId }: { folderId: number }) {
</p> </p>
</div> </div>
{/* 操作按钮区域 */}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<GreenButton <GreenButton
onClick={() => { onClick={() => {
@@ -94,17 +99,21 @@ export default function InFolder({ folderId }: { folderId: number }) {
</div> </div>
</div> </div>
{/* 文本对列表 */}
<CardList> <CardList>
{loading ? ( {loading ? (
// 加载状态
<div className="p-8 text-center"> <div className="p-8 text-center">
<div className="w-8 h-8 border-2 border-gray-200 border-t-gray-400 rounded-full animate-spin mx-auto mb-3"></div> <div className="w-8 h-8 border-2 border-gray-200 border-t-gray-400 rounded-full animate-spin mx-auto mb-3"></div>
<p className="text-sm text-gray-500">{t("loadingTextPairs")}</p> <p className="text-sm text-gray-500">{t("loadingTextPairs")}</p>
</div> </div>
) : textPairs.length === 0 ? ( ) : textPairs.length === 0 ? (
// 空状态
<div className="p-12 text-center"> <div className="p-12 text-center">
<p className="text-sm text-gray-500 mb-2">{t("noTextPairs")}</p> <p className="text-sm text-gray-500 mb-2">{t("noTextPairs")}</p>
</div> </div>
) : ( ) : (
// 文本对卡片列表
<div className="divide-y divide-gray-100"> <div className="divide-y divide-gray-100">
{textPairs {textPairs
.toSorted((a, b) => a.id - b.id) .toSorted((a, b) => a.id - b.id)
@@ -123,6 +132,7 @@ export default function InFolder({ folderId }: { folderId: number }) {
)} )}
</CardList> </CardList>
{/* 添加文本对模态框 */}
<AddTextPairModal <AddTextPairModal
isOpen={openAddModal} isOpen={openAddModal}
onClose={() => setAddModal(false)} onClose={() => setAddModal(false)}

View File

@@ -1,6 +1,6 @@
import Image from "next/image"; import Image from "next/image";
import { Center } from "@/components/common/Center"; import PageLayout from "@/components/ui/PageLayout";
import Container from "@/components/ui/Container"; import PageHeader from "@/components/ui/PageHeader";
import { auth } from "@/auth"; import { auth } from "@/auth";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
@@ -16,25 +16,34 @@ export default async function ProfilePage() {
redirect("/auth?redirect=/profile"); redirect("/auth?redirect=/profile");
} }
console.log(JSON.stringify(session, null, 2));
return ( return (
<Center> <PageLayout>
<Container className="p-6"> <PageHeader title={t("myProfile")} />
<h1>{t("myProfile")}</h1>
{/* 用户信息区域 */}
<div className="flex flex-col items-center gap-4">
{/* 用户头像 */}
{session.user.image && ( {session.user.image && (
<Image <Image
width={64} width={80}
height={64} height={80}
alt="User Avatar" alt="User Avatar"
src={session.user.image as string} src={session.user.image as string}
className="rounded-4xl" className="rounded-full"
></Image> />
)} )}
<p>{session.user.name}</p>
<p>{t("email", { email: session.user.email })}</p> {/* 用户名和邮箱 */}
<div className="text-center">
<h2 className="text-xl font-semibold text-gray-800">
{session.user.name}
</h2>
<p className="text-gray-600">{t("email", { email: session.user.email })}</p>
</div>
{/* 登出按钮 */}
<LogoutButton /> <LogoutButton />
</Container> </div>
</Center> </PageLayout>
); );
} }

View File

@@ -1,5 +1,23 @@
/**
* CardList - 可滚动的卡片列表容器
*
* 用于显示可滚动的列表内容,如文件夹列表、文本对列表等
* - 最大高度 96 (24rem)
* - 垂直滚动
* - 圆角边框
*
* @example
* ```tsx
* <CardList>
* {items.map(item => (
* <div key={item.id}>{item.name}</div>
* ))}
* </CardList>
* ```
*/
interface CardListProps { interface CardListProps {
children: React.ReactNode; children: React.ReactNode;
/** 额外的 CSS 类名 */
className?: string; className?: string;
} }

View File

@@ -1,5 +1,17 @@
/**
* PageHeader - 页面标题组件
*
* 用于 PageLayout 内的页面标题,支持主标题和可选的副标题
*
* @example
* ```tsx
* <PageHeader title="我的文件夹" subtitle="管理和组织你的学习内容" />
* ```
*/
interface PageHeaderProps { interface PageHeaderProps {
/** 页面主标题 */
title: string; title: string;
/** 可选的副标题/描述 */
subtitle?: string; subtitle?: string;
} }

View File

@@ -1,5 +1,22 @@
/**
* PageLayout - 统一的页面布局组件
*
* 提供应用统一的标准页面布局:
* - 绿色背景 (#35786f)
* - 居中的白色圆角卡片
* - 响应式内边距
*
* @example
* ```tsx
* <PageLayout>
* <PageHeader title="标题" subtitle="副标题" />
* <div>页面内容</div>
* </PageLayout>
* ```
*/
interface PageLayoutProps { interface PageLayoutProps {
children: React.ReactNode; children: React.ReactNode;
/** 额外的 CSS 类名,用于自定义布局行为 */
className?: string; className?: string;
} }

View File

@@ -1,28 +0,0 @@
import PlainButton, { ButtonType } from "./PlainButton";
export default function DarkButton({
onClick,
className,
selected,
children,
type = "button",
disabled
}: {
onClick?: (() => void) | undefined;
className?: string;
selected?: boolean;
children?: React.ReactNode;
type?: ButtonType;
disabled?: boolean;
}) {
return (
<PlainButton
onClick={onClick}
className={`hover:bg-gray-100 text-black ${selected ? "bg-gray-100" : "bg-white"} ${className}`}
type={type}
disabled={disabled}
>
{children}
</PlainButton>
);
}

View File

@@ -1,34 +0,0 @@
import PlainButton, { ButtonType } from "./PlainButton";
interface GrayButtonProps {
onClick?: () => void;
children: React.ReactNode;
className?: string;
selected?: boolean;
type?: ButtonType;
disabled?: boolean;
}
export default function GrayButton({
onClick,
children,
className = "",
selected = false,
type = "button",
disabled = false,
}: GrayButtonProps) {
return (
<PlainButton
onClick={onClick}
className={`px-4 py-2 rounded-full transition-colors text-sm ${
selected
? "bg-[#35786f] text-white hover:bg-[#2d5f58]"
: "bg-gray-200 text-gray-700 hover:bg-gray-300"
} ${className}`}
type={type}
disabled={disabled}
>
{children}
</PlainButton>
);
}

View File

@@ -1,4 +1,5 @@
import PlainButton, { ButtonType } from "./PlainButton"; import PlainButton, { ButtonType } from "./PlainButton";
import { COLORS } from "@/lib/theme/colors";
interface GreenButtonProps { interface GreenButtonProps {
onClick?: () => void; onClick?: () => void;
@@ -18,7 +19,8 @@ export default function GreenButton({
return ( return (
<PlainButton <PlainButton
onClick={onClick} onClick={onClick}
className={`px-4 py-2 bg-[#35786f] text-white rounded-full hover:bg-[#2d5f58] transition-colors text-sm font-medium ${className}`} className={`px-4 py-2 rounded-full transition-colors text-sm font-medium text-white hover:bg-opacity-90 ${className}`}
style={{ backgroundColor: COLORS.primary }}
type={type} type={type}
disabled={disabled} disabled={disabled}
> >

View File

@@ -5,13 +5,15 @@ export default function PlainButton({
className, className,
children, children,
type = "button", type = "button",
disabled disabled,
style
}: { }: {
onClick?: () => void; onClick?: () => void;
className?: string; className?: string;
children?: React.ReactNode; children?: React.ReactNode;
type?: ButtonType; type?: ButtonType;
disabled?: boolean; disabled?: boolean;
style?: React.CSSProperties;
}) { }) {
return ( return (
<button <button
@@ -19,6 +21,7 @@ export default function PlainButton({
className={`px-2 py-1 rounded shadow font-bold hover:cursor-pointer ${className}`} className={`px-2 py-1 rounded shadow font-bold hover:cursor-pointer ${className}`}
type={type} type={type}
disabled={disabled} disabled={disabled}
style={style}
> >
{children} {children}
</button> </button>

14
src/lib/theme/colors.ts Normal file
View File

@@ -0,0 +1,14 @@
/**
* 主题配色常量
* 集中管理应用的品牌颜色
*
* 注意Tailwind CSS 已有的标准颜色gray、red 等)请直接使用 Tailwind 类名
* 这里只定义项目独有的品牌色
*/
export const COLORS = {
// ===== 主色调 =====
/** 主绿色 - 应用主题色,用于页面背景、主要按钮 */
primary: '#35786f',
/** 悬停绿色 - 按钮悬停状态 */
primaryHover: '#2d5f58'
} as const;