...
This commit is contained in:
@@ -1,29 +1,37 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
import { LightButton } from "@/components/ui/buttons";
|
import { LightButton } from "@/components/ui/buttons";
|
||||||
import { POPULAR_LANGUAGES } from "./constants";
|
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { POPULAR_LANGUAGES } from "./constants";
|
||||||
|
|
||||||
interface SearchFormProps {
|
interface SearchFormProps {
|
||||||
searchQuery: string;
|
defaultQueryLang?: string;
|
||||||
onSearchQueryChange: (query: string) => void;
|
defaultDefinitionLang?: string;
|
||||||
isSearching: boolean;
|
|
||||||
onSearch: (e: React.FormEvent) => void;
|
|
||||||
queryLang: string;
|
|
||||||
onQueryLangChange: (lang: string) => void;
|
|
||||||
definitionLang: string;
|
|
||||||
onDefinitionLangChange: (lang: string) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SearchForm({
|
export function SearchForm({ defaultQueryLang = "english", defaultDefinitionLang = "chinese" }: SearchFormProps) {
|
||||||
searchQuery,
|
|
||||||
onSearchQueryChange,
|
|
||||||
isSearching,
|
|
||||||
onSearch,
|
|
||||||
queryLang,
|
|
||||||
onQueryLangChange,
|
|
||||||
definitionLang,
|
|
||||||
onDefinitionLangChange,
|
|
||||||
}: SearchFormProps) {
|
|
||||||
const t = useTranslations("dictionary");
|
const t = useTranslations("dictionary");
|
||||||
|
const [queryLang, setQueryLang] = useState(defaultQueryLang);
|
||||||
|
const [definitionLang, setDefinitionLang] = useState(defaultDefinitionLang);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const formData = new FormData(e.currentTarget);
|
||||||
|
const searchQuery = formData.get("searchQuery") as string;
|
||||||
|
|
||||||
|
if (!searchQuery?.trim()) return;
|
||||||
|
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
q: searchQuery,
|
||||||
|
ql: queryLang,
|
||||||
|
dl: definitionLang,
|
||||||
|
});
|
||||||
|
|
||||||
|
router.push(`/dictionary?${params.toString()}`);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -38,20 +46,20 @@ export function SearchForm({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 搜索表单 */}
|
{/* 搜索表单 */}
|
||||||
<form onSubmit={onSearch} className="flex flex-col sm:flex-row gap-2">
|
<form onSubmit={handleSubmit} className="flex flex-col sm:flex-row gap-2">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={searchQuery}
|
name="searchQuery"
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => onSearchQueryChange(e.target.value)}
|
defaultValue=""
|
||||||
placeholder={t("searchPlaceholder")}
|
placeholder={t("searchPlaceholder")}
|
||||||
className="flex-1 min-w-0 px-4 py-3 text-lg text-gray-800 focus:outline-none border-b-2 border-gray-600 bg-white/90 rounded"
|
className="flex-1 min-w-0 px-4 py-3 text-lg text-gray-800 focus:outline-none border-b-2 border-gray-600 bg-white/90 rounded"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
<LightButton
|
<LightButton
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={isSearching || !searchQuery.trim()}
|
|
||||||
className="px-6 py-3 whitespace-nowrap text-center sm:min-w-30"
|
className="px-6 py-3 whitespace-nowrap text-center sm:min-w-30"
|
||||||
>
|
>
|
||||||
{isSearching ? t("searching") : t("search")}
|
{t("search")}
|
||||||
</LightButton>
|
</LightButton>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@@ -62,68 +70,47 @@ export function SearchForm({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* 查询语言 */}
|
{/* 查询语言 */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-gray-700 text-sm mb-2">
|
<label className="block text-gray-700 text-sm mb-2">
|
||||||
{t("queryLanguage")} ({t("queryLanguageHint")})
|
{t("queryLanguage")} ({t("queryLanguageHint")})
|
||||||
</label>
|
</label>
|
||||||
<div className="flex flex-wrap gap-2 mb-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{POPULAR_LANGUAGES.map((lang) => (
|
{POPULAR_LANGUAGES.map((lang) => (
|
||||||
<LightButton
|
<LightButton
|
||||||
key={lang.code}
|
key={lang.code}
|
||||||
selected={queryLang === lang.code}
|
type="button"
|
||||||
onClick={() => onQueryLangChange(lang.code)}
|
selected={queryLang === lang.code}
|
||||||
className="text-sm px-3 py-1"
|
onClick={() => setQueryLang(lang.code)}
|
||||||
>
|
className="text-sm px-3 py-1"
|
||||||
{lang.nativeName}
|
>
|
||||||
</LightButton>
|
{lang.nativeName}
|
||||||
))}
|
</LightButton>
|
||||||
</div>
|
))}
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={queryLang}
|
|
||||||
onChange={(e) => onQueryLangChange(e.target.value)}
|
|
||||||
placeholder={t("otherLanguagePlaceholder")}
|
|
||||||
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">
|
|
||||||
{t("definitionLanguage")} ({t("definitionLanguageHint")})
|
|
||||||
</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.nativeName}
|
|
||||||
</LightButton>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={definitionLang}
|
|
||||||
onChange={(e) => onDefinitionLangChange(e.target.value)}
|
|
||||||
placeholder={t("otherLanguagePlaceholder")}
|
|
||||||
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">
|
|
||||||
{t("currentSettings", {
|
|
||||||
queryLang: POPULAR_LANGUAGES.find(l => l.code === queryLang)?.nativeName || queryLang,
|
|
||||||
definitionLang: POPULAR_LANGUAGES.find(l => l.code === definitionLang)?.nativeName || definitionLang
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
|
{/* 释义语言 */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-gray-700 text-sm mb-2">
|
||||||
|
{t("definitionLanguage")} ({t("definitionLanguageHint")})
|
||||||
|
</label>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{POPULAR_LANGUAGES.map((lang) => (
|
||||||
|
<LightButton
|
||||||
|
key={lang.code}
|
||||||
|
type="button"
|
||||||
|
selected={definitionLang === lang.code}
|
||||||
|
onClick={() => setDefinitionLang(lang.code)}
|
||||||
|
className="text-sm px-3 py-1"
|
||||||
|
>
|
||||||
|
{lang.nativeName}
|
||||||
|
</LightButton>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
121
src/app/(features)/dictionary/SearchResult.client.tsx
Normal file
121
src/app/(features)/dictionary/SearchResult.client.tsx
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Plus, RefreshCw } from "lucide-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { actionCreatePair } from "@/modules/folder/folder-aciton";
|
||||||
|
import { TSharedItem } from "@/shared/dictionary-type";
|
||||||
|
import { TSharedFolder } from "@/shared/folder-type";
|
||||||
|
import { actionLookUpDictionary } from "@/modules/dictionary/dictionary-action";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
|
type Session = {
|
||||||
|
user: {
|
||||||
|
id: string;
|
||||||
|
name?: string | null;
|
||||||
|
email?: string | null;
|
||||||
|
image?: string | null;
|
||||||
|
};
|
||||||
|
} | null;
|
||||||
|
|
||||||
|
interface SaveButtonClientProps {
|
||||||
|
session: Session;
|
||||||
|
folders: TSharedFolder[];
|
||||||
|
searchResult: TSharedItem;
|
||||||
|
queryLang: string;
|
||||||
|
definitionLang: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SaveButtonClient({ session, folders, searchResult, queryLang, definitionLang }: SaveButtonClientProps) {
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (!session) {
|
||||||
|
toast.error("Please login first");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (folders.length === 0) {
|
||||||
|
toast.error("Please create a folder first");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const folderSelect = document.getElementById("folder-select") as HTMLSelectElement;
|
||||||
|
const folderId = folderSelect?.value ? Number(folderSelect.value) : folders[0]?.id;
|
||||||
|
|
||||||
|
const definition = searchResult.entries.reduce((p, e) => {
|
||||||
|
return { ...p, definition: p.definition + ' | ' + e.definition };
|
||||||
|
}).definition;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await actionCreatePair({
|
||||||
|
text1: searchResult.standardForm,
|
||||||
|
text2: definition,
|
||||||
|
language1: queryLang,
|
||||||
|
language2: definitionLang,
|
||||||
|
ipa1: searchResult.entries[0].ipa,
|
||||||
|
folderId: folderId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const folderName = folders.find((f) => f.id === folderId)?.name || "Unknown";
|
||||||
|
toast.success(`Saved to ${folderName}`);
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("Save failed");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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="Save to folder"
|
||||||
|
>
|
||||||
|
<Plus />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ReLookupButtonClientProps {
|
||||||
|
searchQuery: string;
|
||||||
|
queryLang: string;
|
||||||
|
definitionLang: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ReLookupButtonClient({ searchQuery, queryLang, definitionLang }: ReLookupButtonClientProps) {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const handleRelookup = async () => {
|
||||||
|
const getNativeName = (code: string): string => {
|
||||||
|
const popularLanguages: Record<string, string> = {
|
||||||
|
english: "English",
|
||||||
|
chinese: "中文",
|
||||||
|
japanese: "日本語",
|
||||||
|
korean: "한국어",
|
||||||
|
italian: "Italiano",
|
||||||
|
uyghur: "ئۇيغۇرچە",
|
||||||
|
};
|
||||||
|
return popularLanguages[code] || code;
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await actionLookUpDictionary({
|
||||||
|
text: searchQuery,
|
||||||
|
queryLang: getNativeName(queryLang),
|
||||||
|
definitionLang: getNativeName(definitionLang),
|
||||||
|
forceRelook: true
|
||||||
|
});
|
||||||
|
|
||||||
|
toast.success("Re-lookup successful");
|
||||||
|
// 刷新页面以显示新结果
|
||||||
|
router.refresh();
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("Re-lookup failed");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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" />
|
||||||
|
Re-lookup
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,143 +1,93 @@
|
|||||||
import { Plus, RefreshCw } from "lucide-react";
|
import { auth } from "@/auth";
|
||||||
import { toast } from "sonner";
|
|
||||||
import { authClient } from "@/lib/auth-client";
|
|
||||||
import { DictionaryEntry } from "./DictionaryEntry";
|
import { DictionaryEntry } from "./DictionaryEntry";
|
||||||
import { useTranslations } from "next-intl";
|
|
||||||
import { performDictionaryLookup } from "./utils";
|
|
||||||
import { TSharedItem } from "@/shared/dictionary-type";
|
import { TSharedItem } from "@/shared/dictionary-type";
|
||||||
|
import { SaveButtonClient, ReLookupButtonClient } from "./SearchResult.client";
|
||||||
|
import { headers } from "next/headers";
|
||||||
|
import { actionGetFoldersByUserId } from "@/modules/folder/folder-aciton";
|
||||||
import { TSharedFolder } from "@/shared/folder-type";
|
import { TSharedFolder } from "@/shared/folder-type";
|
||||||
import { actionCreatePair } from "@/modules/folder/folder-aciton";
|
|
||||||
|
|
||||||
interface SearchResultProps {
|
interface SearchResultProps {
|
||||||
searchResult: TSharedItem;
|
searchResult: TSharedItem | null;
|
||||||
searchQuery: string;
|
searchQuery: string;
|
||||||
queryLang: string;
|
queryLang: string;
|
||||||
definitionLang: string;
|
definitionLang: string;
|
||||||
folders: TSharedFolder[];
|
|
||||||
selectedFolderId: number | null;
|
|
||||||
onFolderSelect: (folderId: number | null) => void;
|
|
||||||
onResultUpdate: (newResult: TSharedItem) => void;
|
|
||||||
onSearchingChange: (isSearching: boolean) => void;
|
|
||||||
getNativeName: (code: string) => string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SearchResult({
|
export async function SearchResult({
|
||||||
searchResult,
|
searchResult,
|
||||||
searchQuery,
|
searchQuery,
|
||||||
queryLang,
|
queryLang,
|
||||||
definitionLang,
|
definitionLang
|
||||||
folders,
|
|
||||||
selectedFolderId,
|
|
||||||
onFolderSelect,
|
|
||||||
onResultUpdate,
|
|
||||||
onSearchingChange,
|
|
||||||
getNativeName,
|
|
||||||
}: SearchResultProps) {
|
}: SearchResultProps) {
|
||||||
const t = useTranslations("dictionary");
|
// 获取用户会话和文件夹
|
||||||
const { data: session } = authClient.useSession();
|
const session = await auth.api.getSession({ headers: await headers() });
|
||||||
|
let folders: TSharedFolder[] = [];
|
||||||
|
|
||||||
const handleRelookup = async () => {
|
if (session?.user?.id) {
|
||||||
onSearchingChange(true);
|
const result = await actionGetFoldersByUserId(session.user.id as string);
|
||||||
|
if (result.success && result.data) {
|
||||||
const result = await performDictionaryLookup(
|
folders = result.data;
|
||||||
{
|
|
||||||
text: searchQuery,
|
|
||||||
queryLang: getNativeName(queryLang),
|
|
||||||
definitionLang: getNativeName(definitionLang),
|
|
||||||
forceRelook: true
|
|
||||||
},
|
|
||||||
t
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result) {
|
|
||||||
onResultUpdate(result);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
onSearchingChange(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSave = () => {
|
|
||||||
if (!session) {
|
|
||||||
toast.error(t("pleaseLogin"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!selectedFolderId) {
|
|
||||||
toast.error(t("pleaseCreateFolder"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const entry = searchResult.entries[0];
|
|
||||||
actionCreatePair({
|
|
||||||
text1: searchResult.standardForm,
|
|
||||||
text2: entry.definition,
|
|
||||||
language1: queryLang,
|
|
||||||
language2: definitionLang,
|
|
||||||
ipa1: entry.ipa,
|
|
||||||
folderId: selectedFolderId,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
const folderName = folders.find(f => f.id === selectedFolderId)?.name || "Unknown";
|
|
||||||
toast.success(t("savedToFolder", { folderName }));
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error(t("saveFailed"));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="bg-white rounded-lg p-6 shadow-lg">
|
{!searchResult ? (
|
||||||
{/* 标题和保存按钮 */}
|
<div className="text-center py-12 bg-white/20 rounded-lg">
|
||||||
<div className="flex items-start justify-between mb-6">
|
<p className="text-gray-800 text-xl">No results found</p>
|
||||||
<div className="flex-1">
|
<p className="text-gray-600 mt-2">Try other words</p>
|
||||||
<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={t("saveToFolder")}
|
|
||||||
>
|
|
||||||
<Plus />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
{/* 条目列表 */}
|
<div className="bg-white rounded-lg p-6 shadow-lg">
|
||||||
<div className="space-y-6">
|
{/* 标题和保存按钮 */}
|
||||||
{searchResult.entries.map((entry, index) => (
|
<div className="flex items-start justify-between mb-6">
|
||||||
<div key={index} className="border-t border-gray-200 pt-4">
|
<div className="flex-1">
|
||||||
<DictionaryEntry entry={entry} />
|
<h2 className="text-3xl font-bold text-gray-800 mb-2">
|
||||||
|
{searchResult.standardForm}
|
||||||
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
))}
|
<div className="flex items-center gap-2 ml-4">
|
||||||
</div>
|
{session && folders.length > 0 && (
|
||||||
|
<select
|
||||||
|
id="folder-select"
|
||||||
|
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>
|
||||||
|
)}
|
||||||
|
<SaveButtonClient
|
||||||
|
session={session}
|
||||||
|
folders={folders}
|
||||||
|
searchResult={searchResult}
|
||||||
|
queryLang={queryLang}
|
||||||
|
definitionLang={definitionLang}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 重新查询按钮 */}
|
{/* 条目列表 */}
|
||||||
<div className="border-t border-gray-200 pt-4 mt-4">
|
<div className="space-y-6">
|
||||||
<button
|
{searchResult.entries.map((entry, index) => (
|
||||||
onClick={handleRelookup}
|
<div key={index} className="border-t border-gray-200 pt-4">
|
||||||
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"
|
<DictionaryEntry entry={entry} />
|
||||||
>
|
</div>
|
||||||
<RefreshCw className="w-4 h-4" />
|
))}
|
||||||
{t("relookup")}
|
</div>
|
||||||
</button>
|
|
||||||
|
{/* 重新查询按钮 */}
|
||||||
|
<div className="border-t border-gray-200 pt-4 mt-4">
|
||||||
|
<ReLookupButtonClient
|
||||||
|
searchQuery={searchQuery}
|
||||||
|
queryLang={queryLang}
|
||||||
|
definitionLang={definitionLang}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,73 +1,46 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
|
||||||
import { Container } from "@/components/ui/Container";
|
import { Container } from "@/components/ui/Container";
|
||||||
import { authClient } from "@/lib/auth-client";
|
|
||||||
import { SearchForm } from "./SearchForm";
|
import { SearchForm } from "./SearchForm";
|
||||||
import { SearchResult } from "./SearchResult";
|
import { SearchResult } from "./SearchResult";
|
||||||
import { useTranslations } from "next-intl";
|
import { getTranslations } from "next-intl/server";
|
||||||
import { POPULAR_LANGUAGES } from "./constants";
|
import { actionLookUpDictionary } from "@/modules/dictionary/dictionary-action";
|
||||||
import { performDictionaryLookup } from "./utils";
|
|
||||||
import { TSharedItem } from "@/shared/dictionary-type";
|
import { TSharedItem } from "@/shared/dictionary-type";
|
||||||
import { actionGetFoldersByUserId } from "@/modules/folder/folder-aciton";
|
|
||||||
import { TSharedFolder } from "@/shared/folder-type";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
|
|
||||||
export default function DictionaryPage() {
|
interface DictionaryPageProps {
|
||||||
const t = useTranslations("dictionary");
|
searchParams: Promise<{ q?: string; ql?: string; dl?: string; }>;
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
}
|
||||||
const [searchResult, setSearchResult] = useState<TSharedItem | 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<TSharedFolder[]>([]);
|
|
||||||
const { data: session } = authClient.useSession();
|
|
||||||
|
|
||||||
// 加载用户的文件夹列表
|
export default async function DictionaryPage({ searchParams }: DictionaryPageProps) {
|
||||||
useEffect(() => {
|
const t = await getTranslations("dictionary");
|
||||||
if (session) {
|
|
||||||
actionGetFoldersByUserId(session.user.id as string)
|
// 从 searchParams 获取搜索参数
|
||||||
.then(result => {
|
const { q: searchQuery, ql: queryLang = "english", dl: definitionLang = "chinese" } = await searchParams;
|
||||||
if (!result.success || !result.data) throw result.message;
|
|
||||||
return result.data;
|
// 如果有搜索查询,获取搜索结果
|
||||||
})
|
let searchResult: TSharedItem | undefined | null = null;
|
||||||
.then((loadedFolders) => {
|
if (searchQuery) {
|
||||||
setFolders(loadedFolders);
|
const getNativeName = (code: string): string => {
|
||||||
// 如果有文件夹且未选择,默认选择第一个
|
const popularLanguages: Record<string, string> = {
|
||||||
if (loadedFolders.length > 0 && !selectedFolderId) {
|
english: "English",
|
||||||
setSelectedFolderId(loadedFolders[0].id);
|
chinese: "中文",
|
||||||
}
|
japanese: "日本語",
|
||||||
}).catch(e => toast.error);
|
korean: "한국어",
|
||||||
|
italian: "Italiano",
|
||||||
|
uyghur: "ئۇيغۇرچە",
|
||||||
|
};
|
||||||
|
return popularLanguages[code] || code;
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await actionLookUpDictionary({
|
||||||
|
text: searchQuery,
|
||||||
|
queryLang: getNativeName(queryLang),
|
||||||
|
definitionLang: getNativeName(definitionLang),
|
||||||
|
forceRelook: false
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.success && result.data) {
|
||||||
|
searchResult = result.data;
|
||||||
}
|
}
|
||||||
}, [session, selectedFolderId]);
|
}
|
||||||
|
|
||||||
// 将 code 转换为 nativeName
|
|
||||||
const getNativeName = (code: string) => {
|
|
||||||
return POPULAR_LANGUAGES.find(l => l.code === code)?.nativeName || code;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSearch = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (!searchQuery.trim()) return;
|
|
||||||
|
|
||||||
setIsSearching(true);
|
|
||||||
setHasSearched(true);
|
|
||||||
setSearchResult(null);
|
|
||||||
|
|
||||||
const result = await performDictionaryLookup(
|
|
||||||
{
|
|
||||||
text: searchQuery,
|
|
||||||
queryLang: getNativeName(queryLang),
|
|
||||||
definitionLang: getNativeName(definitionLang),
|
|
||||||
forceRelook: false
|
|
||||||
},
|
|
||||||
t
|
|
||||||
);
|
|
||||||
setSearchResult(result);
|
|
||||||
setIsSearching(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-[calc(100vh-64px)] bg-[#35786f]">
|
<div className="min-h-[calc(100vh-64px)] bg-[#35786f]">
|
||||||
@@ -75,14 +48,8 @@ export default function DictionaryPage() {
|
|||||||
<div className="flex items-center justify-center px-4 py-12">
|
<div className="flex items-center justify-center px-4 py-12">
|
||||||
<Container className="max-w-3xl w-full p-4">
|
<Container className="max-w-3xl w-full p-4">
|
||||||
<SearchForm
|
<SearchForm
|
||||||
searchQuery={searchQuery}
|
defaultQueryLang={queryLang}
|
||||||
onSearchQueryChange={setSearchQuery}
|
defaultDefinitionLang={definitionLang}
|
||||||
isSearching={isSearching}
|
|
||||||
onSearch={handleSearch}
|
|
||||||
queryLang={queryLang}
|
|
||||||
onQueryLangChange={setQueryLang}
|
|
||||||
definitionLang={definitionLang}
|
|
||||||
onDefinitionLangChange={setDefinitionLang}
|
|
||||||
/>
|
/>
|
||||||
</Container>
|
</Container>
|
||||||
</div>
|
</div>
|
||||||
@@ -90,36 +57,15 @@ export default function DictionaryPage() {
|
|||||||
{/* 搜索结果区域 */}
|
{/* 搜索结果区域 */}
|
||||||
<div className="flex-1 px-4 pb-12">
|
<div className="flex-1 px-4 pb-12">
|
||||||
<Container className="max-w-3xl w-full p-4">
|
<Container className="max-w-3xl w-full p-4">
|
||||||
{isSearching && (
|
{searchQuery && (
|
||||||
<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">{t("loading")}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!isSearching && hasSearched && !searchResult && (
|
|
||||||
<div className="text-center py-12 bg-white/20 rounded-lg">
|
|
||||||
<p className="text-gray-800 text-xl">{t("noResults")}</p>
|
|
||||||
<p className="text-gray-600 mt-2">{t("tryOtherWords")}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!isSearching && searchResult && (
|
|
||||||
<SearchResult
|
<SearchResult
|
||||||
searchResult={searchResult}
|
searchResult={searchResult}
|
||||||
searchQuery={searchQuery}
|
searchQuery={searchQuery}
|
||||||
queryLang={queryLang}
|
queryLang={queryLang}
|
||||||
definitionLang={definitionLang}
|
definitionLang={definitionLang}
|
||||||
folders={folders}
|
|
||||||
selectedFolderId={selectedFolderId}
|
|
||||||
onFolderSelect={setSelectedFolderId}
|
|
||||||
onResultUpdate={setSearchResult}
|
|
||||||
onSearchingChange={setIsSearching}
|
|
||||||
getNativeName={getNativeName}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{!searchQuery && (
|
||||||
{!hasSearched && (
|
|
||||||
<div className="text-center py-12 bg-white/20 rounded-lg">
|
<div className="text-center py-12 bg-white/20 rounded-lg">
|
||||||
<div className="text-6xl mb-4">📚</div>
|
<div className="text-6xl mb-4">📚</div>
|
||||||
<p className="text-gray-800 text-xl mb-2">{t("welcomeTitle")}</p>
|
<p className="text-gray-800 text-xl mb-2">{t("welcomeTitle")}</p>
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
import { toast } from "sonner";
|
|
||||||
import { actionLookUpDictionary } from "@/modules/dictionary/dictionary-action";
|
|
||||||
import { ActionInputLookUpDictionary, ActionOutputLookUpDictionary } from "@/modules/dictionary/dictionary-action-dto";
|
|
||||||
import { TSharedItem } from "@/shared/dictionary-type";
|
|
||||||
|
|
||||||
export async function performDictionaryLookup(
|
|
||||||
options: ActionInputLookUpDictionary,
|
|
||||||
t?: (key: string) => string
|
|
||||||
): Promise<TSharedItem | null> {
|
|
||||||
const { text, queryLang, definitionLang, forceRelook = false, userId } = options;
|
|
||||||
const result = await actionLookUpDictionary({
|
|
||||||
text,
|
|
||||||
queryLang,
|
|
||||||
definitionLang,
|
|
||||||
forceRelook,
|
|
||||||
userId
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!result.success || !result.data) return null;
|
|
||||||
|
|
||||||
if (forceRelook && t) {
|
|
||||||
toast.success(t("relookupSuccess"));
|
|
||||||
}
|
|
||||||
return result.data;
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user