...
This commit is contained in:
@@ -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}
|
||||||
@@ -174,13 +180,15 @@ export default function AlphabetCard({ alphabet, alphabetType, onBack }: Alphabe
|
|||||||
<span className="text-2xl md:text-3xl text-gray-400">?</span>
|
<span className="text-2xl md:text-3xl text-gray-400">?</span>
|
||||||
</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}
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -67,7 +71,8 @@ export default function Alphabet() {
|
|||||||
<span>{t("japanese")}</span>
|
<span>{t("japanese")}</span>
|
||||||
</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"
|
||||||
@@ -77,7 +82,8 @@ export default function Alphabet() {
|
|||||||
<span>{t("english")}</span>
|
<span>{t("english")}</span>
|
||||||
</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"
|
||||||
@@ -87,7 +93,8 @@ export default function Alphabet() {
|
|||||||
<span>{t("uyghur")}</span>
|
<span>{t("uyghur")}</span>
|
||||||
</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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
<SaveList show={showSaveList} handleUse={handleUseItem}></SaveList>
|
{/* 保存列表 */}
|
||||||
</>
|
{showSaveList && (
|
||||||
|
<div className="mt-4 border border-gray-200 rounded-2xl overflow-hidden">
|
||||||
|
<SaveList show={showSaveList} handleUse={handleUseItem}></SaveList>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</PageLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -18,7 +17,7 @@ export default function AuthForm({ redirectTo }: AuthFormProps) {
|
|||||||
const [mode, setMode] = useState<'signin' | 'signup'>('signin');
|
const [mode, setMode] = useState<'signin' | 'signup'>('signin');
|
||||||
const [clearSignIn, setClearSignIn] = useState(false);
|
const [clearSignIn, setClearSignIn] = useState(false);
|
||||||
const [clearSignUp, setClearSignUp] = useState(false);
|
const [clearSignUp, setClearSignUp] = useState(false);
|
||||||
|
|
||||||
const [signInState, signInActionForm, isSignInPending] = useActionState(
|
const [signInState, signInActionForm, isSignInPending] = useActionState(
|
||||||
async (prevState: SignUpState | undefined, formData: FormData) => {
|
async (prevState: SignUpState | undefined, formData: FormData) => {
|
||||||
if (clearSignIn) {
|
if (clearSignIn) {
|
||||||
@@ -44,7 +43,7 @@ export default function AuthForm({ redirectTo }: AuthFormProps) {
|
|||||||
|
|
||||||
const validateForm = (formData: FormData): boolean => {
|
const validateForm = (formData: FormData): boolean => {
|
||||||
const newErrors: Record<string, string> = {};
|
const newErrors: Record<string, string> = {};
|
||||||
|
|
||||||
const email = formData.get("email") as string;
|
const email = formData.get("email") as string;
|
||||||
const password = formData.get("password") as string;
|
const password = formData.get("password") as string;
|
||||||
const name = formData.get("name") as string;
|
const name = formData.get("name") as string;
|
||||||
@@ -66,7 +65,7 @@ export default function AuthForm({ redirectTo }: AuthFormProps) {
|
|||||||
if (!name) {
|
if (!name) {
|
||||||
newErrors.name = t("nameRequired");
|
newErrors.name = t("nameRequired");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!confirmPassword) {
|
if (!confirmPassword) {
|
||||||
newErrors.confirmPassword = t("confirmPasswordRequired");
|
newErrors.confirmPassword = t("confirmPasswordRequired");
|
||||||
} else if (password !== confirmPassword) {
|
} else if (password !== confirmPassword) {
|
||||||
@@ -81,17 +80,17 @@ export default function AuthForm({ redirectTo }: AuthFormProps) {
|
|||||||
const handleFormSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
const handleFormSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const formData = new FormData(e.currentTarget);
|
const formData = new FormData(e.currentTarget);
|
||||||
|
|
||||||
// 基本客户端验证
|
// 基本客户端验证
|
||||||
if (!validateForm(formData)) {
|
if (!validateForm(formData)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加 redirectTo 到 formData
|
// 添加 redirectTo 到 formData
|
||||||
if (redirectTo) {
|
if (redirectTo) {
|
||||||
formData.append("redirectTo", redirectTo);
|
formData.append("redirectTo", redirectTo);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用 startTransition 包装 action 调用
|
// 使用 startTransition 包装 action 调用
|
||||||
startTransition(() => {
|
startTransition(() => {
|
||||||
// 根据模式调用相应的 action
|
// 根据模式调用相应的 action
|
||||||
@@ -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,18 +195,21 @@ 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' : ''}`}
|
||||||
>
|
>
|
||||||
{isSignInPending || isSignUpPending
|
{isSignInPending || isSignUpPending
|
||||||
? 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"
|
||||||
@@ -234,7 +247,7 @@ export default function AuthForm({ redirectTo }: AuthFormProps) {
|
|||||||
}}
|
}}
|
||||||
className="text-[#35786f] hover:underline"
|
className="text-[#35786f] hover:underline"
|
||||||
>
|
>
|
||||||
{mode === 'signin'
|
{mode === 'signin'
|
||||||
? `${t("noAccount")} ${t("signUp")}`
|
? `${t("noAccount")} ${t("signUp")}`
|
||||||
: `${t("hasAccount")} ${t("signIn")}`
|
: `${t("hasAccount")} ${t("signIn")}`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* PageHeader - 页面标题组件
|
||||||
|
*
|
||||||
|
* 用于 PageLayout 内的页面标题,支持主标题和可选的副标题
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```tsx
|
||||||
|
* <PageHeader title="我的文件夹" subtitle="管理和组织你的学习内容" />
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
interface PageHeaderProps {
|
interface PageHeaderProps {
|
||||||
|
/** 页面主标题 */
|
||||||
title: string;
|
title: string;
|
||||||
|
/** 可选的副标题/描述 */
|
||||||
subtitle?: string;
|
subtitle?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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
14
src/lib/theme/colors.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
/**
|
||||||
|
* 主题配色常量
|
||||||
|
* 集中管理应用的品牌颜色
|
||||||
|
*
|
||||||
|
* 注意:Tailwind CSS 已有的标准颜色(gray、red 等)请直接使用 Tailwind 类名
|
||||||
|
* 这里只定义项目独有的品牌色
|
||||||
|
*/
|
||||||
|
export const COLORS = {
|
||||||
|
// ===== 主色调 =====
|
||||||
|
/** 主绿色 - 应用主题色,用于页面背景、主要按钮 */
|
||||||
|
primary: '#35786f',
|
||||||
|
/** 悬停绿色 - 按钮悬停状态 */
|
||||||
|
primaryHover: '#2d5f58'
|
||||||
|
} as const;
|
||||||
Reference in New Issue
Block a user