This commit is contained in:
2026-02-06 04:01:41 +08:00
parent 6c7095ffb3
commit 058ecf7e39
11 changed files with 127 additions and 112 deletions

View File

@@ -3,7 +3,7 @@
import { useState, useEffect, useCallback } from "react"; import { useState, useEffect, useCallback } from "react";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { Letter, SupportedAlphabets } from "@/lib/interfaces"; import { Letter, SupportedAlphabets } from "@/lib/interfaces";
import { IconClick } from "@/components/ui/buttons"; import { IconClick, CircleToggleButton, CircleButton } from "@/components/ui/buttons";
import { IMAGES } from "@/config/images"; import { IMAGES } from "@/config/images";
import { ChevronLeft, ChevronRight } from "lucide-react"; import { ChevronLeft, ChevronRight } from "lucide-react";
@@ -120,51 +120,35 @@ export function AlphabetCard({ alphabet, alphabetType, onBack }: AlphabetCardPro
</span> </span>
{/* 显示选项切换按钮组 */} {/* 显示选项切换按钮组 */}
<div className="flex gap-2 flex-wrap"> <div className="flex gap-2 flex-wrap">
<button <CircleToggleButton
selected={showLetter}
onClick={() => setShowLetter(!showLetter)} onClick={() => setShowLetter(!showLetter)}
className={`px-3 py-1 rounded-full text-sm transition-colors ${
showLetter
? "bg-[#35786f] text-white"
: "bg-gray-200 text-gray-600"
}`}
> >
{t("letter")} {t("letter")}
</button> </CircleToggleButton>
{/* IPA 音标显示切换 */} {/* IPA 音标显示切换 */}
<button <CircleToggleButton
selected={showIPA}
onClick={() => setShowIPA(!showIPA)} onClick={() => setShowIPA(!showIPA)}
className={`px-3 py-1 rounded-full text-sm transition-colors ${
showIPA
? "bg-[#35786f] text-white"
: "bg-gray-200 text-gray-600"
}`}
> >
IPA IPA
</button> </CircleToggleButton>
{/* 罗马音显示切换(仅日语显示) */} {/* 罗马音显示切换(仅日语显示) */}
{hasRomanization && ( {hasRomanization && (
<button <CircleToggleButton
selected={showRoman}
onClick={() => setShowRoman(!showRoman)} onClick={() => setShowRoman(!showRoman)}
className={`px-3 py-1 rounded-full text-sm transition-colors ${
showRoman
? "bg-[#35786f] text-white"
: "bg-gray-200 text-gray-600"
}`}
> >
{t("roman")} {t("roman")}
</button> </CircleToggleButton>
)} )}
{/* 随机模式切换 */} {/* 随机模式切换 */}
<button <CircleToggleButton
selected={isRandomMode}
onClick={() => setIsRandomMode(!isRandomMode)} onClick={() => setIsRandomMode(!isRandomMode)}
className={`px-3 py-1 rounded-full text-sm transition-colors ${
isRandomMode
? "bg-[#35786f] text-white"
: "bg-gray-200 text-gray-600"
}`}
> >
{t("random")} {t("random")}
</button> </CircleToggleButton>
</div> </div>
</div> </div>
@@ -199,13 +183,9 @@ export function AlphabetCard({ alphabet, alphabetType, onBack }: AlphabetCardPro
{/* 底部导航控制区域 */} {/* 底部导航控制区域 */}
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
{/* 上一个按钮 */} {/* 上一个按钮 */}
<button <CircleButton onClick={goToPrevious} aria-label="上一个字母">
onClick={goToPrevious}
className="p-3 rounded-full bg-gray-100 hover:bg-gray-200 transition-colors"
aria-label="上一个字母"
>
<ChevronLeft size={24} /> <ChevronLeft size={24} />
</button> </CircleButton>
{/* 中间区域:随机按钮 */} {/* 中间区域:随机按钮 */}
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">
@@ -220,13 +200,9 @@ export function AlphabetCard({ alphabet, alphabetType, onBack }: AlphabetCardPro
</div> </div>
{/* 下一个按钮 */} {/* 下一个按钮 */}
<button <CircleButton onClick={goToNext} aria-label="下一个字母">
onClick={goToNext}
className="p-3 rounded-full bg-gray-100 hover:bg-gray-200 transition-colors"
aria-label="下一个字母"
>
<ChevronRight size={24} /> <ChevronRight size={24} />
</button> </CircleButton>
</div> </div>
</div> </div>

View File

@@ -1,6 +1,7 @@
"use client"; "use client";
import { Plus, RefreshCw } from "lucide-react"; import { Plus, RefreshCw } from "lucide-react";
import { CircleButton, LightButton } from "@/components/ui/buttons";
import { toast } from "sonner"; import { toast } from "sonner";
import { actionCreatePair } from "@/modules/folder/folder-aciton"; import { actionCreatePair } from "@/modules/folder/folder-aciton";
import { TSharedItem } from "@/shared/dictionary-type"; import { TSharedItem } from "@/shared/dictionary-type";
@@ -61,13 +62,13 @@ export function SaveButtonClient({ session, folders, searchResult, queryLang, de
}; };
return ( return (
<button <CircleButton
onClick={handleSave} onClick={handleSave}
className="hover:bg-gray-200 hover:cursor-pointer rounded-4xl border border-gray-200 w-10 h-10 flex justify-center items-center shrink-0" className="w-10 h-10 shrink-0"
title="Save to folder" title="Save to folder"
> >
<Plus /> <Plus />
</button> </CircleButton>
); );
} }
@@ -110,12 +111,12 @@ export function ReLookupButtonClient({ searchQuery, queryLang, definitionLang }:
}; };
return ( return (
<button <LightButton
onClick={handleRelookup} onClick={handleRelookup}
className="flex items-center gap-2 px-4 py-2 text-sm text-gray-700 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors" className="flex items-center gap-2 px-4 py-2 text-sm"
leftIcon={<RefreshCw className="w-4 h-4" />}
> >
<RefreshCw className="w-4 h-4" />
Re-lookup Re-lookup
</button> </LightButton>
); );
} }

View File

@@ -1,6 +1,7 @@
"use client"; "use client";
import { useState } from "react"; import { useState } from "react";
import { LinkButton, CircleToggleButton, LightButton } from "@/components/ui/buttons";
import { useAudioPlayer } from "@/hooks/useAudioPlayer"; import { useAudioPlayer } from "@/hooks/useAudioPlayer";
import { getTTSUrl, TTS_SUPPORTED_LANGUAGES } from "@/lib/bigmodel/tts"; import { getTTSUrl, TTS_SUPPORTED_LANGUAGES } from "@/lib/bigmodel/tts";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
@@ -117,12 +118,9 @@ const Memorize: React.FC<MemorizeProps> = ({ textPairs }) => {
<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">
{/* 进度指示器 */} {/* 进度指示器 */}
<div className="flex justify-center mb-4"> <div className="flex justify-center mb-4">
<button <LinkButton onClick={handleIndexClick} className="text-sm">
onClick={handleIndexClick}
className="text-sm text-gray-500 hover:text-gray-700 transition-colors"
>
{index + 1} / {getTextPairs().length} {index + 1} / {getTextPairs().length}
</button> </LinkButton>
</div> </div>
{/* 文本显示区域 */} {/* 文本显示区域 */}
@@ -162,45 +160,36 @@ const Memorize: React.FC<MemorizeProps> = ({ textPairs }) => {
{/* 底部按钮 */} {/* 底部按钮 */}
<div className="flex flex-row gap-2 items-center justify-center flex-wrap"> <div className="flex flex-row gap-2 items-center justify-center flex-wrap">
<button <LightButton
onClick={handleNext} onClick={handleNext}
className="px-4 py-2 rounded-full bg-gray-200 text-gray-700 hover:bg-gray-300 transition-colors text-sm" className="px-4 py-2 rounded-full text-sm"
> >
{show === "question" ? t("answer") : t("next")} {show === "question" ? t("answer") : t("next")}
</button> </LightButton>
<button <LightButton
onClick={handlePrevious} onClick={handlePrevious}
className="px-4 py-2 rounded-full bg-gray-200 text-gray-700 hover:bg-gray-300 transition-colors text-sm" className="px-4 py-2 rounded-full text-sm"
> >
{t("previous")} {t("previous")}
</button> </LightButton>
<button <CircleToggleButton
selected={reverse}
onClick={toggleReverse} onClick={toggleReverse}
className={`px-4 py-2 rounded-full transition-colors text-sm ${reverse
? "bg-[#35786f] text-white hover:bg-[#2d5f58]"
: "bg-gray-200 text-gray-700 hover:bg-gray-300"
}`}
> >
{t("reverse")} {t("reverse")}
</button> </CircleToggleButton>
<button <CircleToggleButton
selected={dictation}
onClick={toggleDictation} onClick={toggleDictation}
className={`px-4 py-2 rounded-full transition-colors text-sm ${dictation
? "bg-[#35786f] text-white hover:bg-[#2d5f58]"
: "bg-gray-200 text-gray-700 hover:bg-gray-300"
}`}
> >
{t("dictation")} {t("dictation")}
</button> </CircleToggleButton>
<button <CircleToggleButton
selected={disorder}
onClick={toggleDisorder} onClick={toggleDisorder}
className={`px-4 py-2 rounded-full transition-colors text-sm ${disorder
? "bg-[#35786f] text-white hover:bg-[#2d5f58]"
: "bg-gray-200 text-gray-700 hover:bg-gray-300"
}`}
> >
{t("disorder")} {t("disorder")}
</button> </CircleToggleButton>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,6 +1,7 @@
"use client"; "use client";
import React, { useRef } from "react"; import React, { useRef } from "react";
import { Button } from "@/components/ui/Button";
import { FileInputProps } from "../../types/controls"; import { FileInputProps } from "../../types/controls";
interface FileInputComponentProps extends FileInputProps { interface FileInputComponentProps extends FileInputProps {
@@ -33,13 +34,15 @@ export function FileInput({ accept, onFileSelect, disabled, className, children
disabled={disabled} disabled={disabled}
className="hidden" className="hidden"
/> />
<button <Button
onClick={handleClick} onClick={handleClick}
disabled={disabled} disabled={disabled}
className={`px-2 py-1 rounded shadow font-bold hover:cursor-pointer hover:bg-gray-200 text-gray-800 bg-white ${disabled ? 'opacity-50 cursor-not-allowed' : ''} ${className || ''}`} variant="secondary"
size="sm"
className={className}
> >
{children} {children}
</button> </Button>
</> </>
); );
} }

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { LightButton } from "@/components/ui/buttons"; import { LightButton, GreenButton } from "@/components/ui/buttons";
import { IconClick } from "@/components/ui/buttons"; import { IconClick } from "@/components/ui/buttons";
import { IMAGES } from "@/config/images"; import { IMAGES } from "@/config/images";
import { useAudioPlayer } from "@/hooks/useAudioPlayer"; import { useAudioPlayer } from "@/hooks/useAudioPlayer";
@@ -210,12 +210,13 @@ export default function TranslatorPage() {
{/* TranslateButton Component */} {/* TranslateButton Component */}
<div className="w-screen flex justify-center items-center"> <div className="w-screen flex justify-center items-center">
<button <GreenButton
className={`duration-150 ease-in text-xl font-extrabold border rounded-4xl p-3 border-gray-200 h-16 ${processing ? "bg-gray-200" : "bg-white hover:bg-gray-200 hover:cursor-pointer"}`}
onClick={translate} onClick={translate}
disabled={processing}
className="text-xl h-16 px-8"
> >
{t("translate")} {t("translate")}
</button> </GreenButton>
</div> </div>
</> </>
); );

View File

@@ -4,7 +4,7 @@ import { useState, useActionState, startTransition } from "react";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
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"; import { LightButton, LinkButton } from "@/components/ui/buttons";
import { authClient } from "@/lib/auth-client"; import { authClient } from "@/lib/auth-client";
import { actionSignIn, actionSignUp, ActionOutputAuth } from "@/modules/auth/auth-action"; import { actionSignIn, actionSignUp, ActionOutputAuth } from "@/modules/auth/auth-action";
@@ -262,7 +262,7 @@ export function AuthForm({ redirectTo }: AuthFormProps) {
{/* 模式切换链接 */} {/* 模式切换链接 */}
<div className="mt-6 text-center"> <div className="mt-6 text-center">
<button <LinkButton
type="button" type="button"
onClick={() => { onClick={() => {
setMode(mode === 'signin' ? 'signup' : 'signin'); setMode(mode === 'signin' ? 'signup' : 'signin');
@@ -274,13 +274,12 @@ export function AuthForm({ redirectTo }: AuthFormProps) {
setClearSignUp(true); setClearSignUp(true);
} }
}} }}
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")}`
} }
</button> </LinkButton>
</div> </div>
</Container> </Container>
</div> </div>

View File

@@ -7,6 +7,7 @@ import {
FolderPlus, FolderPlus,
Trash2, Trash2,
} from "lucide-react"; } from "lucide-react";
import { CircleButton, DashedButton } from "@/components/ui/buttons";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
@@ -51,8 +52,8 @@ const FolderCard = ({ folder, refresh }: FolderProps) => {
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button <CircleButton
onClick={(e) => { onClick={(e: React.MouseEvent) => {
e.stopPropagation(); e.stopPropagation();
const newName = prompt("Input a new name.")?.trim(); const newName = prompt("Input a new name.")?.trim();
if (newName && newName.length > 0) { if (newName && newName.length > 0) {
@@ -67,12 +68,11 @@ const FolderCard = ({ folder, refresh }: FolderProps) => {
}); });
} }
}} }}
className="p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
> >
<FolderPen size={16} /> <FolderPen size={16} />
</button> </CircleButton>
<button <CircleButton
onClick={(e) => { onClick={(e: React.MouseEvent) => {
e.stopPropagation(); e.stopPropagation();
const confirm = prompt(t("confirmDelete", { name: folder.name })); const confirm = prompt(t("confirmDelete", { name: folder.name }));
if (confirm === folder.name) { if (confirm === folder.name) {
@@ -87,10 +87,10 @@ const FolderCard = ({ folder, refresh }: FolderProps) => {
}); });
} }
}} }}
className="p-2 text-gray-400 hover:text-red-500 hover:bg-red-50 rounded-lg transition-colors" className="text-gray-400 hover:text-red-500 hover:bg-red-50"
> >
<Trash2 size={16} /> <Trash2 size={16} />
</button> </CircleButton>
<ChevronRight size={18} className="text-gray-400" /> <ChevronRight size={18} className="text-gray-400" />
</div> </div>
</div> </div>
@@ -135,7 +135,7 @@ export function FoldersClient({ userId }: { userId: string; }) {
<PageHeader title={t("title")} subtitle={t("subtitle")} /> <PageHeader title={t("title")} subtitle={t("subtitle")} />
{/* 新建文件夹按钮 */} {/* 新建文件夹按钮 */}
<button <DashedButton
onClick={async () => { onClick={async () => {
const folderName = prompt(t("enterFolderName")); const folderName = prompt(t("enterFolderName"));
if (!folderName) return; if (!folderName) return;
@@ -154,11 +154,11 @@ export function FoldersClient({ userId }: { userId: string; }) {
} }
}} }}
disabled={loading} disabled={loading}
className="w-full p-3 border-2 border-dashed border-gray-300 rounded-xl text-gray-500 hover:border-gray-400 hover:text-gray-600 transition-colors flex items-center justify-center gap-2" className="w-full"
> >
<FolderPlus size={18} /> <FolderPlus size={18} />
<span>{loading ? t("creating") : t("newFolder")}</span> <span>{loading ? t("creating") : t("newFolder")}</span>
</button> </DashedButton>
{/* 文件夹列表 */} {/* 文件夹列表 */}
<div className="mt-4"> <div className="mt-4">

View File

@@ -7,8 +7,7 @@ import { AddTextPairModal } from "./AddTextPairModal";
import { TextPairCard } from "./TextPairCard"; import { TextPairCard } from "./TextPairCard";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { PageLayout } from "@/components/ui/PageLayout"; import { PageLayout } from "@/components/ui/PageLayout";
import { GreenButton } from "@/components/ui/buttons"; import { GreenButton, IconButton, LinkButton } from "@/components/ui/buttons";
import { IconButton } from "@/components/ui/buttons";
import { CardList } from "@/components/ui/CardList"; import { CardList } from "@/components/ui/CardList";
import { actionCreatePair, actionDeletePairById, actionGetPairsByFolderId } from "@/modules/folder/folder-aciton"; import { actionCreatePair, actionDeletePairById, actionGetPairsByFolderId } from "@/modules/folder/folder-aciton";
import { TSharedPair } from "@/shared/folder-type"; import { TSharedPair } from "@/shared/folder-type";
@@ -52,13 +51,13 @@ export function InFolder({ folderId, isReadOnly }: { folderId: number; isReadOnl
{/* 顶部导航和标题栏 */} {/* 顶部导航和标题栏 */}
<div className="mb-6"> <div className="mb-6">
{/* 返回按钮 */} {/* 返回按钮 */}
<button <LinkButton
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 mb-4"
> >
<ArrowLeft size={16} /> <ArrowLeft size={16} />
<span className="text-sm">{t("back")}</span> <span className="text-sm">{t("back")}</span>
</button> </LinkButton>
{/* 页面标题和操作按钮 */} {/* 页面标题和操作按钮 */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">

View File

@@ -1,5 +1,6 @@
import { Edit, Trash2 } from "lucide-react"; import { Edit, Trash2 } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
import { CircleButton } from "@/components/ui/buttons";
import { UpdateTextPairModal } from "./UpdateTextPairModal"; import { UpdateTextPairModal } from "./UpdateTextPairModal";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { TSharedPair } from "@/shared/folder-type"; import { TSharedPair } from "@/shared/folder-type";
@@ -39,20 +40,20 @@ export function TextPairCard({
<div className="flex items-center gap-1 opacity-50 group-hover:opacity-100 transition-opacity"> <div className="flex items-center gap-1 opacity-50 group-hover:opacity-100 transition-opacity">
{!isReadOnly && ( {!isReadOnly && (
<> <>
<button <CircleButton
className="p-1.5 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-md transition-colors"
onClick={() => setOpenUpdateModal(true)} onClick={() => setOpenUpdateModal(true)}
title={t("edit")} title={t("edit")}
className="text-gray-400 hover:text-gray-600"
> >
<Edit size={14} /> <Edit size={14} />
</button> </CircleButton>
<button <CircleButton
className="p-1.5 text-gray-400 hover:text-red-500 hover:bg-red-50 rounded-md transition-colors"
onClick={onDel} onClick={onDel}
title={t("delete")} title={t("delete")}
className="text-gray-400 hover:text-red-500 hover:bg-red-50"
> >
<Trash2 size={14} /> <Trash2 size={14} />
</button> </CircleButton>
</> </>
)} )}
</div> </div>

View File

@@ -3,9 +3,8 @@
import React from "react"; import React from "react";
import Link from "next/link"; import Link from "next/link";
import Image from "next/image"; import Image from "next/image";
import { COLORS } from "@/lib/theme/colors";
export type ButtonVariant = "primary" | "secondary" | "ghost" | "icon"; export type ButtonVariant = "primary" | "secondary" | "ghost" | "icon" | "circle" | "dashed" | "link";
export type ButtonSize = "sm" | "md" | "lg"; export type ButtonSize = "sm" | "md" | "lg";
export interface ButtonProps { export interface ButtonProps {
@@ -70,6 +69,24 @@ export function Button({
icon: ` icon: `
p-2 bg-gray-200 rounded-full p-2 bg-gray-200 rounded-full
hover:bg-gray-300 hover:bg-gray-300
`,
circle: `
p-2 rounded-full
hover:bg-gray-200
`,
dashed: `
border-2 border-dashed border-gray-300
text-gray-500
hover:border-gray-400
hover:text-gray-600
bg-transparent
shadow-none
`,
link: `
text-[#35786f]
hover:underline
p-0
shadow-none
` `
}; };

View File

@@ -54,3 +54,32 @@ export const IconClick = (props: any) => {
// PlainButton: 基础小按钮 // PlainButton: 基础小按钮
export const PlainButton = (props: any) => <Button variant="secondary" size="sm" {...props} />; export const PlainButton = (props: any) => <Button variant="secondary" size="sm" {...props} />;
// CircleButton: 圆形导航按钮
export const CircleButton = (props: any) => {
const { icon, className, ...rest } = props;
return <Button variant="circle" leftIcon={icon} className={className} {...rest} />;
};
// DashedButton: 虚线边框按钮
export const DashedButton = (props: any) => <Button variant="dashed" {...props} />;
// LinkButton: 链接样式按钮
export const LinkButton = (props: any) => <Button variant="link" {...props} />;
// CircleToggleButton: 圆形切换按钮(支持 selected 状态)
export const CircleToggleButton = (props: any) => {
const { selected, className, children, ...rest } = props;
const selectedClass = selected
? "bg-[#35786f] text-white"
: "bg-gray-200 text-gray-600 hover:bg-gray-300";
return (
<Button
variant="circle"
className={`rounded-full px-3 py-1 text-sm transition-colors ${selectedClass} ${className || ""}`}
{...rest}
>
{children}
</Button>
);
};