This commit is contained in:
2026-01-14 16:57:35 +08:00
parent 804baa64b2
commit ec265be26b
38 changed files with 585 additions and 294 deletions

View File

@@ -1,18 +1,19 @@
import { Plus, RefreshCw } from "lucide-react";
import { toast } from "sonner";
import { authClient } from "@/lib/auth-client";
import { Folder } from "../../../../generated/prisma/browser";
import { DictionaryEntry } from "./DictionaryEntry";
import { useTranslations } from "next-intl";
import { performDictionaryLookup } from "./utils";
import { TSharedItem } from "@/shared";
import { TSharedFolder } from "@/shared/folder-type";
import { actionCreatePair } from "@/modules/folder";
interface SearchResultProps {
searchResult: TSharedItem;
searchQuery: string;
queryLang: string;
definitionLang: string;
folders: Folder[];
folders: TSharedFolder[];
selectedFolderId: number | null;
onFolderSelect: (folderId: number | null) => void;
onResultUpdate: (newResult: TSharedItem) => void;
@@ -66,12 +67,12 @@ export function SearchResult({
}
const entry = searchResult.entries[0];
createPair({
actionCreatePair({
text1: searchResult.standardForm,
text2: entry.definition,
language1: queryLang,
language2: definitionLang,
ipa1: isDictWordResponse(searchResult) && (entry as DictWordEntry).ipa ? (entry as DictWordEntry).ipa : undefined,
ipa1: entry.ipa,
folderId: selectedFolderId,
})
.then(() => {

View File

@@ -3,13 +3,15 @@
import { useState, useEffect } from "react";
import Container from "@/components/ui/Container";
import { authClient } from "@/lib/auth-client";
import { Folder } from "../../../../generated/prisma/browser";
import { SearchForm } from "./SearchForm";
import { SearchResult } from "./SearchResult";
import { useTranslations } from "next-intl";
import { POPULAR_LANGUAGES } from "./constants";
import { performDictionaryLookup } from "./utils";
import { TSharedItem } from "@/shared";
import { actionGetFoldersByUserId } from "@/modules/folder";
import { TSharedFolder } from "@/shared/folder-type";
import { toast } from "sonner";
export default function DictionaryPage() {
const t = useTranslations("dictionary");
@@ -20,20 +22,24 @@ export default function DictionaryPage() {
const [queryLang, setQueryLang] = useState("english");
const [definitionLang, setDefinitionLang] = useState("chinese");
const [selectedFolderId, setSelectedFolderId] = useState<number | null>(null);
const [folders, setFolders] = useState<Folder[]>([]);
const [folders, setFolders] = useState<TSharedFolder[]>([]);
const { data: session } = authClient.useSession();
// 加载用户的文件夹列表
useEffect(() => {
if (session) {
getFoldersByUserId(session.user.id as string)
actionGetFoldersByUserId(session.user.id as string)
.then(result => {
if (!result.success || !result.data) throw result.message;
return result.data;
})
.then((loadedFolders) => {
setFolders(loadedFolders);
// 如果有文件夹且未选择,默认选择第一个
if (loadedFolders.length > 0 && !selectedFolderId) {
setSelectedFolderId(loadedFolders[0].id);
}
});
}).catch(e => toast.error);
}
}, [session, selectedFolderId]);

View File

@@ -1,14 +1,14 @@
import { toast } from "sonner";
import { lookUpDictionaryAction } from "@/modules/dictionary/dictionary-action";
import { DictionaryActionInputDto, DictionaryActionOutputDto } from "@/modules/dictionary";
import { actionLookUpDictionary } from "@/modules/dictionary/dictionary-action";
import { ActionInputLookUpDictionary, ActionOutputLookUpDictionary } from "@/modules/dictionary";
import { TSharedItem } from "@/shared";
export async function performDictionaryLookup(
options: DictionaryActionInputDto,
options: ActionInputLookUpDictionary,
t?: (key: string) => string
): Promise<TSharedItem | null> {
const { text, queryLang, definitionLang, forceRelook = false, userId } = options;
const result = await lookUpDictionaryAction({
const result = await actionLookUpDictionary({
text,
queryLang,
definitionLang,

View File

@@ -2,11 +2,11 @@
import { useState, useActionState, startTransition } from "react";
import { useTranslations } from "next-intl";
import { signInAction, signUpAction, SignUpState } from "@/modules/user/user-action";
import Container from "@/components/ui/Container";
import Input from "@/components/ui/Input";
import { LightButton } from "@/components/ui/buttons";
import { authClient } from "@/lib/auth-client";
import { signInAction, signUpAction, SignUpState } from "@/modules/auth";
interface AuthFormProps {
redirectTo?: string;
@@ -225,7 +225,7 @@ export default function AuthForm({ redirectTo }: AuthFormProps) {
className="w-full mt-4 py-2 flex items-center justify-center gap-2"
>
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
</svg>
{t(mode === 'signin' ? 'signInWithGitHub' : 'signUpWithGitHub')}
</LightButton>

View File

@@ -8,23 +8,17 @@ import {
Trash2,
} from "lucide-react";
import { useEffect, useState } from "react";
import { logger } from "@/lib/logger";
import { useRouter } from "next/navigation";
import { Folder } from "../../../generated/prisma/browser";
import {
createFolder,
deleteFolderById,
getFoldersWithTotalPairsByUserId,
renameFolderById,
} from "@/lib/server/services/folderService";
import { useTranslations } from "next-intl";
import { toast } from "sonner";
import PageLayout from "@/components/ui/PageLayout";
import PageHeader from "@/components/ui/PageHeader";
import CardList from "@/components/ui/CardList";
import { actionCreateFolder, actionDeleteFolderById, actionGetFoldersWithTotalPairsByUserId, actionRenameFolderById } from "@/modules/folder";
import { TSharedFolderWithTotalPairs } from "@/shared/folder-type";
interface FolderProps {
folder: Folder & { total: number };
folder: TSharedFolderWithTotalPairs;
refresh: () => void;
}
@@ -62,7 +56,15 @@ const FolderCard = ({ folder, refresh }: FolderProps) => {
e.stopPropagation();
const newName = prompt("Input a new name.")?.trim();
if (newName && newName.length > 0) {
renameFolderById(folder.id, newName).then(refresh);
actionRenameFolderById(folder.id, newName)
.then(result => {
if (result.success) {
refresh();
}
else {
toast.error(result.message);
}
});
}
}}
className="p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
@@ -74,7 +76,15 @@ const FolderCard = ({ folder, refresh }: FolderProps) => {
e.stopPropagation();
const confirm = prompt(t("confirmDelete", { name: folder.name }));
if (confirm === folder.name) {
deleteFolderById(folder.id).then(refresh);
actionDeleteFolderById(folder.id)
.then(result => {
if (result.success) {
refresh();
}
else {
toast.error(result.message);
}
});
}
}}
className="p-2 text-gray-400 hover:text-red-500 hover:bg-red-50 rounded-lg transition-colors"
@@ -87,33 +97,37 @@ const FolderCard = ({ folder, refresh }: FolderProps) => {
);
};
export default function FoldersClient({ userId }: { userId: string }) {
export default function FoldersClient({ userId }: { userId: string; }) {
const t = useTranslations("folders");
const [folders, setFolders] = useState<(Folder & { total: number })[]>(
const [folders, setFolders] = useState<TSharedFolderWithTotalPairs[]>(
[],
);
const [loading, setLoading] = useState(false);
useEffect(() => {
setLoading(true);
getFoldersWithTotalPairsByUserId(userId)
actionGetFoldersWithTotalPairsByUserId(userId)
.then((folders) => {
setFolders(folders);
setLoading(false);
})
.catch((error) => {
logger.error("加载文件夹失败", error);
toast.error("加载出错,请重试。");
if (folders.success && folders.data) {
setFolders(folders.data);
setLoading(false);
}
});
}, [userId]);
const updateFolders = async () => {
try {
const updatedFolders = await getFoldersWithTotalPairsByUserId(userId);
setFolders(updatedFolders);
} catch (error) {
logger.error("更新文件夹失败", error);
}
setLoading(true);
await actionGetFoldersWithTotalPairsByUserId(userId)
.then(async result => {
if (!result.success) toast.error(result.message);
else await actionGetFoldersWithTotalPairsByUserId(userId)
.then((folders) => {
if (folders.success && folders.data) {
setFolders(folders.data);
}
});
});
setLoading(false);
};
return (
@@ -127,11 +141,14 @@ export default function FoldersClient({ userId }: { userId: string }) {
if (!folderName) return;
setLoading(true);
try {
await createFolder({
name: folderName,
userId: userId,
});
await updateFolders();
await actionCreateFolder(userId, folderName)
.then(result => {
if (result.success) {
updateFolders();
} else {
toast.error(result.message);
}
});
} finally {
setLoading(false);
}

View File

@@ -3,30 +3,20 @@
import { ArrowLeft, Plus } from "lucide-react";
import { useEffect, useState } from "react";
import { redirect, useRouter } from "next/navigation";
import {
createPair,
deletePairById,
getPairsByFolderId,
} from "@/lib/server/services/pairService";
import AddTextPairModal from "./AddTextPairModal";
import TextPairCard from "./TextPairCard";
import { useTranslations } from "next-intl";
import PageLayout from "@/components/ui/PageLayout";
import { GreenButton } from "@/components/ui/buttons";
import { logger } from "@/lib/logger";
import { IconButton } from "@/components/ui/buttons";
import CardList from "@/components/ui/CardList";
import { actionCreatePair, actionDeletePairById, actionGetPairsByFolderId } from "@/modules/folder";
import { TSharedPair } from "@/shared/folder-type";
import { toast } from "sonner";
export interface TextPair {
id: number;
text1: string;
text2: string;
language1: string;
language2: string;
}
export default function InFolder({ folderId }: { folderId: number }) {
const [textPairs, setTextPairs] = useState<TextPair[]>([]);
export default function InFolder({ folderId }: { folderId: number; }) {
const [textPairs, setTextPairs] = useState<TSharedPair[]>([]);
const [loading, setLoading] = useState(true);
const [openAddModal, setAddModal] = useState(false);
const router = useRouter();
@@ -35,25 +25,26 @@ export default function InFolder({ folderId }: { folderId: number }) {
useEffect(() => {
const fetchTextPairs = async () => {
setLoading(true);
try {
const data = await getPairsByFolderId(folderId);
setTextPairs(data as TextPair[]);
} catch (error) {
logger.error("获取文本对失败", error);
} finally {
setLoading(false);
}
await actionGetPairsByFolderId(folderId)
.then(result => {
if (!result.success || !result.data) throw result.message;
return result.data;
}).then(setTextPairs)
.catch(toast.error)
.finally(() => {
setLoading(false);
});
};
fetchTextPairs();
}, [folderId]);
const refreshTextPairs = async () => {
try {
const data = await getPairsByFolderId(folderId);
setTextPairs(data as TextPair[]);
} catch (error) {
logger.error("获取文本对失败", error);
}
await actionGetPairsByFolderId(folderId)
.then(result => {
if (!result.success || !result.data) throw result.message;
return result.data;
}).then(setTextPairs)
.catch(toast.error);
};
return (
@@ -123,8 +114,11 @@ export default function InFolder({ folderId }: { folderId: number }) {
key={textPair.id}
textPair={textPair}
onDel={() => {
deletePairById(textPair.id);
refreshTextPairs();
actionDeletePairById(textPair.id)
.then(result => {
if (!result.success) throw result.message;
}).then(refreshTextPairs)
.catch(toast.error);
}}
refreshTextPairs={refreshTextPairs}
/>
@@ -143,7 +137,7 @@ export default function InFolder({ folderId }: { folderId: number }) {
language1: string,
language2: string,
) => {
await createPair({
await actionCreatePair({
text1: text1,
text2: text2,
language1: language1,
@@ -155,4 +149,4 @@ export default function InFolder({ folderId }: { folderId: number }) {
/>
</PageLayout>
);
}
};

View File

@@ -1,13 +1,13 @@
import { Edit, Trash2 } from "lucide-react";
import { TextPair } from "./InFolder";
import { updatePairById } from "@/lib/server/services/pairService";
import { useState } from "react";
import UpdateTextPairModal from "./UpdateTextPairModal";
import { useTranslations } from "next-intl";
import { UpdatePairInput } from "@/lib/server/services/types";
import { TSharedPair } from "@/shared/folder-type";
import { actionUpdatePairById, ActionInputUpdatePairById } from "@/modules/folder";
import { toast } from "sonner";
interface TextPairCardProps {
textPair: TextPair;
textPair: TSharedPair;
onDel: () => void;
refreshTextPairs: () => void;
}
@@ -66,8 +66,8 @@ export default function TextPairCard({
<UpdateTextPairModal
isOpen={openUpdateModal}
onClose={() => setOpenUpdateModal(false)}
onUpdate={async (id: number, data: UpdatePairInput) => {
await updatePairById(id, data);
onUpdate={async (id: number, data: ActionInputUpdatePairById) => {
await actionUpdatePairById(id, data).then(result => result.success ? toast.success(result.message) : toast.error(result.message));
setOpenUpdateModal(false);
refreshTextPairs();
}}

View File

@@ -3,15 +3,15 @@ import Input from "@/components/ui/Input";
import { LocaleSelector } from "@/components/ui/LocaleSelector";
import { X } from "lucide-react";
import { useRef, useState } from "react";
import { UpdatePairInput } from "@/lib/server/services/types";
import { TextPair } from "./InFolder";
import { useTranslations } from "next-intl";
import { TSharedPair } from "@/shared/folder-type";
import { ActionInputUpdatePairById } from "@/modules/folder";
interface UpdateTextPairModalProps {
isOpen: boolean;
onClose: () => void;
textPair: TextPair;
onUpdate: (id: number, tp: UpdatePairInput) => void;
textPair: TSharedPair;
onUpdate: (id: number, tp: ActionInputUpdatePairById) => void;
}
export default function UpdateTextPairModal({

View File

@@ -1,9 +1,9 @@
import { redirect } from "next/navigation";
import { getTranslations } from "next-intl/server";
import InFolder from "./InFolder";
import { getUserIdByFolderId } from "@/lib/server/services/folderService";
import { auth } from "@/auth";
import { headers } from "next/headers";
import { actionGetUserIdByFolderId } from "@/modules/folder";
export default async function FoldersPage({
params,
}: {
@@ -17,7 +17,7 @@ export default async function FoldersPage({
redirect("/folders");
}
if (!session) redirect(`/auth?redirect=/folders/${folder_id}`);
if ((await getUserIdByFolderId(Number(folder_id))) !== session.user.id) {
if ((await actionGetUserIdByFolderId(Number(folder_id))).data !== session.user.id) {
return <p>{t("unauthorized")}</p>;
}
return <InFolder folderId={Number(folder_id)} />;