Compare commits
2 Commits
804baa64b2
...
d4d5a53747
| Author | SHA1 | Date | |
|---|---|---|---|
| d4d5a53747 | |||
| ec265be26b |
@@ -173,7 +173,14 @@
|
||||
"translateInto": "Übersetzen in",
|
||||
"chinese": "Chinesisch",
|
||||
"english": "Englisch",
|
||||
"french": "Französisch",
|
||||
"german": "Deutsch",
|
||||
"italian": "Italienisch",
|
||||
"japanese": "Japanisch",
|
||||
"korean": "Koreanisch",
|
||||
"portuguese": "Portugiesisch",
|
||||
"russian": "Russisch",
|
||||
"spanish": "Spanisch",
|
||||
"other": "Andere",
|
||||
"translating": "Übersetzung läuft...",
|
||||
"translate": "Übersetzen",
|
||||
|
||||
@@ -173,7 +173,14 @@
|
||||
"translateInto": "translate into",
|
||||
"chinese": "Chinese",
|
||||
"english": "English",
|
||||
"french": "French",
|
||||
"german": "German",
|
||||
"italian": "Italian",
|
||||
"japanese": "Japanese",
|
||||
"korean": "Korean",
|
||||
"portuguese": "Portuguese",
|
||||
"russian": "Russian",
|
||||
"spanish": "Spanish",
|
||||
"other": "Other",
|
||||
"translating": "translating...",
|
||||
"translate": "translate",
|
||||
|
||||
@@ -173,7 +173,14 @@
|
||||
"translateInto": "traduire en",
|
||||
"chinese": "Chinois",
|
||||
"english": "Anglais",
|
||||
"french": "Français",
|
||||
"german": "Allemand",
|
||||
"italian": "Italien",
|
||||
"japanese": "Japonais",
|
||||
"korean": "Coréen",
|
||||
"portuguese": "Portugais",
|
||||
"russian": "Russe",
|
||||
"spanish": "Espagnol",
|
||||
"other": "Autre",
|
||||
"translating": "traduction...",
|
||||
"translate": "traduire",
|
||||
|
||||
@@ -173,7 +173,14 @@
|
||||
"translateInto": "traduci in",
|
||||
"chinese": "Cinese",
|
||||
"english": "Inglese",
|
||||
"french": "Francese",
|
||||
"german": "Tedesco",
|
||||
"italian": "Italiano",
|
||||
"japanese": "Giapponese",
|
||||
"korean": "Coreano",
|
||||
"portuguese": "Portoghese",
|
||||
"russian": "Russo",
|
||||
"spanish": "Spagnolo",
|
||||
"other": "Altro",
|
||||
"translating": "traduzione...",
|
||||
"translate": "traduci",
|
||||
|
||||
@@ -173,7 +173,14 @@
|
||||
"translateInto": "翻訳",
|
||||
"chinese": "中国語",
|
||||
"english": "英語",
|
||||
"french": "フランス語",
|
||||
"german": "ドイツ語",
|
||||
"italian": "イタリア語",
|
||||
"japanese": "日本語",
|
||||
"korean": "韓国語",
|
||||
"portuguese": "ポルトガル語",
|
||||
"russian": "ロシア語",
|
||||
"spanish": "スペイン語",
|
||||
"other": "その他",
|
||||
"translating": "翻訳中...",
|
||||
"translate": "翻訳",
|
||||
|
||||
@@ -173,7 +173,14 @@
|
||||
"translateInto": "번역",
|
||||
"chinese": "중국어",
|
||||
"english": "영어",
|
||||
"french": "프랑스어",
|
||||
"german": "독일어",
|
||||
"italian": "이탈리아어",
|
||||
"japanese": "일본어",
|
||||
"korean": "한국어",
|
||||
"portuguese": "포르투갈어",
|
||||
"russian": "러시아어",
|
||||
"spanish": "스페인어",
|
||||
"other": "기타",
|
||||
"translating": "번역 중...",
|
||||
"translate": "번역",
|
||||
|
||||
@@ -173,7 +173,14 @@
|
||||
"translateInto": "تەرجىمە قىلىش",
|
||||
"chinese": "خەنزۇچە",
|
||||
"english": "ئىنگلىزچە",
|
||||
"french": "فرانسۇزچە",
|
||||
"german": "گېرمانچە",
|
||||
"italian": "ئىتاليانچە",
|
||||
"japanese": "ياپونچە",
|
||||
"korean": "كورېيەچە",
|
||||
"portuguese": "پورتۇگالچە",
|
||||
"russian": "رۇسچە",
|
||||
"spanish": "ئىسپانچە",
|
||||
"other": "باشقا",
|
||||
"translating": "تەرجىمە قىلىۋاتىدۇ...",
|
||||
"translate": "تەرجىمە قىلىش",
|
||||
|
||||
@@ -173,7 +173,14 @@
|
||||
"translateInto": "翻译为",
|
||||
"chinese": "中文",
|
||||
"english": "英文",
|
||||
"french": "法语",
|
||||
"german": "德语",
|
||||
"italian": "意大利语",
|
||||
"japanese": "日语",
|
||||
"korean": "韩语",
|
||||
"portuguese": "葡萄牙语",
|
||||
"russian": "俄语",
|
||||
"spanish": "西班牙语",
|
||||
"other": "其他",
|
||||
"translating": "翻译中...",
|
||||
"translate": "翻译",
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
if (folders.success && folders.data) {
|
||||
setFolders(folders.data);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error("加载文件夹失败", error);
|
||||
toast.error("加载出错,请重试。");
|
||||
}
|
||||
});
|
||||
}, [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 actionCreateFolder(userId, folderName)
|
||||
.then(result => {
|
||||
if (result.success) {
|
||||
updateFolders();
|
||||
} else {
|
||||
toast.error(result.message);
|
||||
}
|
||||
});
|
||||
await updateFolders();
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
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>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
}}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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)} />;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { LookUpServiceOutputDto } from "@/modules/dictionary/dictionary-service-dto";
|
||||
import { ServiceOutputLookUp } from "@/modules/dictionary/dictionary-service-dto";
|
||||
import { analyzeInput } from "./stage1-inputAnalysis";
|
||||
import { determineSemanticMapping } from "./stage2-semanticMapping";
|
||||
import { generateStandardForm } from "./stage3-standardForm";
|
||||
@@ -9,7 +9,7 @@ export async function executeDictionaryLookup(
|
||||
text: string,
|
||||
queryLang: string,
|
||||
definitionLang: string
|
||||
): Promise<LookUpServiceOutputDto> {
|
||||
): Promise<ServiceOutputLookUp> {
|
||||
try {
|
||||
// ========== 阶段 1:输入分析 ==========
|
||||
console.log("[阶段1] 开始输入分析...");
|
||||
@@ -74,7 +74,7 @@ export async function executeDictionaryLookup(
|
||||
console.log("[阶段4] 词条生成完成:", entriesResult);
|
||||
|
||||
// ========== 组装最终结果 ==========
|
||||
const finalResult: LookUpServiceOutputDto = {
|
||||
const finalResult: ServiceOutputLookUp = {
|
||||
standardForm: standardFormResult.standardForm,
|
||||
entries: entriesResult.entries,
|
||||
};
|
||||
|
||||
@@ -67,6 +67,9 @@ export async function signUpAction(prevState: SignUpState, formData: FormData) {
|
||||
|
||||
redirect(redirectTo || "/");
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message.includes('NEXT_REDIRECT')) {
|
||||
throw error;
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
message: "注册失败,请稍后再试"
|
||||
1
src/modules/auth/index.ts
Normal file
1
src/modules/auth/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './auth-action';
|
||||
@@ -1,29 +1,21 @@
|
||||
import { ValidateError } from "@/lib/errors";
|
||||
import { TSharedItem } from "@/shared";
|
||||
import { LENGTH_MAX_DICTIONARY_TEXT, LENGTH_MAX_LANGUAGE, LENGTH_MIN_DICTIONARY_TEXT, LENGTH_MIN_LANGUAGE } from "@/shared/constant";
|
||||
import { generateValidator } from "@/utils/validate";
|
||||
import z from "zod";
|
||||
|
||||
const DictionaryActionInputDtoSchema = z.object({
|
||||
text: z.string().min(1, 'Empty text.').max(30, 'Text too long.'),
|
||||
queryLang: z.string().min(1, 'Query lang too short.').max(20, 'Query lang too long.'),
|
||||
const schemaActionInputLookUpDictionary = z.object({
|
||||
text: z.string().min(LENGTH_MIN_DICTIONARY_TEXT).max(LENGTH_MAX_DICTIONARY_TEXT),
|
||||
queryLang: z.string().min(LENGTH_MIN_LANGUAGE).max(LENGTH_MAX_LANGUAGE),
|
||||
forceRelook: z.boolean(),
|
||||
definitionLang: z.string().min(1, 'Definition lang too short.').max(20, 'Definition lang too long.'),
|
||||
definitionLang: z.string().min(LENGTH_MIN_LANGUAGE).max(LENGTH_MAX_LANGUAGE),
|
||||
userId: z.string().optional()
|
||||
});
|
||||
|
||||
export type DictionaryActionInputDto = z.infer<typeof DictionaryActionInputDtoSchema>;
|
||||
export type ActionInputLookUpDictionary = z.infer<typeof schemaActionInputLookUpDictionary>;
|
||||
|
||||
export const validateDictionaryActionInput = (dto: DictionaryActionInputDto): DictionaryActionInputDto => {
|
||||
const result = DictionaryActionInputDtoSchema.safeParse(dto);
|
||||
if (result.success) return result.data;
|
||||
export const validateActionInputLookUpDictionary = generateValidator(schemaActionInputLookUpDictionary);
|
||||
|
||||
const errorMessages = result.error.issues.map((issue) =>
|
||||
`${issue.path.join('.')}: ${issue.message}`
|
||||
).join('; ');
|
||||
|
||||
throw new ValidateError(`Validation failed: ${errorMessages}`);
|
||||
};
|
||||
|
||||
export type DictionaryActionOutputDto = {
|
||||
export type ActionOutputLookUpDictionary = {
|
||||
message: string,
|
||||
success: boolean;
|
||||
data?: TSharedItem;
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
"use server";
|
||||
|
||||
import { DictionaryActionInputDto, DictionaryActionOutputDto, validateDictionaryActionInput } from "./dictionary-action-dto";
|
||||
import { ActionInputLookUpDictionary, ActionOutputLookUpDictionary, validateActionInputLookUpDictionary } from "./dictionary-action-dto";
|
||||
import { ValidateError } from "@/lib/errors";
|
||||
import { lookUpService } from "./dictionary-service";
|
||||
import { serviceLookUp } from "./dictionary-service";
|
||||
|
||||
export const lookUpDictionaryAction = async (dto: DictionaryActionInputDto): Promise<DictionaryActionOutputDto> => {
|
||||
export const actionLookUpDictionary = async (dto: ActionInputLookUpDictionary): Promise<ActionOutputLookUpDictionary> => {
|
||||
try {
|
||||
return {
|
||||
message: 'success',
|
||||
success: true,
|
||||
data: await lookUpService(validateDictionaryActionInput(dto))
|
||||
data: await serviceLookUp(validateActionInputLookUpDictionary(dto))
|
||||
};
|
||||
} catch (e) {
|
||||
if (e instanceof ValidateError) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { TSharedItem } from "@/shared";
|
||||
|
||||
export type CreateDictionaryLookUpInputDto = {
|
||||
export type RepoInputCreateDictionaryLookUp = {
|
||||
userId?: string;
|
||||
text: string;
|
||||
queryLang: string;
|
||||
@@ -8,15 +8,15 @@ export type CreateDictionaryLookUpInputDto = {
|
||||
dictionaryItemId?: number;
|
||||
};
|
||||
|
||||
export type SelectLastLookUpResultOutputDto = TSharedItem & {id: number} | null;
|
||||
export type RepoOutputSelectLastLookUpResult = TSharedItem & {id: number} | null;
|
||||
|
||||
export type CreateDictionaryItemInputDto = {
|
||||
export type RepoInputCreateDictionaryItem = {
|
||||
standardForm: string;
|
||||
queryLang: string;
|
||||
definitionLang: string;
|
||||
};
|
||||
|
||||
export type CreateDictionaryEntryInputDto = {
|
||||
export type RepoInputCreateDictionaryEntry = {
|
||||
itemId: number;
|
||||
ipa?: string;
|
||||
definition: string;
|
||||
@@ -24,14 +24,14 @@ export type CreateDictionaryEntryInputDto = {
|
||||
example: string;
|
||||
};
|
||||
|
||||
export type CreateDictionaryEntryWithoutItemIdInputDto = {
|
||||
export type RepoInputCreateDictionaryEntryWithoutItemId = {
|
||||
ipa?: string;
|
||||
definition: string;
|
||||
partOfSpeech?: string;
|
||||
example: string;
|
||||
};
|
||||
|
||||
export type SelectLastLookUpResultInputDto = {
|
||||
export type RepoInputSelectLastLookUpResult = {
|
||||
text: string,
|
||||
queryLang: string,
|
||||
definitionLang: string;
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { stringNormalize } from "@/utils/string";
|
||||
import {
|
||||
CreateDictionaryEntryInputDto,
|
||||
CreateDictionaryEntryWithoutItemIdInputDto,
|
||||
CreateDictionaryItemInputDto,
|
||||
CreateDictionaryLookUpInputDto,
|
||||
SelectLastLookUpResultInputDto,
|
||||
SelectLastLookUpResultOutputDto,
|
||||
RepoInputCreateDictionaryEntry,
|
||||
RepoInputCreateDictionaryEntryWithoutItemId,
|
||||
RepoInputCreateDictionaryItem,
|
||||
RepoInputCreateDictionaryLookUp,
|
||||
RepoInputSelectLastLookUpResult,
|
||||
RepoOutputSelectLastLookUpResult,
|
||||
} from "./dictionary-repository-dto";
|
||||
import prisma from "@/lib/db";
|
||||
|
||||
export async function selectLastLookUpResult(dto: SelectLastLookUpResultInputDto): Promise<SelectLastLookUpResultOutputDto> {
|
||||
export async function repoSelectLastLookUpResult(dto: RepoInputSelectLastLookUpResult): Promise<RepoOutputSelectLastLookUpResult> {
|
||||
const result = await prisma.dictionaryLookUp.findFirst({
|
||||
where: {
|
||||
normalizedText: stringNormalize(dto.text),
|
||||
@@ -48,16 +48,16 @@ export async function selectLastLookUpResult(dto: SelectLastLookUpResultInputDto
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function createLookUp(content: CreateDictionaryLookUpInputDto) {
|
||||
export async function repoCreateLookUp(content: RepoInputCreateDictionaryLookUp) {
|
||||
return (await prisma.dictionaryLookUp.create({
|
||||
data: { ...content, normalizedText: stringNormalize(content.text) }
|
||||
})).id;
|
||||
}
|
||||
|
||||
export async function createLookUpWithItemAndEntries(
|
||||
itemData: CreateDictionaryItemInputDto,
|
||||
lookUpData: CreateDictionaryLookUpInputDto,
|
||||
entries: CreateDictionaryEntryWithoutItemIdInputDto[]
|
||||
export async function repoCreateLookUpWithItemAndEntries(
|
||||
itemData: RepoInputCreateDictionaryItem,
|
||||
lookUpData: RepoInputCreateDictionaryLookUp,
|
||||
entries: RepoInputCreateDictionaryEntryWithoutItemId[]
|
||||
) {
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
const item = await tx.dictionaryItem.create({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { TSharedItem } from "@/shared";
|
||||
|
||||
export type LookUpServiceInputDto = {
|
||||
export type ServiceInputLookUp = {
|
||||
text: string,
|
||||
queryLang: string,
|
||||
definitionLang: string,
|
||||
@@ -8,4 +8,4 @@ export type LookUpServiceInputDto = {
|
||||
userId?: string;
|
||||
};
|
||||
|
||||
export type LookUpServiceOutputDto = TSharedItem;
|
||||
export type ServiceOutputLookUp = TSharedItem;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { executeDictionaryLookup } from "@/lib/bigmodel/dictionary";
|
||||
import { createLookUp, createLookUpWithItemAndEntries, selectLastLookUpResult } from "./dictionary-repository";
|
||||
import { LookUpServiceInputDto } from "./dictionary-service-dto";
|
||||
import { repoCreateLookUp, repoCreateLookUpWithItemAndEntries, repoSelectLastLookUpResult } from "./dictionary-repository";
|
||||
import { ServiceInputLookUp } from "./dictionary-service-dto";
|
||||
|
||||
export const lookUpService = async (dto: LookUpServiceInputDto) => {
|
||||
export const serviceLookUp = async (dto: ServiceInputLookUp) => {
|
||||
const {
|
||||
text,
|
||||
queryLang,
|
||||
@@ -11,7 +11,7 @@ export const lookUpService = async (dto: LookUpServiceInputDto) => {
|
||||
forceRelook
|
||||
} = dto;
|
||||
|
||||
const lastLookUpResult = await selectLastLookUpResult({
|
||||
const lastLookUpResult = await repoSelectLastLookUpResult({
|
||||
text,
|
||||
queryLang,
|
||||
definitionLang,
|
||||
@@ -25,7 +25,7 @@ export const lookUpService = async (dto: LookUpServiceInputDto) => {
|
||||
);
|
||||
|
||||
// 使用事务确保数据一致性
|
||||
createLookUpWithItemAndEntries(
|
||||
repoCreateLookUpWithItemAndEntries(
|
||||
{
|
||||
standardForm: response.standardForm,
|
||||
queryLang,
|
||||
@@ -44,7 +44,7 @@ export const lookUpService = async (dto: LookUpServiceInputDto) => {
|
||||
|
||||
return response;
|
||||
} else {
|
||||
createLookUp({
|
||||
repoCreateLookUp({
|
||||
userId: userId,
|
||||
text: text,
|
||||
queryLang: queryLang,
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
"use server";
|
||||
|
||||
import { ValidateError } from "@/lib/errors";
|
||||
import { ActionInputCreatePair, ActionInputUpdatePairById, ActionOutputGetFoldersWithTotalPairsByUserId, validateActionInputCreatePair, validateActionInputUpdatePairById } from "./folder-action-dto";
|
||||
import { repoCreateFolder, repoCreatePair, repoDeleteFolderById, repoDeletePairById, repoGetFoldersByUserId, repoGetFoldersWithTotalPairsByUserId, repoGetPairsByFolderId, repoGetUserIdByFolderId, repoRenameFolderById, repoUpdatePairById } from "./folder-repository";
|
||||
import { validate } from "@/utils/validate";
|
||||
import z from "zod";
|
||||
import { LENGTH_MAX_FOLDER_NAME, LENGTH_MIN_FOLDER_NAME } from "@/shared/constant";
|
||||
|
||||
export async function actionGetPairsByFolderId(folderId: number) {
|
||||
try {
|
||||
return {
|
||||
success: true,
|
||||
message: 'success',
|
||||
data: await repoGetPairsByFolderId(folderId)
|
||||
};
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
return {
|
||||
success: false,
|
||||
message: 'Unknown error occured.'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function actionUpdatePairById(id: number, dto: ActionInputUpdatePairById) {
|
||||
try {
|
||||
const validatedDto = validateActionInputUpdatePairById(dto);
|
||||
await repoUpdatePairById(id, validatedDto);
|
||||
return {
|
||||
success: true,
|
||||
message: 'success',
|
||||
};
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
return {
|
||||
success: false,
|
||||
message: 'Unknown error occured.'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function actionGetUserIdByFolderId(folderId: number) {
|
||||
try {
|
||||
return {
|
||||
success: true,
|
||||
message: 'success',
|
||||
data: await repoGetUserIdByFolderId(folderId)
|
||||
};
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
return {
|
||||
success: false,
|
||||
message: 'Unknown error occured.'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function actionDeleteFolderById(folderId: number) {
|
||||
try {
|
||||
await repoDeleteFolderById(folderId);
|
||||
return {
|
||||
success: true,
|
||||
message: 'success',
|
||||
};
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
return {
|
||||
success: false,
|
||||
message: 'Unknown error occured.'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function actionDeletePairById(id: number) {
|
||||
try {
|
||||
await repoDeletePairById(id);
|
||||
return {
|
||||
success: true,
|
||||
message: 'success'
|
||||
};
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
return {
|
||||
success: false,
|
||||
message: 'Unknown error occured.'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function actionGetFoldersWithTotalPairsByUserId(id: string): Promise<ActionOutputGetFoldersWithTotalPairsByUserId> {
|
||||
try {
|
||||
return {
|
||||
success: true,
|
||||
message: 'success',
|
||||
data: await repoGetFoldersWithTotalPairsByUserId(id)
|
||||
};
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
return {
|
||||
success: false,
|
||||
message: 'Unknown error occured.'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function actionGetFoldersByUserId(userId: string) {
|
||||
try {
|
||||
return {
|
||||
success: true,
|
||||
message: 'success',
|
||||
data: await repoGetFoldersByUserId(userId)
|
||||
};
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
return {
|
||||
success: false,
|
||||
message: 'Unknown error occured.'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function actionCreatePair(dto: ActionInputCreatePair) {
|
||||
try {
|
||||
const validatedDto = validateActionInputCreatePair(dto);
|
||||
await repoCreatePair(validatedDto);
|
||||
return {
|
||||
success: true,
|
||||
message: 'success'
|
||||
};
|
||||
} catch (e) {
|
||||
if (e instanceof ValidateError) {
|
||||
return {
|
||||
success: false,
|
||||
message: e.message
|
||||
};
|
||||
}
|
||||
console.log(e);
|
||||
return {
|
||||
success: false,
|
||||
message: 'Unknown error occured.'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function actionCreateFolder(userId: string, folderName: string) {
|
||||
try {
|
||||
const validatedFolderName = validate(folderName,
|
||||
z.string()
|
||||
.trim()
|
||||
.min(LENGTH_MIN_FOLDER_NAME)
|
||||
.max(LENGTH_MAX_FOLDER_NAME));
|
||||
await repoCreateFolder({
|
||||
name: validatedFolderName,
|
||||
userId: userId
|
||||
});
|
||||
return {
|
||||
success: true,
|
||||
message: 'success'
|
||||
};
|
||||
} catch (e) {
|
||||
if (e instanceof ValidateError) {
|
||||
return {
|
||||
success: false,
|
||||
message: e.message
|
||||
};
|
||||
}
|
||||
console.log(e);
|
||||
return {
|
||||
success: false,
|
||||
message: 'Unknown error occured.'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function actionRenameFolderById(id: number, newName: string) {
|
||||
try {
|
||||
const validatedNewName = validate(
|
||||
newName,
|
||||
z.string()
|
||||
.min(LENGTH_MIN_FOLDER_NAME)
|
||||
.max(LENGTH_MAX_FOLDER_NAME)
|
||||
.trim());
|
||||
await repoRenameFolderById(id, validatedNewName);
|
||||
return {
|
||||
success: true,
|
||||
message: 'success'
|
||||
};
|
||||
} catch (e) {
|
||||
if (e instanceof ValidateError) {
|
||||
return {
|
||||
success: false,
|
||||
message: e.message
|
||||
};
|
||||
}
|
||||
console.log(e);
|
||||
return {
|
||||
success: false,
|
||||
message: 'Unknown error occured.'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { LENGTH_MAX_FOLDER_NAME, LENGTH_MAX_IPA, LENGTH_MAX_LANGUAGE, LENGTH_MAX_PAIR_TEXT, LENGTH_MIN_FOLDER_NAME, LENGTH_MIN_IPA, LENGTH_MIN_LANGUAGE, LENGTH_MIN_PAIR_TEXT } from "@/shared/constant";
|
||||
import { TSharedFolderWithTotalPairs } from "@/shared/folder-type";
|
||||
import { generateValidator } from "@/utils/validate";
|
||||
import z from "zod";
|
||||
|
||||
export const schemaActionInputCreatePair = z.object({
|
||||
text1: z.string().min(LENGTH_MIN_PAIR_TEXT).max(LENGTH_MAX_PAIR_TEXT),
|
||||
text2: z.string().min(LENGTH_MIN_PAIR_TEXT).max(LENGTH_MAX_PAIR_TEXT),
|
||||
language1: z.string().min(LENGTH_MIN_LANGUAGE).max(LENGTH_MAX_LANGUAGE),
|
||||
language2: z.string().min(LENGTH_MIN_LANGUAGE).max(LENGTH_MAX_LANGUAGE),
|
||||
ipa1: z.string().min(LENGTH_MIN_IPA).max(LENGTH_MAX_IPA).optional(),
|
||||
ipa2: z.string().min(LENGTH_MIN_IPA).max(LENGTH_MAX_IPA).optional(),
|
||||
folderId: z.int()
|
||||
});
|
||||
export type ActionInputCreatePair = z.infer<typeof schemaActionInputCreatePair>;
|
||||
export const validateActionInputCreatePair = generateValidator(schemaActionInputCreatePair);
|
||||
|
||||
export const schemaActionInputUpdatePairById = z.object({
|
||||
text1: z.string().min(LENGTH_MIN_PAIR_TEXT).max(LENGTH_MAX_PAIR_TEXT).optional(),
|
||||
text2: z.string().min(LENGTH_MIN_PAIR_TEXT).max(LENGTH_MAX_PAIR_TEXT).optional(),
|
||||
language1: z.string().min(LENGTH_MIN_LANGUAGE).max(LENGTH_MAX_LANGUAGE).optional(),
|
||||
language2: z.string().min(LENGTH_MIN_LANGUAGE).max(LENGTH_MAX_LANGUAGE).optional(),
|
||||
ipa1: z.string().min(LENGTH_MIN_IPA).max(LENGTH_MAX_IPA).optional(),
|
||||
ipa2: z.string().min(LENGTH_MIN_IPA).max(LENGTH_MAX_IPA).optional(),
|
||||
folderId: z.int().optional()
|
||||
});
|
||||
export type ActionInputUpdatePairById = z.infer<typeof schemaActionInputUpdatePairById>;
|
||||
export const validateActionInputUpdatePairById = generateValidator(schemaActionInputUpdatePairById);
|
||||
|
||||
export type ActionOutputGetFoldersWithTotalPairsByUserId = {
|
||||
message: string,
|
||||
success: boolean,
|
||||
data?: TSharedFolderWithTotalPairs[];
|
||||
};
|
||||
|
||||
23
src/modules/folder/folder-repository-dto.ts
Normal file
23
src/modules/folder/folder-repository-dto.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
export interface RepoInputCreateFolder {
|
||||
name: string;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
export interface RepoInputCreatePair {
|
||||
text1: string;
|
||||
text2: string;
|
||||
language1: string;
|
||||
language2: string;
|
||||
ipa1?: string;
|
||||
ipa2?: string;
|
||||
folderId: number;
|
||||
}
|
||||
|
||||
export interface RepoInputUpdatePair {
|
||||
text1?: string;
|
||||
text2?: string;
|
||||
language1?: string;
|
||||
language2?: string;
|
||||
ipa1?: string;
|
||||
ipa2?: string;
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import prisma from "@/lib/db";
|
||||
import { RepoInputCreateFolder, RepoInputCreatePair, RepoInputUpdatePair } from "./folder-repository-dto";
|
||||
|
||||
export async function repoCreatePair(data: RepoInputCreatePair) {
|
||||
return (await prisma.pair.create({
|
||||
data: data,
|
||||
})).id;
|
||||
}
|
||||
|
||||
export async function repoDeletePairById(id: number) {
|
||||
await prisma.pair.delete({
|
||||
where: {
|
||||
id: id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function repoUpdatePairById(
|
||||
id: number,
|
||||
data: RepoInputUpdatePair,
|
||||
) {
|
||||
await prisma.pair.update({
|
||||
where: {
|
||||
id: id,
|
||||
},
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
export async function repoGetPairCountByFolderId(folderId: number) {
|
||||
return prisma.pair.count({
|
||||
where: {
|
||||
folderId: folderId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function repoGetPairsByFolderId(folderId: number) {
|
||||
return (await prisma.pair.findMany({
|
||||
where: {
|
||||
folderId: folderId,
|
||||
},
|
||||
})).map(pair => {
|
||||
return {
|
||||
text1:pair.text1,
|
||||
text2: pair.text2,
|
||||
language1: pair.language1,
|
||||
language2: pair.language2,
|
||||
ipa1: pair.ipa1,
|
||||
ipa2: pair.ipa2,
|
||||
id: pair.id,
|
||||
folderId: pair.folderId
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function repoGetFoldersByUserId(userId: string) {
|
||||
return (await prisma.folder.findMany({
|
||||
where: {
|
||||
userId: userId,
|
||||
},
|
||||
}))?.map(v => {
|
||||
return {
|
||||
id: v.id,
|
||||
name: v.name,
|
||||
userId: v.userId
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export async function repoRenameFolderById(id: number, newName: string) {
|
||||
await prisma.folder.update({
|
||||
where: {
|
||||
id: id,
|
||||
},
|
||||
data: {
|
||||
name: newName,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function repoGetFoldersWithTotalPairsByUserId(userId: string) {
|
||||
const folders = await prisma.folder.findMany({
|
||||
where: { userId },
|
||||
include: {
|
||||
_count: {
|
||||
select: { pairs: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
return folders.map(folder => ({
|
||||
id: folder.id,
|
||||
name: folder.name,
|
||||
userId: folder.userId,
|
||||
total: folder._count?.pairs ?? 0,
|
||||
}));
|
||||
}
|
||||
|
||||
export async function repoCreateFolder(folder: RepoInputCreateFolder) {
|
||||
await prisma.folder.create({
|
||||
data: folder,
|
||||
});
|
||||
}
|
||||
|
||||
export async function repoDeleteFolderById(id: number) {
|
||||
await prisma.folder.delete({
|
||||
where: {
|
||||
id: id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function repoGetUserIdByFolderId(id: number) {
|
||||
const folder = await prisma.folder.findUnique({
|
||||
where: {
|
||||
id: id,
|
||||
},
|
||||
});
|
||||
return folder?.userId;
|
||||
}
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
import { CreateFolderInput, UpdateFolderInput } from "../translator/translator-dto";
|
||||
import prisma from "@/lib/db";
|
||||
|
||||
export async function getFoldersByUserId(userId: string) {
|
||||
return prisma.folder.findMany({
|
||||
where: {
|
||||
userId: userId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function renameFolderById(id: number, newName: string) {
|
||||
return prisma.folder.update({
|
||||
where: {
|
||||
id: id,
|
||||
},
|
||||
data: {
|
||||
name: newName,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function getFoldersWithTotalPairsByUserId(userId: string) {
|
||||
const folders = await prisma.folder.findMany({
|
||||
where: { userId },
|
||||
include: {
|
||||
_count: {
|
||||
select: { pairs: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
return folders.map(folder => ({
|
||||
...folder,
|
||||
total: folder._count?.pairs ?? 0,
|
||||
}));
|
||||
}
|
||||
|
||||
export async function createFolder(folder: CreateFolderInput) {
|
||||
return prisma.folder.create({
|
||||
data: folder,
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteFolderById(id: number) {
|
||||
return prisma.folder.delete({
|
||||
where: {
|
||||
id: id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateFolderById(id: number, data: UpdateFolderInput) {
|
||||
return prisma.folder.update({
|
||||
where: {
|
||||
id: id,
|
||||
},
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
export async function getUserIdByFolderId(id: number) {
|
||||
const folder = await prisma.folder.findUnique({
|
||||
where: {
|
||||
id: id,
|
||||
},
|
||||
});
|
||||
return folder?.userId;
|
||||
}
|
||||
|
||||
2
src/modules/folder/index.ts
Normal file
2
src/modules/folder/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './folder-aciton';
|
||||
export * from './folder-action-dto';
|
||||
@@ -1 +0,0 @@
|
||||
"use server";
|
||||
@@ -1,44 +0,0 @@
|
||||
import { CreatePairInput, UpdatePairInput } from "../translator/translator-dto";
|
||||
import prisma from "@/lib/db";
|
||||
|
||||
export async function createPair(data: CreatePairInput) {
|
||||
return (await prisma.pair.create({
|
||||
data: data,
|
||||
})).id;
|
||||
}
|
||||
|
||||
export async function deletePairById(id: number) {
|
||||
await prisma.pair.delete({
|
||||
where: {
|
||||
id: id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function updatePairById(
|
||||
id: number,
|
||||
data: UpdatePairInput,
|
||||
) {
|
||||
await prisma.pair.update({
|
||||
where: {
|
||||
id: id,
|
||||
},
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
export async function getPairCountByFolderId(folderId: number) {
|
||||
return prisma.pair.count({
|
||||
where: {
|
||||
folderId: folderId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function getPairsByFolderId(folderId: number) {
|
||||
return prisma.pair.findMany({
|
||||
where: {
|
||||
folderId: folderId,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,40 +1,3 @@
|
||||
/**
|
||||
* Service 层的自定义业务类型
|
||||
*
|
||||
* 这些类型用于替换 Prisma 生成的类型,提高代码的可维护性和抽象层次
|
||||
*/
|
||||
|
||||
// Folder 相关
|
||||
export interface CreateFolderInput {
|
||||
name: string;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
export interface UpdateFolderInput {
|
||||
name?: string;
|
||||
}
|
||||
|
||||
// Pair 相关
|
||||
export interface CreatePairInput {
|
||||
text1: string;
|
||||
text2: string;
|
||||
language1: string;
|
||||
language2: string;
|
||||
ipa1?: string;
|
||||
ipa2?: string;
|
||||
folderId: number;
|
||||
}
|
||||
|
||||
export interface UpdatePairInput {
|
||||
text1?: string;
|
||||
text2?: string;
|
||||
language1?: string;
|
||||
language2?: string;
|
||||
ipa1?: string;
|
||||
ipa2?: string;
|
||||
}
|
||||
|
||||
// Translation 相关
|
||||
export interface CreateTranslationHistoryInput {
|
||||
userId?: string;
|
||||
sourceText: string;
|
||||
@@ -50,7 +13,6 @@ export interface TranslationHistoryQuery {
|
||||
targetLanguage: string;
|
||||
}
|
||||
|
||||
// 翻译相关 - 统一翻译函数
|
||||
export interface TranslateTextInput {
|
||||
sourceText: string;
|
||||
targetLanguage: string;
|
||||
|
||||
14
src/shared/constant.ts
Normal file
14
src/shared/constant.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export const LENGTH_MAX_DICTIONARY_TEXT = 30;
|
||||
export const LENGTH_MIN_DICTIONARY_TEXT = 1;
|
||||
|
||||
export const LENGTH_MAX_LANGUAGE = 20;
|
||||
export const LENGTH_MIN_LANGUAGE = 1;
|
||||
|
||||
export const LENGTH_MAX_PAIR_TEXT = 50;
|
||||
export const LENGTH_MIN_PAIR_TEXT = 1;
|
||||
|
||||
export const LENGTH_MAX_IPA = 150;
|
||||
export const LENGTH_MIN_IPA = 1;
|
||||
|
||||
export const LENGTH_MAX_FOLDER_NAME = 20;
|
||||
export const LENGTH_MIN_FOLDER_NAME = 1;
|
||||
@@ -1,3 +0,0 @@
|
||||
export type TSharedPair = {
|
||||
|
||||
};
|
||||
23
src/shared/folder-type.ts
Normal file
23
src/shared/folder-type.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
export type TSharedFolder = {
|
||||
id: number,
|
||||
name: string,
|
||||
userId: string;
|
||||
};
|
||||
|
||||
export type TSharedFolderWithTotalPairs = {
|
||||
id: number,
|
||||
name: string,
|
||||
userId: string,
|
||||
total: number;
|
||||
};
|
||||
|
||||
export type TSharedPair = {
|
||||
text1: string;
|
||||
text2: string;
|
||||
language1: string;
|
||||
language2: string;
|
||||
ipa1: string | null;
|
||||
ipa2: string | null;
|
||||
id: number;
|
||||
folderId: number;
|
||||
};
|
||||
13
src/utils/validate.ts
Normal file
13
src/utils/validate.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { ValidateError } from "@/lib/errors";
|
||||
import z from "zod";
|
||||
|
||||
export const validate = <T, U extends z.ZodType>(dto: T, schema: U) => {
|
||||
const result = schema.safeParse(dto);
|
||||
if (result.success) return result.data as z.infer<U>;
|
||||
const errorMessages = result.error.issues.map((issue) =>
|
||||
`${issue.path.join('.')}: ${issue.message}`
|
||||
).join('; ');
|
||||
throw new ValidateError(`Validation failed: ${errorMessages}`);
|
||||
};
|
||||
|
||||
export const generateValidator = <T extends z.ZodType>(schema: T) => <T>(dto: T) => validate(dto, schema);
|
||||
Reference in New Issue
Block a user