diff --git a/src/app/(features)/alphabet/AlphabetCard.tsx b/src/app/(features)/alphabet/AlphabetCard.tsx index 637a120..e985c64 100644 --- a/src/app/(features)/alphabet/AlphabetCard.tsx +++ b/src/app/(features)/alphabet/AlphabetCard.tsx @@ -99,7 +99,7 @@ export default function AlphabetCard({ alphabet, alphabetType, onBack }: Alphabe return (
- {/* 返回按钮 */} + {/* 右上角返回按钮 */}
- {/* 主卡片 */} + {/* 白色主卡片容器 */}
- {/* 进度指示器 */} + {/* 顶部进度指示器和显示选项按钮 */}
+ {/* 当前字母进度 */} {currentIndex + 1} / {alphabet.length} + {/* 显示选项切换按钮组 */}
+ {/* IPA 音标显示切换 */} + {/* 罗马音显示切换(仅日语显示) */} {hasRomanization && ( )} + {/* 随机模式切换 */}
- {/* 字母显示区域 */} + {/* 字母主要内容显示区域 */}
+ {/* 字母本身(可隐藏) */} {showLetter ? (
{currentLetter.letter} @@ -174,13 +180,15 @@ export default function AlphabetCard({ alphabet, alphabetType, onBack }: Alphabe ?
)} - + + {/* IPA 音标显示 */} {showIPA && (
{currentLetter.letter_sound_ipa}
)} - + + {/* 罗马音显示(日语) */} {showRoman && hasRomanization && currentLetter.roman_letter && (
{currentLetter.roman_letter} @@ -188,8 +196,9 @@ export default function AlphabetCard({ alphabet, alphabetType, onBack }: Alphabe )}
- {/* 导航控制 */} + {/* 底部导航控制区域 */}
+ {/* 上一个按钮 */} + {/* 中间区域:随机按钮或进度条 */}
{isRandomMode ? ( + // 随机模式:显示随机切换按钮
- {/* 操作提示 */} + {/* 底部操作提示文字 */}

{isRandomMode @@ -246,7 +260,7 @@ export default function AlphabetCard({ alphabet, alphabetType, onBack }: Alphabe

- {/* 触摸事件处理 */} + {/* 全屏触摸事件监听层(用于滑动切换) */}
+ {/* 页面标题 */}

{t("chooseCharacters")}

+ {/* 副标题说明 */}

选择一种语言的字母表开始学习

- + + {/* 语言选择按钮网格 */}
+ {/* 日语假名选项 */} setChosenAlphabet("japanese")} className="p-6 text-lg font-medium hover:scale-105 transition-transform" @@ -67,7 +71,8 @@ export default function Alphabet() { {t("japanese")}
- + + {/* 英语字母选项 */} setChosenAlphabet("english")} className="p-6 text-lg font-medium hover:scale-105 transition-transform" @@ -77,7 +82,8 @@ export default function Alphabet() { {t("english")}
- + + {/* 维吾尔语字母选项 */} setChosenAlphabet("uyghur")} className="p-6 text-lg font-medium hover:scale-105 transition-transform" @@ -87,7 +93,8 @@ export default function Alphabet() { {t("uyghur")}
- + + {/* 世界语字母选项 */} setChosenAlphabet("esperanto")} className="p-6 text-lg font-medium hover:scale-105 transition-transform" diff --git a/src/app/(features)/memorize/FolderSelector.tsx b/src/app/(features)/memorize/FolderSelector.tsx index f95985d..2a91d62 100644 --- a/src/app/(features)/memorize/FolderSelector.tsx +++ b/src/app/(features)/memorize/FolderSelector.tsx @@ -19,6 +19,7 @@ const FolderSelector: React.FC = ({ folders }) => {
{folders.length === 0 ? ( + // 空状态 - 显示提示和跳转按钮

{t("noFolders")} @@ -32,9 +33,11 @@ const FolderSelector: React.FC = ({ folders }) => {

) : ( <> + {/* 页面标题 */}

{t("selectFolder")}

+ {/* 文件夹列表 */}
{folders .toSorted((a, b) => a.id - b.id) @@ -46,9 +49,11 @@ const FolderSelector: React.FC = ({ 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" > + {/* 文件夹图标 */}
+ {/* 文件夹信息 */}
{folder.name} @@ -61,6 +66,7 @@ const FolderSelector: React.FC = ({ folders }) => { })}
+ {/* 右箭头 */}
- {t("previous")} - + - {t("next")} - + - {t("restart")} - + - {t("autoPause", { enabled: autoPause ? t("on") : t("off") })} - +
); } \ No newline at end of file diff --git a/src/app/(features)/srt-player/components/compounds/UploadZone.tsx b/src/app/(features)/srt-player/components/compounds/UploadZone.tsx index 9286cd7..fb9a694 100644 --- a/src/app/(features)/srt-player/components/compounds/UploadZone.tsx +++ b/src/app/(features)/srt-player/components/compounds/UploadZone.tsx @@ -4,7 +4,7 @@ import React from "react"; import { useTranslations } from "next-intl"; import { toast } from "sonner"; 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 { useFileUpload } from "../../hooks/useFileUpload"; @@ -26,21 +26,21 @@ export default function UploadZone({ onVideoUpload, onSubtitleUpload, className return (
- + - {t("uploadSubtitle")} - +
); } \ No newline at end of file diff --git a/src/app/(features)/srt-player/page.tsx b/src/app/(features)/srt-player/page.tsx index ba802ae..9f82083 100644 --- a/src/app/(features)/srt-player/page.tsx +++ b/src/app/(features)/srt-player/page.tsx @@ -14,7 +14,7 @@ import SubtitleArea from "./components/compounds/SubtitleArea"; import ControlBar from "./components/compounds/ControlBar"; import UploadZone from "./components/compounds/UploadZone"; import SeekBar from "./components/atoms/SeekBar"; -import DarkButton from "@/components/ui/buttons/DarkButton"; +import LightButton from "@/components/ui/buttons/LightButton"; export default function SrtPlayerPage() { const t = useTranslations("home"); @@ -182,13 +182,13 @@ export default function SrtPlayerPage() {

- {state.video.url ? srtT("uploaded") : srtT("upload")} - +
@@ -206,13 +206,13 @@ export default function SrtPlayerPage() {

- {state.subtitle.url ? srtT("uploaded") : srtT("upload")} - + diff --git a/src/app/(features)/text-speaker/page.tsx b/src/app/(features)/text-speaker/page.tsx index d6a9136..6ac9e94 100644 --- a/src/app/(features)/text-speaker/page.tsx +++ b/src/app/(features)/text-speaker/page.tsx @@ -17,6 +17,7 @@ import { useTranslations } from "next-intl"; import { getLocalStorageOperator } from "@/lib/browser/localStorageOperators"; import { getTTSAudioUrl } from "@/lib/browser/tts"; import { genIPA, genLocale } from "@/lib/server/translatorActions"; +import PageLayout from "@/components/ui/PageLayout"; export default function TextSpeakerPage() { const t = useTranslations("text_speaker"); @@ -225,24 +226,30 @@ export default function TextSpeakerPage() { }; return ( - <> + + {/* 文本输入区域 */}
+ {/* 文本输入框 */} + {/* IPA 显示区域 */} {(ipa.length !== 0 && ( -
+
{ipa}
)) ||
} -
+ + {/* 控制按钮区域 */} +
+ {/* 速度调节面板 */} {showSpeedAdjust && ( -
+
)} + {/* 播放/暂停按钮 */} + {/* 自动暂停按钮 */} { @@ -299,6 +308,7 @@ export default function TextSpeakerPage() { src={autopause ? IMAGES.autoplay : IMAGES.autopause} alt="autoplayorpause" > + {/* 速度调节按钮 */} setShowSpeedAdjust(!showSpeedAdjust)} @@ -306,6 +316,7 @@ export default function TextSpeakerPage() { alt="speed" className={`${showSpeedAdjust ? "bg-gray-200" : ""}`} > + {/* 保存按钮 */} + {/* 功能开关按钮 */}
- - + {/* 保存列表 */} + {showSaveList && ( +
+ +
+ )} + ); } diff --git a/src/app/auth/AuthForm.tsx b/src/app/auth/AuthForm.tsx index 3b61829..030a44d 100644 --- a/src/app/auth/AuthForm.tsx +++ b/src/app/auth/AuthForm.tsx @@ -6,7 +6,6 @@ import { signInAction, signUpAction, SignUpState } from "@/lib/actions/auth"; import Container from "@/components/ui/Container"; import Input from "@/components/ui/Input"; import LightButton from "@/components/ui/buttons/LightButton"; -import DarkButton from "@/components/ui/buttons/DarkButton"; import { authClient } from "@/lib/auth-client"; interface AuthFormProps { @@ -18,7 +17,7 @@ export default function AuthForm({ redirectTo }: AuthFormProps) { const [mode, setMode] = useState<'signin' | 'signup'>('signin'); const [clearSignIn, setClearSignIn] = useState(false); const [clearSignUp, setClearSignUp] = useState(false); - + const [signInState, signInActionForm, isSignInPending] = useActionState( async (prevState: SignUpState | undefined, formData: FormData) => { if (clearSignIn) { @@ -44,7 +43,7 @@ export default function AuthForm({ redirectTo }: AuthFormProps) { const validateForm = (formData: FormData): boolean => { const newErrors: Record = {}; - + const email = formData.get("email") as string; const password = formData.get("password") as string; const name = formData.get("name") as string; @@ -66,7 +65,7 @@ export default function AuthForm({ redirectTo }: AuthFormProps) { if (!name) { newErrors.name = t("nameRequired"); } - + if (!confirmPassword) { newErrors.confirmPassword = t("confirmPasswordRequired"); } else if (password !== confirmPassword) { @@ -81,17 +80,17 @@ export default function AuthForm({ redirectTo }: AuthFormProps) { const handleFormSubmit = (e: React.FormEvent) => { e.preventDefault(); const formData = new FormData(e.currentTarget); - + // 基本客户端验证 if (!validateForm(formData)) { return; } - + // 添加 redirectTo 到 formData if (redirectTo) { formData.append("redirectTo", redirectTo); } - + // 使用 startTransition 包装 action 调用 startTransition(() => { // 根据模式调用相应的 action @@ -115,17 +114,21 @@ export default function AuthForm({ redirectTo }: AuthFormProps) { return (
+ {/* 页面标题 */}

{t(mode === 'signin' ? 'signIn' : 'signUp')}

+ {/* 服务器端错误提示 */} {currentError?.message && (
{currentError.message}
)} + {/* 登录/注册表单 */}
+ {/* 用户名输入(仅注册模式显示) */} {mode === 'signup' && (
+ {/* 客户端验证错误 */} {errors.name && (

{errors.name}

)} + {/* 服务器端验证错误 */} {currentError?.errors?.username && (

{currentError.errors.username[0]}

)}
)} + {/* 邮箱输入 */}
+ {/* 密码输入 */}
+ {/* 确认密码输入(仅注册模式显示) */} {mode === 'signup' && (
)} - - {isSignInPending || isSignUpPending - ? t("loading") + {isSignInPending || isSignUpPending + ? t("loading") : t(mode === 'signin' ? 'signInButton' : 'signUpButton') } - + + {/* 第三方登录区域 */}
+ {/* 分隔线 */}
@@ -208,6 +219,7 @@ export default function AuthForm({ redirectTo }: AuthFormProps) {
+ {/* GitHub 登录按钮 */}
+ {/* 模式切换链接 */}
+ {/* 文件夹列表 */}
{folders.length === 0 ? ( + // 空状态
@@ -151,6 +154,7 @@ export default function FoldersClient({ userId }: { userId: string }) {

{t("noFoldersYet")}

) : ( + // 文件夹卡片列表
{folders .toSorted((a, b) => a.id - b.id) diff --git a/src/app/folders/[folder_id]/InFolder.tsx b/src/app/folders/[folder_id]/InFolder.tsx index fa6d010..fa5b9ce 100644 --- a/src/app/folders/[folder_id]/InFolder.tsx +++ b/src/app/folders/[folder_id]/InFolder.tsx @@ -57,7 +57,9 @@ export default function InFolder({ folderId }: { folderId: number }) { return ( + {/* 顶部导航和标题栏 */}
+ {/* 返回按钮 */} + {/* 页面标题和操作按钮 */}
+ {/* 标题区域 */}

{t("textPairs")} @@ -76,6 +80,7 @@ export default function InFolder({ folderId }: { folderId: number }) {

+ {/* 操作按钮区域 */}
{ @@ -94,17 +99,21 @@ export default function InFolder({ folderId }: { folderId: number }) {
+ {/* 文本对列表 */} {loading ? ( + // 加载状态

{t("loadingTextPairs")}

) : textPairs.length === 0 ? ( + // 空状态

{t("noTextPairs")}

) : ( + // 文本对卡片列表
{textPairs .toSorted((a, b) => a.id - b.id) @@ -123,6 +132,7 @@ export default function InFolder({ folderId }: { folderId: number }) { )} + {/* 添加文本对模态框 */} setAddModal(false)} diff --git a/src/app/profile/page.tsx b/src/app/profile/page.tsx index cb74811..c7d4bc3 100644 --- a/src/app/profile/page.tsx +++ b/src/app/profile/page.tsx @@ -1,6 +1,6 @@ import Image from "next/image"; -import { Center } from "@/components/common/Center"; -import Container from "@/components/ui/Container"; +import PageLayout from "@/components/ui/PageLayout"; +import PageHeader from "@/components/ui/PageHeader"; import { auth } from "@/auth"; import { getTranslations } from "next-intl/server"; import { redirect } from "next/navigation"; @@ -16,25 +16,34 @@ export default async function ProfilePage() { redirect("/auth?redirect=/profile"); } - console.log(JSON.stringify(session, null, 2)); - return ( -
- -

{t("myProfile")}

+ + + + {/* 用户信息区域 */} +
+ {/* 用户头像 */} {session.user.image && ( User Avatar + className="rounded-full" + /> )} -

{session.user.name}

-

{t("email", { email: session.user.email })}

+ + {/* 用户名和邮箱 */} +
+

+ {session.user.name} +

+

{t("email", { email: session.user.email })}

+
+ + {/* 登出按钮 */} - -
+
+ ); } diff --git a/src/components/ui/CardList.tsx b/src/components/ui/CardList.tsx index e177436..40b1e35 100644 --- a/src/components/ui/CardList.tsx +++ b/src/components/ui/CardList.tsx @@ -1,5 +1,23 @@ +/** + * CardList - 可滚动的卡片列表容器 + * + * 用于显示可滚动的列表内容,如文件夹列表、文本对列表等 + * - 最大高度 96 (24rem) + * - 垂直滚动 + * - 圆角边框 + * + * @example + * ```tsx + * + * {items.map(item => ( + *
{item.name}
+ * ))} + *
+ * ``` + */ interface CardListProps { children: React.ReactNode; + /** 额外的 CSS 类名 */ className?: string; } diff --git a/src/components/ui/PageHeader.tsx b/src/components/ui/PageHeader.tsx index 47abe5f..c9a05b6 100644 --- a/src/components/ui/PageHeader.tsx +++ b/src/components/ui/PageHeader.tsx @@ -1,5 +1,17 @@ +/** + * PageHeader - 页面标题组件 + * + * 用于 PageLayout 内的页面标题,支持主标题和可选的副标题 + * + * @example + * ```tsx + * + * ``` + */ interface PageHeaderProps { + /** 页面主标题 */ title: string; + /** 可选的副标题/描述 */ subtitle?: string; } diff --git a/src/components/ui/PageLayout.tsx b/src/components/ui/PageLayout.tsx index d9735c2..aa18d47 100644 --- a/src/components/ui/PageLayout.tsx +++ b/src/components/ui/PageLayout.tsx @@ -1,5 +1,22 @@ +/** + * PageLayout - 统一的页面布局组件 + * + * 提供应用统一的标准页面布局: + * - 绿色背景 (#35786f) + * - 居中的白色圆角卡片 + * - 响应式内边距 + * + * @example + * ```tsx + * + * + *
页面内容
+ *
+ * ``` + */ interface PageLayoutProps { children: React.ReactNode; + /** 额外的 CSS 类名,用于自定义布局行为 */ className?: string; } diff --git a/src/components/ui/buttons/DarkButton.tsx b/src/components/ui/buttons/DarkButton.tsx deleted file mode 100644 index 163d01f..0000000 --- a/src/components/ui/buttons/DarkButton.tsx +++ /dev/null @@ -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 ( - - {children} - - ); -} diff --git a/src/components/ui/buttons/GrayButton.tsx b/src/components/ui/buttons/GrayButton.tsx deleted file mode 100644 index 1af16dc..0000000 --- a/src/components/ui/buttons/GrayButton.tsx +++ /dev/null @@ -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 ( - - {children} - - ); -} diff --git a/src/components/ui/buttons/GreenButton.tsx b/src/components/ui/buttons/GreenButton.tsx index 48e327f..818909f 100644 --- a/src/components/ui/buttons/GreenButton.tsx +++ b/src/components/ui/buttons/GreenButton.tsx @@ -1,4 +1,5 @@ import PlainButton, { ButtonType } from "./PlainButton"; +import { COLORS } from "@/lib/theme/colors"; interface GreenButtonProps { onClick?: () => void; @@ -18,7 +19,8 @@ export default function GreenButton({ return ( diff --git a/src/components/ui/buttons/PlainButton.tsx b/src/components/ui/buttons/PlainButton.tsx index 12042de..c4e2d59 100644 --- a/src/components/ui/buttons/PlainButton.tsx +++ b/src/components/ui/buttons/PlainButton.tsx @@ -5,13 +5,15 @@ export default function PlainButton({ className, children, type = "button", - disabled + disabled, + style }: { onClick?: () => void; className?: string; children?: React.ReactNode; type?: ButtonType; disabled?: boolean; + style?: React.CSSProperties; }) { return ( diff --git a/src/lib/theme/colors.ts b/src/lib/theme/colors.ts new file mode 100644 index 0000000..33eacfe --- /dev/null +++ b/src/lib/theme/colors.ts @@ -0,0 +1,14 @@ +/** + * 主题配色常量 + * 集中管理应用的品牌颜色 + * + * 注意:Tailwind CSS 已有的标准颜色(gray、red 等)请直接使用 Tailwind 类名 + * 这里只定义项目独有的品牌色 + */ +export const COLORS = { + // ===== 主色调 ===== + /** 主绿色 - 应用主题色,用于页面背景、主要按钮 */ + primary: '#35786f', + /** 悬停绿色 - 按钮悬停状态 */ + primaryHover: '#2d5f58' +} as const;