...
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
-- DropIndex
|
||||
DROP INDEX "dictionary_phrases_standard_form_query_lang_definition_lang_key";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "dictionary_words_standard_form_query_lang_definition_lang_key";
|
||||
|
||||
-- RenameIndex
|
||||
ALTER INDEX "pairs_folder_id_locale1_locale2_text1_key" RENAME TO "pairs_folder_id_language1_language2_text1_key";
|
||||
@@ -136,7 +136,6 @@ model DictionaryWord {
|
||||
lookups DictionaryLookUp[]
|
||||
entries DictionaryWordEntry[]
|
||||
|
||||
@@unique([standardForm, queryLang, definitionLang])
|
||||
@@index([standardForm])
|
||||
@@index([queryLang, definitionLang])
|
||||
@@map("dictionary_words")
|
||||
@@ -153,7 +152,6 @@ model DictionaryPhrase {
|
||||
lookups DictionaryLookUp[]
|
||||
entries DictionaryPhraseEntry[]
|
||||
|
||||
@@unique([standardForm, queryLang, definitionLang])
|
||||
@@index([standardForm])
|
||||
@@index([queryLang, definitionLang])
|
||||
@@map("dictionary_phrases")
|
||||
|
||||
78
src/app/(features)/dictionary/DictionaryEntry.tsx
Normal file
78
src/app/(features)/dictionary/DictionaryEntry.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import { DictWordEntry, DictPhraseEntry } from "./types";
|
||||
|
||||
interface DictionaryEntryProps {
|
||||
entry: DictWordEntry | DictPhraseEntry;
|
||||
}
|
||||
|
||||
export function DictionaryEntry({ entry }: DictionaryEntryProps) {
|
||||
// 检查是否有 ipa 字段来判断是否为单词条目
|
||||
const isWordEntry = "ipa" in entry && "partOfSpeech" in entry;
|
||||
|
||||
if (isWordEntry) {
|
||||
// 单词条目
|
||||
const wordEntry = entry as DictWordEntry;
|
||||
return (
|
||||
<div>
|
||||
{/* 音标和词性 */}
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
{wordEntry.ipa && (
|
||||
<span className="text-gray-600 text-lg">
|
||||
{wordEntry.ipa}
|
||||
</span>
|
||||
)}
|
||||
{wordEntry.partOfSpeech && (
|
||||
<span className="px-3 py-1 bg-[#35786f] text-white text-sm rounded-full">
|
||||
{wordEntry.partOfSpeech}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 释义 */}
|
||||
<div className="mb-3">
|
||||
<h3 className="text-sm font-semibold text-gray-700 mb-1">
|
||||
释义
|
||||
</h3>
|
||||
<p className="text-gray-800">{wordEntry.definition}</p>
|
||||
</div>
|
||||
|
||||
{/* 例句 */}
|
||||
{wordEntry.example && (
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-gray-700 mb-1">
|
||||
例句
|
||||
</h3>
|
||||
<p className="text-gray-700 pl-4 border-l-4 border-[#35786f]">
|
||||
{wordEntry.example}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 短语条目
|
||||
const phraseEntry = entry as DictPhraseEntry;
|
||||
return (
|
||||
<div>
|
||||
{/* 释义 */}
|
||||
<div className="mb-3">
|
||||
<h3 className="text-sm font-semibold text-gray-700 mb-1">
|
||||
释义
|
||||
</h3>
|
||||
<p className="text-gray-800">{phraseEntry.definition}</p>
|
||||
</div>
|
||||
|
||||
{/* 例句 */}
|
||||
{phraseEntry.example && (
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-gray-700 mb-1">
|
||||
例句
|
||||
</h3>
|
||||
<p className="text-gray-700 pl-4 border-l-4 border-[#35786f]">
|
||||
{phraseEntry.example}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
133
src/app/(features)/dictionary/DictionaryPage.tsx
Normal file
133
src/app/(features)/dictionary/DictionaryPage.tsx
Normal file
@@ -0,0 +1,133 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import Container from "@/components/ui/Container";
|
||||
import { lookUp } from "@/lib/server/bigmodel/dictionaryActions";
|
||||
import { toast } from "sonner";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { Folder } from "../../../../generated/prisma/browser";
|
||||
import { getFoldersByUserId } from "@/lib/server/services/folderService";
|
||||
import { DictLookUpResponse, isDictErrorResponse } from "./types";
|
||||
import { SearchForm } from "./SearchForm";
|
||||
import { SearchResult } from "./SearchResult";
|
||||
|
||||
export default function Dictionary() {
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [searchResult, setSearchResult] = useState<DictLookUpResponse | null>(null);
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
const [hasSearched, setHasSearched] = useState(false);
|
||||
const [queryLang, setQueryLang] = useState("english");
|
||||
const [definitionLang, setDefinitionLang] = useState("chinese");
|
||||
const [selectedFolderId, setSelectedFolderId] = useState<number | null>(null);
|
||||
const [folders, setFolders] = useState<Folder[]>([]);
|
||||
const { data: session } = authClient.useSession();
|
||||
|
||||
// 加载用户的文件夹列表
|
||||
useEffect(() => {
|
||||
if (session) {
|
||||
getFoldersByUserId(session.user.id as string)
|
||||
.then((loadedFolders) => {
|
||||
setFolders(loadedFolders);
|
||||
// 如果有文件夹且未选择,默认选择第一个
|
||||
if (loadedFolders.length > 0 && !selectedFolderId) {
|
||||
setSelectedFolderId(loadedFolders[0].id);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [session, selectedFolderId]);
|
||||
|
||||
const handleSearch = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!searchQuery.trim()) return;
|
||||
|
||||
setIsSearching(true);
|
||||
setHasSearched(true);
|
||||
setSearchResult(null);
|
||||
|
||||
try {
|
||||
// 使用查询语言和释义语言
|
||||
// const result = await lookUp(searchQuery, queryLang, definitionLang);
|
||||
const result = await lookUp({
|
||||
text: searchQuery,
|
||||
definitionLang: definitionLang,
|
||||
queryLang: queryLang,
|
||||
forceRelook: false
|
||||
})
|
||||
|
||||
// 检查是否为错误响应
|
||||
if (isDictErrorResponse(result)) {
|
||||
toast.error(result.error);
|
||||
setSearchResult(null);
|
||||
} else {
|
||||
setSearchResult(result);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("词典查询失败:", error);
|
||||
toast.error("查询失败,请稍后重试");
|
||||
setSearchResult(null);
|
||||
} finally {
|
||||
setIsSearching(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-[calc(100vh-64px)] bg-[#35786f]">
|
||||
{/* 搜索区域 */}
|
||||
<div className="flex items-center justify-center px-4 py-12">
|
||||
<Container className="max-w-3xl w-full p-4">
|
||||
<SearchForm
|
||||
searchQuery={searchQuery}
|
||||
onSearchQueryChange={setSearchQuery}
|
||||
isSearching={isSearching}
|
||||
onSearch={handleSearch}
|
||||
queryLang={queryLang}
|
||||
onQueryLangChange={setQueryLang}
|
||||
definitionLang={definitionLang}
|
||||
onDefinitionLangChange={setDefinitionLang}
|
||||
/>
|
||||
</Container>
|
||||
</div>
|
||||
|
||||
{/* 搜索结果区域 */}
|
||||
<div className="flex-1 px-4 pb-12">
|
||||
<Container className="max-w-3xl w-full p-4">
|
||||
{isSearching && (
|
||||
<div className="text-center py-8">
|
||||
<div className="inline-block animate-spin rounded-full h-12 w-12 border-b-2 border-white"></div>
|
||||
<p className="mt-4 text-white">加载中...</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isSearching && hasSearched && !searchResult && (
|
||||
<div className="text-center py-12 bg-white/20 rounded-lg">
|
||||
<p className="text-gray-800 text-xl">未找到结果</p>
|
||||
<p className="text-gray-600 mt-2">尝试其他单词或短语</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isSearching && searchResult && !isDictErrorResponse(searchResult) && (
|
||||
<SearchResult
|
||||
searchResult={searchResult}
|
||||
searchQuery={searchQuery}
|
||||
queryLang={queryLang}
|
||||
definitionLang={definitionLang}
|
||||
folders={folders}
|
||||
selectedFolderId={selectedFolderId}
|
||||
onFolderSelect={setSelectedFolderId}
|
||||
onResultUpdate={setSearchResult}
|
||||
onSearchingChange={setIsSearching}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!hasSearched && (
|
||||
<div className="text-center py-12 bg-white/20 rounded-lg">
|
||||
<div className="text-6xl mb-4">📚</div>
|
||||
<p className="text-gray-800 text-xl mb-2">欢迎使用词典</p>
|
||||
<p className="text-gray-600">在上方搜索框中输入单词或短语开始查询</p>
|
||||
</div>
|
||||
)}
|
||||
</Container>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
124
src/app/(features)/dictionary/SearchForm.tsx
Normal file
124
src/app/(features)/dictionary/SearchForm.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
import { LightButton } from "@/components/ui/buttons";
|
||||
import { POPULAR_LANGUAGES } from "./constants";
|
||||
|
||||
interface SearchFormProps {
|
||||
searchQuery: string;
|
||||
onSearchQueryChange: (query: string) => void;
|
||||
isSearching: boolean;
|
||||
onSearch: (e: React.FormEvent) => void;
|
||||
queryLang: string;
|
||||
onQueryLangChange: (lang: string) => void;
|
||||
definitionLang: string;
|
||||
onDefinitionLangChange: (lang: string) => void;
|
||||
}
|
||||
|
||||
export function SearchForm({
|
||||
searchQuery,
|
||||
onSearchQueryChange,
|
||||
isSearching,
|
||||
onSearch,
|
||||
queryLang,
|
||||
onQueryLangChange,
|
||||
definitionLang,
|
||||
onDefinitionLangChange,
|
||||
}: SearchFormProps) {
|
||||
return (
|
||||
<>
|
||||
{/* 页面标题 */}
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-4xl md:text-5xl font-bold text-gray-800 mb-4">
|
||||
词典
|
||||
</h1>
|
||||
<p className="text-gray-700 text-lg">
|
||||
查询单词和短语,提供详细的释义和例句
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 搜索表单 */}
|
||||
<form onSubmit={onSearch} className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => onSearchQueryChange(e.target.value)}
|
||||
placeholder="输入要查询的单词或短语..."
|
||||
className="flex-1 px-4 py-3 text-lg text-gray-800 focus:outline-none border-b-2 border-gray-600 bg-white/90 rounded"
|
||||
/>
|
||||
<LightButton
|
||||
type="submit"
|
||||
disabled={isSearching || !searchQuery.trim()}
|
||||
className="px-6 py-3"
|
||||
>
|
||||
{isSearching ? "查询中..." : "查询"}
|
||||
</LightButton>
|
||||
</form>
|
||||
|
||||
{/* 语言设置 */}
|
||||
<div className="mt-4 bg-white/20 rounded-lg p-4">
|
||||
<div className="mb-3">
|
||||
<span className="text-gray-800 font-semibold">语言设置</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* 查询语言 */}
|
||||
<div>
|
||||
<label className="block text-gray-700 text-sm mb-2">
|
||||
查询语言 (你要查询的单词/短语是什么语言)
|
||||
</label>
|
||||
<div className="flex flex-wrap gap-2 mb-2">
|
||||
{POPULAR_LANGUAGES.map((lang) => (
|
||||
<LightButton
|
||||
key={lang.code}
|
||||
selected={queryLang === lang.code}
|
||||
onClick={() => onQueryLangChange(lang.code)}
|
||||
className="text-sm px-3 py-1"
|
||||
>
|
||||
{lang.name}
|
||||
</LightButton>
|
||||
))}
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={queryLang}
|
||||
onChange={(e) => onQueryLangChange(e.target.value)}
|
||||
placeholder="或输入其他语言..."
|
||||
className="w-full px-3 py-2 text-sm text-gray-800 focus:outline-none border-b-2 border-gray-600 bg-white/90 rounded"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 释义语言 */}
|
||||
<div>
|
||||
<label className="block text-gray-700 text-sm mb-2">
|
||||
释义语言 (你希望用什么语言查看释义)
|
||||
</label>
|
||||
<div className="flex flex-wrap gap-2 mb-2">
|
||||
{POPULAR_LANGUAGES.map((lang) => (
|
||||
<LightButton
|
||||
key={lang.code}
|
||||
selected={definitionLang === lang.code}
|
||||
onClick={() => onDefinitionLangChange(lang.code)}
|
||||
className="text-sm px-3 py-1"
|
||||
>
|
||||
{lang.name}
|
||||
</LightButton>
|
||||
))}
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={definitionLang}
|
||||
onChange={(e) => onDefinitionLangChange(e.target.value)}
|
||||
placeholder="或输入其他语言..."
|
||||
className="w-full px-3 py-2 text-sm text-gray-800 focus:outline-none border-b-2 border-gray-600 bg-white/90 rounded"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 当前设置显示 */}
|
||||
<div className="text-center text-gray-700 text-sm pt-2 border-t border-gray-300">
|
||||
当前设置:查询 <span className="font-semibold">{POPULAR_LANGUAGES.find(l => l.code === queryLang)?.name || queryLang}</span>
|
||||
,释义 <span className="font-semibold">{POPULAR_LANGUAGES.find(l => l.code === definitionLang)?.name || definitionLang}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</>
|
||||
);
|
||||
}
|
||||
155
src/app/(features)/dictionary/SearchResult.tsx
Normal file
155
src/app/(features)/dictionary/SearchResult.tsx
Normal file
@@ -0,0 +1,155 @@
|
||||
import { Plus, RefreshCw } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { Folder } from "../../../../generated/prisma/browser";
|
||||
import { createPair } from "@/lib/server/services/pairService";
|
||||
import { lookUp } from "@/lib/server/bigmodel/dictionaryActions";
|
||||
import {
|
||||
DictWordResponse,
|
||||
DictPhraseResponse,
|
||||
isDictWordResponse,
|
||||
DictWordEntry,
|
||||
isDictErrorResponse,
|
||||
} from "./types";
|
||||
import { DictionaryEntry } from "./DictionaryEntry";
|
||||
import { POPULAR_LANGUAGES } from "./constants";
|
||||
|
||||
interface SearchResultProps {
|
||||
searchResult: DictWordResponse | DictPhraseResponse;
|
||||
searchQuery: string;
|
||||
queryLang: string;
|
||||
definitionLang: string;
|
||||
folders: Folder[];
|
||||
selectedFolderId: number | null;
|
||||
onFolderSelect: (folderId: number | null) => void;
|
||||
onResultUpdate: (newResult: DictWordResponse | DictPhraseResponse) => void;
|
||||
onSearchingChange: (isSearching: boolean) => void;
|
||||
}
|
||||
|
||||
export function SearchResult({
|
||||
searchResult,
|
||||
searchQuery,
|
||||
queryLang,
|
||||
definitionLang,
|
||||
folders,
|
||||
selectedFolderId,
|
||||
onFolderSelect,
|
||||
onResultUpdate,
|
||||
onSearchingChange,
|
||||
}: SearchResultProps) {
|
||||
const { data: session } = authClient.useSession();
|
||||
|
||||
const handleRelookup = async () => {
|
||||
onSearchingChange(true);
|
||||
|
||||
try {
|
||||
const result = await lookUp({
|
||||
text: searchQuery,
|
||||
definitionLang: definitionLang,
|
||||
queryLang: queryLang,
|
||||
forceRelook: true
|
||||
});
|
||||
|
||||
if (isDictErrorResponse(result)) {
|
||||
toast.error(result.error);
|
||||
} else {
|
||||
onResultUpdate(result);
|
||||
toast.success("已重新查询");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("词典重新查询失败:", error);
|
||||
toast.error("查询失败,请稍后重试");
|
||||
} finally {
|
||||
onSearchingChange(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
if (!session) {
|
||||
toast.error("请先登录");
|
||||
return;
|
||||
}
|
||||
if (!selectedFolderId) {
|
||||
toast.error("请先创建文件夹");
|
||||
return;
|
||||
}
|
||||
|
||||
const entry = searchResult.entries[0];
|
||||
createPair({
|
||||
text1: searchResult.standardForm,
|
||||
text2: entry.definition,
|
||||
language1: queryLang,
|
||||
language2: definitionLang,
|
||||
ipa1: isDictWordResponse(searchResult) && (entry as DictWordEntry).ipa ? (entry as DictWordEntry).ipa : undefined,
|
||||
folder: {
|
||||
connect: {
|
||||
id: selectedFolderId,
|
||||
},
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
const folderName = folders.find(f => f.id === selectedFolderId)?.name;
|
||||
toast.success(`已保存到文件夹:${folderName}`);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("保存失败,请稍后重试");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="bg-white rounded-lg p-6 shadow-lg">
|
||||
{/* 标题和保存按钮 */}
|
||||
<div className="flex items-start justify-between mb-6">
|
||||
<div className="flex-1">
|
||||
<h2 className="text-3xl font-bold text-gray-800 mb-2">
|
||||
{searchResult.standardForm}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 ml-4">
|
||||
{session && folders.length > 0 && (
|
||||
<select
|
||||
value={selectedFolderId || ""}
|
||||
onChange={(e) => onFolderSelect(e.target.value ? Number(e.target.value) : null)}
|
||||
className="px-3 py-2 text-sm border border-gray-300 rounded-lg bg-white focus:outline-none focus:ring-2 focus:ring-[#35786f]"
|
||||
>
|
||||
{folders.map((folder) => (
|
||||
<option key={folder.id} value={folder.id}>
|
||||
{folder.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
<button
|
||||
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"
|
||||
title="保存到文件夹"
|
||||
>
|
||||
<Plus />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 条目列表 */}
|
||||
<div className="space-y-6">
|
||||
{searchResult.entries.map((entry, index) => (
|
||||
<div key={index} className="border-t border-gray-200 pt-4">
|
||||
<DictionaryEntry entry={entry} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 重新查询按钮 */}
|
||||
<div className="border-t border-gray-200 pt-4 mt-4">
|
||||
<button
|
||||
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"
|
||||
>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
重新查询
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
10
src/app/(features)/dictionary/constants.ts
Normal file
10
src/app/(features)/dictionary/constants.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export const POPULAR_LANGUAGES = [
|
||||
{ code: "english", name: "英语" },
|
||||
{ code: "chinese", name: "中文" },
|
||||
{ code: "japanese", name: "日语" },
|
||||
{ code: "korean", name: "韩语" },
|
||||
{ code: "french", name: "法语" },
|
||||
{ code: "german", name: "德语" },
|
||||
{ code: "italian", name: "意大利语" },
|
||||
{ code: "spanish", name: "西班牙语" },
|
||||
] as const;
|
||||
11
src/app/(features)/dictionary/index.ts
Normal file
11
src/app/(features)/dictionary/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
// 类型定义
|
||||
export * from "./types";
|
||||
|
||||
// 常量
|
||||
export * from "./constants";
|
||||
|
||||
// 组件
|
||||
export { default as DictionaryPage } from "./DictionaryPage";
|
||||
export { SearchForm } from "./SearchForm";
|
||||
export { SearchResult } from "./SearchResult";
|
||||
export { DictionaryEntry } from "./DictionaryEntry";
|
||||
@@ -1,398 +1 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import Container from "@/components/ui/Container";
|
||||
import { LightButton } from "@/components/ui/buttons";
|
||||
import { lookUp } from "@/lib/server/bigmodel/dictionaryActions";
|
||||
import { toast } from "sonner";
|
||||
import { Plus } from "lucide-react";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { Folder } from "../../../../generated/prisma/browser";
|
||||
import { getFoldersByUserId } from "@/lib/server/services/folderService";
|
||||
import { createPair } from "@/lib/server/services/pairService";
|
||||
|
||||
// 主流语言列表
|
||||
const POPULAR_LANGUAGES = [
|
||||
{ code: "english", name: "英语" },
|
||||
{ code: "chinese", name: "中文" },
|
||||
{ code: "japanese", name: "日语" },
|
||||
{ code: "korean", name: "韩语" },
|
||||
{ code: "french", name: "法语" },
|
||||
{ code: "german", name: "德语" },
|
||||
{ code: "italian", name: "意大利语" },
|
||||
{ code: "spanish", name: "西班牙语" },
|
||||
];
|
||||
|
||||
type DictionaryWordEntry = {
|
||||
ipa: string;
|
||||
definition: string;
|
||||
partOfSpeech: string;
|
||||
example: string;
|
||||
};
|
||||
|
||||
type DictionaryPhraseEntry = {
|
||||
definition: string;
|
||||
example: string;
|
||||
};
|
||||
|
||||
type DictionaryErrorResponse = {
|
||||
error: string;
|
||||
};
|
||||
|
||||
type DictionarySuccessResponse = {
|
||||
standardForm: string;
|
||||
entries: (DictionaryWordEntry | DictionaryPhraseEntry)[];
|
||||
};
|
||||
|
||||
type DictionaryResponse = DictionarySuccessResponse | DictionaryErrorResponse;
|
||||
|
||||
// 类型守卫:判断是否为单词条目
|
||||
function isWordEntry(entry: DictionaryWordEntry | DictionaryPhraseEntry): entry is DictionaryWordEntry {
|
||||
return "ipa" in entry && "partOfSpeech" in entry;
|
||||
}
|
||||
|
||||
// 类型守卫:判断是否为错误响应
|
||||
function isErrorResponse(response: DictionaryResponse): response is DictionaryErrorResponse {
|
||||
return "error" in response;
|
||||
}
|
||||
|
||||
export default function Dictionary() {
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [searchResult, setSearchResult] = useState<DictionaryResponse | null>(null);
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
const [hasSearched, setHasSearched] = useState(false);
|
||||
const [queryLang, setQueryLang] = useState("english");
|
||||
const [definitionLang, setDefinitionLang] = useState("chinese");
|
||||
const [showLangSettings, setShowLangSettings] = useState(false);
|
||||
const [selectedFolderId, setSelectedFolderId] = useState<number | null>(null);
|
||||
const [folders, setFolders] = useState<Folder[]>([]);
|
||||
const { data: session } = authClient.useSession();
|
||||
|
||||
// 加载用户的文件夹列表
|
||||
useEffect(() => {
|
||||
if (session) {
|
||||
getFoldersByUserId(session.user.id as string)
|
||||
.then((loadedFolders) => {
|
||||
setFolders(loadedFolders);
|
||||
// 如果有文件夹且未选择,默认选择第一个
|
||||
if (loadedFolders.length > 0 && !selectedFolderId) {
|
||||
setSelectedFolderId(loadedFolders[0].id);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [session, selectedFolderId]);
|
||||
|
||||
const handleSearch = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!searchQuery.trim()) return;
|
||||
|
||||
setIsSearching(true);
|
||||
setHasSearched(true);
|
||||
setSearchResult(null);
|
||||
|
||||
try {
|
||||
// 使用查询语言和释义语言
|
||||
const result = await lookUp(searchQuery, queryLang, definitionLang);
|
||||
|
||||
// 检查是否为错误响应
|
||||
if (isErrorResponse(result)) {
|
||||
toast.error(result.error);
|
||||
setSearchResult(null);
|
||||
} else {
|
||||
setSearchResult(result);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("词典查询失败:", error);
|
||||
toast.error("查询失败,请稍后重试");
|
||||
setSearchResult(null);
|
||||
} finally {
|
||||
setIsSearching(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-[calc(100vh-64px)] bg-[#35786f]">
|
||||
{/* 搜索区域 */}
|
||||
<div className="flex items-center justify-center px-4 py-12">
|
||||
<Container className="max-w-3xl w-full p-4">
|
||||
{/* 页面标题 */}
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-4xl md:text-5xl font-bold text-gray-800 mb-4">
|
||||
词典
|
||||
</h1>
|
||||
<p className="text-gray-700 text-lg">
|
||||
查询单词和短语,提供详细的释义和例句
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 搜索表单 */}
|
||||
<form onSubmit={handleSearch} className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSearchQuery(e.target.value)}
|
||||
placeholder="输入要查询的单词或短语..."
|
||||
className="flex-1 px-4 py-3 text-lg text-gray-800 focus:outline-none border-b-2 border-gray-600 bg-white/90 rounded"
|
||||
/>
|
||||
<LightButton
|
||||
type="submit"
|
||||
disabled={isSearching || !searchQuery.trim()}
|
||||
className="px-6 py-3"
|
||||
>
|
||||
{isSearching ? "查询中..." : "查询"}
|
||||
</LightButton>
|
||||
</form>
|
||||
|
||||
{/* 语言设置 */}
|
||||
<div className="mt-4 bg-white/20 rounded-lg p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className="text-gray-800 font-semibold">语言设置</span>
|
||||
<LightButton
|
||||
onClick={() => setShowLangSettings(!showLangSettings)}
|
||||
className="text-sm px-4 py-2"
|
||||
>
|
||||
{showLangSettings ? "收起" : "展开"}
|
||||
</LightButton>
|
||||
</div>
|
||||
|
||||
{showLangSettings && (
|
||||
<div className="space-y-4">
|
||||
{/* 查询语言 */}
|
||||
<div>
|
||||
<label className="block text-gray-700 text-sm mb-2">
|
||||
查询语言 (你要查询的单词/短语是什么语言)
|
||||
</label>
|
||||
<div className="flex flex-wrap gap-2 mb-2">
|
||||
{POPULAR_LANGUAGES.map((lang) => (
|
||||
<LightButton
|
||||
key={lang.code}
|
||||
selected={queryLang === lang.code}
|
||||
onClick={() => setQueryLang(lang.code)}
|
||||
className="text-sm px-3 py-1"
|
||||
>
|
||||
{lang.name}
|
||||
</LightButton>
|
||||
))}
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={queryLang}
|
||||
onChange={(e) => setQueryLang(e.target.value)}
|
||||
placeholder="或输入其他语言..."
|
||||
className="w-full px-3 py-2 text-sm text-gray-800 focus:outline-none border-b-2 border-gray-600 bg-white/90 rounded"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 释义语言 */}
|
||||
<div>
|
||||
<label className="block text-gray-700 text-sm mb-2">
|
||||
释义语言 (你希望用什么语言查看释义)
|
||||
</label>
|
||||
<div className="flex flex-wrap gap-2 mb-2">
|
||||
{POPULAR_LANGUAGES.map((lang) => (
|
||||
<LightButton
|
||||
key={lang.code}
|
||||
selected={definitionLang === lang.code}
|
||||
onClick={() => setDefinitionLang(lang.code)}
|
||||
className="text-sm px-3 py-1"
|
||||
>
|
||||
{lang.name}
|
||||
</LightButton>
|
||||
))}
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={definitionLang}
|
||||
onChange={(e) => setDefinitionLang(e.target.value)}
|
||||
placeholder="或输入其他语言..."
|
||||
className="w-full px-3 py-2 text-sm text-gray-800 focus:outline-none border-b-2 border-gray-600 bg-white/90 rounded"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 当前设置显示 */}
|
||||
<div className="text-center text-gray-700 text-sm pt-2 border-t border-gray-300">
|
||||
当前设置:查询 <span className="font-semibold">{POPULAR_LANGUAGES.find(l => l.code === queryLang)?.name || queryLang}</span>
|
||||
,释义 <span className="font-semibold">{POPULAR_LANGUAGES.find(l => l.code === definitionLang)?.name || definitionLang}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 搜索提示 */}
|
||||
<div className="mt-4 text-center text-gray-700 text-sm">
|
||||
<p>试试搜索:hello, look up, dictionary</p>
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
|
||||
{/* 搜索结果区域 */}
|
||||
<div className="flex-1 px-4 pb-12">
|
||||
<Container className="max-w-3xl w-full p-4">
|
||||
{isSearching && (
|
||||
<div className="text-center py-8">
|
||||
<div className="inline-block animate-spin rounded-full h-12 w-12 border-b-2 border-white"></div>
|
||||
<p className="mt-4 text-white">加载中...</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isSearching && hasSearched && !searchResult && (
|
||||
<div className="text-center py-12 bg-white/20 rounded-lg">
|
||||
<p className="text-gray-800 text-xl">未找到结果</p>
|
||||
<p className="text-gray-600 mt-2">尝试其他单词或短语</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isSearching && searchResult && !isErrorResponse(searchResult) && (
|
||||
<div className="space-y-6">
|
||||
<div className="bg-white rounded-lg p-6 shadow-lg">
|
||||
{/* 标题和保存按钮 */}
|
||||
<div className="flex items-start justify-between mb-6">
|
||||
<div className="flex-1">
|
||||
<h2 className="text-3xl font-bold text-gray-800 mb-2">
|
||||
{searchResult.standardForm}
|
||||
</h2>
|
||||
{searchResult.standardForm !== searchQuery && (
|
||||
<p className="text-gray-500 text-sm">
|
||||
原始输入: {searchQuery}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 ml-4">
|
||||
{session && folders.length > 0 && (
|
||||
<select
|
||||
value={selectedFolderId || ""}
|
||||
onChange={(e) => setSelectedFolderId(e.target.value ? Number(e.target.value) : null)}
|
||||
className="px-3 py-2 text-sm border border-gray-300 rounded-lg bg-white focus:outline-none focus:ring-2 focus:ring-[#35786f]"
|
||||
>
|
||||
{folders.map((folder) => (
|
||||
<option key={folder.id} value={folder.id}>
|
||||
{folder.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
<button
|
||||
onClick={() => {
|
||||
if (!session) {
|
||||
toast.error("请先登录");
|
||||
return;
|
||||
}
|
||||
if (!selectedFolderId) {
|
||||
toast.error("请先创建文件夹");
|
||||
return;
|
||||
}
|
||||
if (!searchResult || isErrorResponse(searchResult)) return;
|
||||
|
||||
const entry = searchResult.entries[0];
|
||||
createPair({
|
||||
text1: searchResult.standardForm,
|
||||
text2: entry.definition,
|
||||
language1: queryLang,
|
||||
language2: definitionLang,
|
||||
ipa1: isWordEntry(entry) ? entry.ipa : undefined,
|
||||
folder: {
|
||||
connect: {
|
||||
id: selectedFolderId,
|
||||
},
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
const folderName = folders.find(f => f.id === selectedFolderId)?.name;
|
||||
toast.success(`已保存到文件夹:${folderName}`);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("保存失败,请稍后重试");
|
||||
});
|
||||
}}
|
||||
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"
|
||||
title="保存到文件夹"
|
||||
>
|
||||
<Plus />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 条目列表 */}
|
||||
<div className="space-y-6">
|
||||
{searchResult.entries.map((entry, index) => (
|
||||
<div key={index} className="border-t border-gray-200 pt-4">
|
||||
{isWordEntry(entry) ? (
|
||||
// 单词条目
|
||||
<div>
|
||||
{/* 音标和词性 */}
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
{entry.ipa && (
|
||||
<span className="text-gray-600 text-lg">
|
||||
{entry.ipa}
|
||||
</span>
|
||||
)}
|
||||
{entry.partOfSpeech && (
|
||||
<span className="px-3 py-1 bg-[#35786f] text-white text-sm rounded-full">
|
||||
{entry.partOfSpeech}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 释义 */}
|
||||
<div className="mb-3">
|
||||
<h3 className="text-sm font-semibold text-gray-700 mb-1">
|
||||
释义
|
||||
</h3>
|
||||
<p className="text-gray-800">{entry.definition}</p>
|
||||
</div>
|
||||
|
||||
{/* 例句 */}
|
||||
{entry.example && (
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-gray-700 mb-1">
|
||||
例句
|
||||
</h3>
|
||||
<p className="text-gray-700 pl-4 border-l-4 border-[#35786f]">
|
||||
{entry.example}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
// 短语条目
|
||||
<div>
|
||||
{/* 释义 */}
|
||||
<div className="mb-3">
|
||||
<h3 className="text-sm font-semibold text-gray-700 mb-1">
|
||||
释义
|
||||
</h3>
|
||||
<p className="text-gray-800">{entry.definition}</p>
|
||||
</div>
|
||||
|
||||
{/* 例句 */}
|
||||
{entry.example && (
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-gray-700 mb-1">
|
||||
例句
|
||||
</h3>
|
||||
<p className="text-gray-700 pl-4 border-l-4 border-[#35786f]">
|
||||
{entry.example}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!hasSearched && (
|
||||
<div className="text-center py-12 bg-white/20 rounded-lg">
|
||||
<div className="text-6xl mb-4">📚</div>
|
||||
<p className="text-gray-800 text-xl mb-2">欢迎使用词典</p>
|
||||
<p className="text-gray-600">在上方搜索框中输入单词或短语开始查询</p>
|
||||
</div>
|
||||
)}
|
||||
</Container>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export { default } from "./DictionaryPage";
|
||||
|
||||
2
src/app/(features)/dictionary/types.ts
Normal file
2
src/app/(features)/dictionary/types.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
// 从 shared 文件夹导出所有词典类型和类型守卫
|
||||
export * from "@/lib/shared";
|
||||
@@ -2,99 +2,183 @@
|
||||
|
||||
import { parseAIGeneratedJSON } from "@/lib/utils";
|
||||
import { getAnswer } from "./zhipu";
|
||||
import { createLookUp, createPhrase, createWord, selectLastLookUp } from "../services/dictionaryService";
|
||||
import { DictLookUpRequest, DictWordResponse, isDictErrorResponse, isDictPhraseResponse, isDictWordResponse, type DictLookUpResponse } from "@/lib/shared";
|
||||
|
||||
type DictionaryWordEntry = {
|
||||
ipa: string;
|
||||
definition: string;
|
||||
partOfSpeech: string;
|
||||
example: string;
|
||||
const saveResult = async (req: DictLookUpRequest, res: DictLookUpResponse) => {
|
||||
if (isDictErrorResponse(res)) return;
|
||||
else if (isDictPhraseResponse(res)) {
|
||||
return createPhrase({
|
||||
standardForm: res.standardForm,
|
||||
queryLang: req.queryLang,
|
||||
definitionLang: req.definitionLang,
|
||||
lookups: {
|
||||
create: {
|
||||
user: req.userId ? {
|
||||
connect: {
|
||||
id: req.userId
|
||||
}
|
||||
} : undefined,
|
||||
text: req.text,
|
||||
queryLang: req.queryLang,
|
||||
definitionLang: req.definitionLang
|
||||
}
|
||||
},
|
||||
entries: {
|
||||
createMany: {
|
||||
data: res.entries
|
||||
}
|
||||
}
|
||||
});
|
||||
} else if (isDictWordResponse(res)) {
|
||||
return createWord({
|
||||
standardForm: (res as DictWordResponse).standardForm,
|
||||
queryLang: req.queryLang,
|
||||
definitionLang: req.definitionLang,
|
||||
lookups: {
|
||||
create: {
|
||||
user: req.userId ? {
|
||||
connect: {
|
||||
id: req.userId
|
||||
}
|
||||
} : undefined,
|
||||
text: req.text,
|
||||
queryLang: req.queryLang,
|
||||
definitionLang: req.definitionLang
|
||||
}
|
||||
},
|
||||
entries: {
|
||||
createMany: {
|
||||
data: (res as DictWordResponse).entries
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
type DictionaryPhraseEntry = {
|
||||
definition: string;
|
||||
example: string;
|
||||
};
|
||||
|
||||
type DictionaryErrorResponse = {
|
||||
error: string;
|
||||
};
|
||||
|
||||
type DictionarySuccessResponse = {
|
||||
standardForm: string;
|
||||
entries: (DictionaryWordEntry | DictionaryPhraseEntry)[];
|
||||
};
|
||||
|
||||
export const lookUp = async (
|
||||
text: string,
|
||||
queryLang: string,
|
||||
definitionLang: string
|
||||
): Promise<DictionarySuccessResponse | DictionaryErrorResponse> => {
|
||||
export const lookUp = async ({
|
||||
text,
|
||||
queryLang,
|
||||
definitionLang,
|
||||
userId,
|
||||
forceRelook = false
|
||||
}: DictLookUpRequest): Promise<DictLookUpResponse> => {
|
||||
try {
|
||||
const lastLookUp = await selectLastLookUp({
|
||||
text,
|
||||
queryLang,
|
||||
definitionLang
|
||||
});
|
||||
if (forceRelook || !lastLookUp) {
|
||||
const response = await getAnswer([
|
||||
{
|
||||
role: "system",
|
||||
content: `
|
||||
你是一个词典工具,返回单词/短语的JSON解释。
|
||||
你是一个词典工具,返回单词或短语的 JSON 解释结果。
|
||||
|
||||
查询语言:${queryLang}
|
||||
释义语言:${definitionLang}
|
||||
|
||||
用户输入在<text>标签内。判断是单词还是短语。
|
||||
用户输入在 <text> 标签内,判断是单词还是短语。
|
||||
|
||||
如果输入有效,返回JSON对象,格式为:
|
||||
语言规则:
|
||||
|
||||
若输入语言与查询语言一致,直接查询。
|
||||
|
||||
若不一致但语义清晰(如“吃”“跑”“睡觉”),先理解其语义,再映射到查询语言中最常见、最标准的对应词或短语(如 查询语言=意大利语,输入“吃” → mangiare)。
|
||||
|
||||
若语义不清晰或存在明显歧义,视为无效输入。
|
||||
|
||||
standardForm 规则:
|
||||
返回查询语言下的标准形式(英语动词原形、日语基本形、罗曼语族不定式等)。如无法确定,则与输入相同。
|
||||
|
||||
有效输入时返回:
|
||||
{
|
||||
"standardForm": "字符串,该语言下的正确形式",
|
||||
"entries": [数组,包含一个或多个条目]
|
||||
"standardForm": "标准形式",
|
||||
"entries": [...]
|
||||
}
|
||||
|
||||
如果是单词,条目格式:
|
||||
单词条目格式:
|
||||
{
|
||||
"ipa": "音标(如适用)",
|
||||
"definition": "释义",
|
||||
"definition": "释义(使用 ${definitionLang})",
|
||||
"partOfSpeech": "词性",
|
||||
"example": "例句"
|
||||
"example": "例句(使用 ${queryLang})"
|
||||
}
|
||||
|
||||
如果是短语,条目格式:
|
||||
短语条目格式:
|
||||
{
|
||||
"definition": "短语释义",
|
||||
"example": "例句"
|
||||
"definition": "释义(使用 ${definitionLang})",
|
||||
"example": "例句(使用 ${queryLang})"
|
||||
}
|
||||
|
||||
所有释义内容使用${definitionLang}语言。
|
||||
例句使用${queryLang}语言。
|
||||
|
||||
如果输入无效(如:输入为空、包含非法字符、无法识别的语言等),返回JSON对象:
|
||||
无效输入返回:
|
||||
{
|
||||
"error": "错误描述信息,使用${definitionLang}语言"
|
||||
"error": "错误信息(使用 ${definitionLang})"
|
||||
}
|
||||
|
||||
提供standardForm时:尝试修正笔误或返回原形(如英语动词原形、日语基本形等)。若无法确定或输入正确,则与输入相同。
|
||||
|
||||
示例:
|
||||
英语输入"ran" -> standardForm: "run"
|
||||
中文输入"跑眬" -> standardForm: "跑"
|
||||
日语输入"走った" -> standardForm: "走る"
|
||||
|
||||
短语同理,尝试返回其标准/常见形式。
|
||||
|
||||
现在处理用户输入。
|
||||
只输出 JSON,不附加任何解释性文字。
|
||||
`.trim()
|
||||
}, {
|
||||
role: "user",
|
||||
content: `<text>${text}</text>请处理text标签内的内容后返回给我json`
|
||||
}
|
||||
]);
|
||||
|
||||
const result = parseAIGeneratedJSON<
|
||||
DictionaryErrorResponse |
|
||||
{
|
||||
standardForm: string,
|
||||
entries: DictionaryPhraseEntry[];
|
||||
} |
|
||||
{
|
||||
standardForm: string,
|
||||
entries: DictionaryWordEntry[];
|
||||
}>(response);
|
||||
|
||||
return result;
|
||||
]).then(parseAIGeneratedJSON<DictLookUpResponse>);
|
||||
saveResult({
|
||||
text,
|
||||
queryLang,
|
||||
definitionLang,
|
||||
userId,
|
||||
forceRelook
|
||||
}, response);
|
||||
return response;
|
||||
} else {
|
||||
if (lastLookUp.dictionaryWordId) {
|
||||
createLookUp({
|
||||
user: userId ? {
|
||||
connect: {
|
||||
id: userId
|
||||
}
|
||||
} : undefined,
|
||||
text: text,
|
||||
queryLang: queryLang,
|
||||
definitionLang: definitionLang,
|
||||
dictionaryWord: {
|
||||
connect: {
|
||||
id: lastLookUp.dictionaryWordId,
|
||||
}
|
||||
}
|
||||
});
|
||||
return {
|
||||
standardForm: lastLookUp.dictionaryWord!.standardForm,
|
||||
entries: lastLookUp.dictionaryWord!.entries
|
||||
};
|
||||
} else if (lastLookUp.dictionaryPhraseId) {
|
||||
createLookUp({
|
||||
user: userId ? {
|
||||
connect: {
|
||||
id: userId
|
||||
}
|
||||
} : undefined,
|
||||
text: text,
|
||||
queryLang: queryLang,
|
||||
definitionLang: definitionLang,
|
||||
dictionaryPhrase: {
|
||||
connect: {
|
||||
id: lastLookUp.dictionaryPhraseId
|
||||
}
|
||||
}
|
||||
});
|
||||
return {
|
||||
standardForm: lastLookUp.dictionaryPhrase!.standardForm,
|
||||
entries: lastLookUp.dictionaryPhrase!.entries
|
||||
};
|
||||
} else {
|
||||
return { error: "Database structure error!" };
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return { error: "LOOK_UP_ERROR" };
|
||||
}
|
||||
};
|
||||
|
||||
56
src/lib/server/services/dictionaryService.ts
Normal file
56
src/lib/server/services/dictionaryService.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
"use server";
|
||||
|
||||
import { DictionaryLookUpCreateInput, DictionaryLookUpWhereInput, DictionaryPhraseCreateInput, DictionaryPhraseEntryCreateInput, DictionaryWordCreateInput, DictionaryWordEntryCreateInput } from "../../../../generated/prisma/models";
|
||||
import prisma from "../../db";
|
||||
|
||||
export async function selectLastLookUp(content: DictionaryLookUpWhereInput) {
|
||||
const lookUp = await prisma.dictionaryLookUp.findFirst({
|
||||
where: content,
|
||||
include: {
|
||||
dictionaryPhrase: {
|
||||
include: {
|
||||
entries: true
|
||||
}
|
||||
},
|
||||
dictionaryWord: {
|
||||
include: {
|
||||
entries: true
|
||||
}
|
||||
}
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc'
|
||||
}
|
||||
});
|
||||
return lookUp;
|
||||
}
|
||||
|
||||
export async function createPhraseEntry(content: DictionaryPhraseEntryCreateInput) {
|
||||
return await prisma.dictionaryPhraseEntry.create({
|
||||
data: content
|
||||
});
|
||||
}
|
||||
|
||||
export async function createWordEntry(content: DictionaryWordEntryCreateInput) {
|
||||
return await prisma.dictionaryWordEntry.create({
|
||||
data: content
|
||||
});
|
||||
}
|
||||
|
||||
export async function createPhrase(content: DictionaryPhraseCreateInput) {
|
||||
return await prisma.dictionaryPhrase.create({
|
||||
data: content
|
||||
});
|
||||
}
|
||||
|
||||
export async function createWord(content: DictionaryWordCreateInput) {
|
||||
return await prisma.dictionaryWord.create({
|
||||
data: content
|
||||
});
|
||||
}
|
||||
|
||||
export async function createLookUp(content: DictionaryLookUpCreateInput) {
|
||||
return await prisma.dictionaryLookUp.create({
|
||||
data: content
|
||||
});
|
||||
}
|
||||
63
src/lib/shared/dictionaryTypes.ts
Normal file
63
src/lib/shared/dictionaryTypes.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
export type DictLookUpRequest = {
|
||||
text: string,
|
||||
queryLang: string,
|
||||
definitionLang: string,
|
||||
userId?: string,
|
||||
forceRelook: boolean;
|
||||
};
|
||||
|
||||
export type DictWordEntry = {
|
||||
ipa: string;
|
||||
definition: string;
|
||||
partOfSpeech: string;
|
||||
example: string;
|
||||
};
|
||||
|
||||
export type DictPhraseEntry = {
|
||||
definition: string;
|
||||
example: string;
|
||||
};
|
||||
|
||||
export type DictErrorResponse = {
|
||||
error: string;
|
||||
};
|
||||
|
||||
export type DictWordResponse = {
|
||||
standardForm: string;
|
||||
entries: DictWordEntry[];
|
||||
};
|
||||
|
||||
export type DictPhraseResponse = {
|
||||
standardForm: string;
|
||||
entries: DictPhraseEntry[];
|
||||
};
|
||||
|
||||
export type DictLookUpResponse =
|
||||
| DictErrorResponse
|
||||
| DictWordResponse
|
||||
| DictPhraseResponse;
|
||||
|
||||
// 类型守卫:判断是否为错误响应
|
||||
export function isDictErrorResponse(
|
||||
response: DictLookUpResponse
|
||||
): response is DictErrorResponse {
|
||||
return "error" in response;
|
||||
}
|
||||
|
||||
// 类型守卫:判断是否为单词响应
|
||||
export function isDictWordResponse(
|
||||
response: DictLookUpResponse
|
||||
): response is DictWordResponse {
|
||||
if (isDictErrorResponse(response)) return false;
|
||||
const entries = (response as DictWordResponse | DictPhraseResponse).entries;
|
||||
return entries.length > 0 && "ipa" in entries[0] && "partOfSpeech" in entries[0];
|
||||
}
|
||||
|
||||
// 类型守卫:判断是否为短语响应
|
||||
export function isDictPhraseResponse(
|
||||
response: DictLookUpResponse
|
||||
): response is DictPhraseResponse {
|
||||
if (isDictErrorResponse(response)) return false;
|
||||
const entries = (response as DictWordResponse | DictPhraseResponse).entries;
|
||||
return entries.length > 0 && !("ipa" in entries[0] || "partOfSpeech" in entries[0]);
|
||||
}
|
||||
1
src/lib/shared/index.ts
Normal file
1
src/lib/shared/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./dictionaryTypes";
|
||||
Reference in New Issue
Block a user