重构
This commit is contained in:
@@ -1,75 +1,42 @@
|
||||
import { DictWordEntry, DictPhraseEntry } from "./types";
|
||||
import { TSharedEntry } from "@/shared";
|
||||
|
||||
interface DictionaryEntryProps {
|
||||
entry: DictWordEntry | DictPhraseEntry;
|
||||
entry: TSharedEntry;
|
||||
}
|
||||
|
||||
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="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">{phraseEntry.definition}</p>
|
||||
<p className="text-gray-800">{entry.definition}</p>
|
||||
</div>
|
||||
|
||||
{/* 例句 */}
|
||||
{phraseEntry.example && (
|
||||
{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]">
|
||||
{phraseEntry.example}
|
||||
{entry.example}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import Container from "@/components/ui/Container";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { Folder } from "../../../../generated/prisma/browser";
|
||||
import { getFoldersByUserId } from "@/lib/server/services/folderService";
|
||||
import { DictLookUpResponse } from "./types";
|
||||
import { SearchForm } from "./SearchForm";
|
||||
import { SearchResult } from "./SearchResult";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { POPULAR_LANGUAGES } from "./constants";
|
||||
import { performDictionaryLookup } from "./utils";
|
||||
|
||||
export default function Dictionary() {
|
||||
const t = useTranslations("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]);
|
||||
|
||||
// 将 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)
|
||||
},
|
||||
t
|
||||
);
|
||||
|
||||
if (result.success && result.data) {
|
||||
setSearchResult(result.data);
|
||||
} else {
|
||||
setSearchResult(null);
|
||||
}
|
||||
|
||||
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">{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}
|
||||
searchQuery={searchQuery}
|
||||
queryLang={queryLang}
|
||||
definitionLang={definitionLang}
|
||||
folders={folders}
|
||||
selectedFolderId={selectedFolderId}
|
||||
onFolderSelect={setSelectedFolderId}
|
||||
onResultUpdate={setSearchResult}
|
||||
onSearchingChange={setIsSearching}
|
||||
getNativeName={getNativeName}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!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">{t("welcomeTitle")}</p>
|
||||
<p className="text-gray-600">{t("welcomeHint")}</p>
|
||||
</div>
|
||||
)}
|
||||
</Container>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,26 +2,20 @@ 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 {
|
||||
DictWordResponse,
|
||||
DictPhraseResponse,
|
||||
isDictWordResponse,
|
||||
DictWordEntry,
|
||||
} from "./types";
|
||||
import { DictionaryEntry } from "./DictionaryEntry";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { performDictionaryLookup } from "./utils";
|
||||
import { TSharedItem } from "@/shared";
|
||||
|
||||
interface SearchResultProps {
|
||||
searchResult: DictWordResponse | DictPhraseResponse;
|
||||
searchResult: TSharedItem;
|
||||
searchQuery: string;
|
||||
queryLang: string;
|
||||
definitionLang: string;
|
||||
folders: Folder[];
|
||||
selectedFolderId: number | null;
|
||||
onFolderSelect: (folderId: number | null) => void;
|
||||
onResultUpdate: (newResult: DictWordResponse | DictPhraseResponse) => void;
|
||||
onResultUpdate: (newResult: TSharedItem) => void;
|
||||
onSearchingChange: (isSearching: boolean) => void;
|
||||
getNativeName: (code: string) => string;
|
||||
}
|
||||
@@ -54,8 +48,8 @@ export function SearchResult({
|
||||
t
|
||||
);
|
||||
|
||||
if (result.success && result.data) {
|
||||
onResultUpdate(result.data);
|
||||
if (result) {
|
||||
onResultUpdate(result);
|
||||
}
|
||||
|
||||
onSearchingChange(false);
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
// 类型定义
|
||||
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 +1,127 @@
|
||||
export { default } from "./DictionaryPage";
|
||||
"use client";
|
||||
|
||||
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";
|
||||
|
||||
export default function DictionaryPage() {
|
||||
const t = useTranslations("dictionary");
|
||||
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<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]);
|
||||
|
||||
// 将 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 (
|
||||
<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">{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}
|
||||
searchQuery={searchQuery}
|
||||
queryLang={queryLang}
|
||||
definitionLang={definitionLang}
|
||||
folders={folders}
|
||||
selectedFolderId={selectedFolderId}
|
||||
onFolderSelect={setSelectedFolderId}
|
||||
onResultUpdate={setSearchResult}
|
||||
onSearchingChange={setIsSearching}
|
||||
getNativeName={getNativeName}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!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">{t("welcomeTitle")}</p>
|
||||
<p className="text-gray-600">{t("welcomeHint")}</p>
|
||||
</div>
|
||||
)}
|
||||
</Container>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
// 从 shared 文件夹导出所有词典类型和类型守卫
|
||||
export * from "@/lib/shared";
|
||||
@@ -1,51 +1,25 @@
|
||||
import { toast } from "sonner";
|
||||
import { lookUp } from "@/lib/server/bigmodel/dictionaryActions";
|
||||
import {
|
||||
DictWordResponse,
|
||||
DictPhraseResponse,
|
||||
} from "./types";
|
||||
import { lookUpDictionaryAction } from "@/modules/dictionary/dictionary-action";
|
||||
import { DictionaryActionInputDto, DictionaryActionOutputDto } from "@/modules/dictionary";
|
||||
import { TSharedItem } from "@/shared";
|
||||
|
||||
interface LookupOptions {
|
||||
text: string;
|
||||
queryLang: string;
|
||||
definitionLang: string;
|
||||
forceRelook?: boolean;
|
||||
}
|
||||
|
||||
interface LookupResult {
|
||||
success: boolean;
|
||||
data?: DictWordResponse | DictPhraseResponse;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行词典查询的通用函数
|
||||
* @param options - 查询选项
|
||||
* @param t - 翻译函数
|
||||
* @returns 查询结果
|
||||
*/
|
||||
export async function performDictionaryLookup(
|
||||
options: LookupOptions,
|
||||
options: DictionaryActionInputDto,
|
||||
t?: (key: string) => string
|
||||
): Promise<LookupResult> {
|
||||
const { text, queryLang, definitionLang, forceRelook = false } = options;
|
||||
): Promise<TSharedItem | null> {
|
||||
const { text, queryLang, definitionLang, forceRelook = false, userId } = options;
|
||||
const result = await lookUpDictionaryAction({
|
||||
text,
|
||||
queryLang,
|
||||
definitionLang,
|
||||
forceRelook,
|
||||
userId
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await lookUp({
|
||||
text,
|
||||
queryLang,
|
||||
definitionLang,
|
||||
forceRelook
|
||||
});
|
||||
if (!result.success || !result.data) return null;
|
||||
|
||||
// 成功时显示提示(仅强制重新查询时)
|
||||
if (forceRelook && t) {
|
||||
toast.success(t("relookupSuccess"));
|
||||
}
|
||||
|
||||
return { success: true, data: result };
|
||||
} catch (error) {
|
||||
toast.error(String(error));
|
||||
return { success: false, error: String(error) };
|
||||
if (forceRelook && t) {
|
||||
toast.success(t("relookupSuccess"));
|
||||
}
|
||||
return result.data;
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { useAudioPlayer } from "@/hooks/useAudioPlayer";
|
||||
import { getTTSUrl, TTS_SUPPORTED_LANGUAGES } from "@/lib/server/bigmodel/tts";
|
||||
import { getTTSUrl, TTS_SUPPORTED_LANGUAGES } from "@/lib/bigmodel/tts";
|
||||
import { useTranslations } from "next-intl";
|
||||
import localFont from "next/font/local";
|
||||
import { isNonNegativeInteger, SeededRandom } from "@/lib/utils";
|
||||
import { isNonNegativeInteger, SeededRandom } from "@/utils/random";
|
||||
import { Pair } from "../../../../generated/prisma/browser";
|
||||
|
||||
const myFont = localFont({
|
||||
|
||||
@@ -3,7 +3,7 @@ import { getTranslations } from "next-intl/server";
|
||||
import {
|
||||
getFoldersWithTotalPairsByUserId,
|
||||
} from "@/lib/server/services/folderService";
|
||||
import { isNonNegativeInteger } from "@/lib/utils";
|
||||
import { isNonNegativeInteger } from "@/utils/random";
|
||||
import FolderSelector from "./FolderSelector";
|
||||
import Memorize from "./Memorize";
|
||||
import { getPairsByFolderId } from "@/lib/server/services/pairService";
|
||||
|
||||
@@ -14,10 +14,10 @@ import SaveList from "./SaveList";
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
import { getLocalStorageOperator } from "@/lib/browser/localStorageOperators";
|
||||
import { genIPA, genLanguage } from "@/lib/server/bigmodel/translatorActions";
|
||||
import { genIPA, genLanguage } from "@/modules/translator/translator-action";
|
||||
import { logger } from "@/lib/logger";
|
||||
import PageLayout from "@/components/ui/PageLayout";
|
||||
import { getTTSUrl, TTS_SUPPORTED_LANGUAGES } from "@/lib/server/bigmodel/tts";
|
||||
import { getTTSUrl, TTS_SUPPORTED_LANGUAGES } from "@/lib/bigmodel/tts";
|
||||
|
||||
export default function TextSpeakerPage() {
|
||||
const t = useTranslations("text_speaker");
|
||||
|
||||
@@ -12,14 +12,14 @@ import { useTranslations } from "next-intl";
|
||||
import { useRef, useState } from "react";
|
||||
import z from "zod";
|
||||
import AddToFolder from "./AddToFolder";
|
||||
import { translateText } from "@/lib/server/bigmodel/translatorActions";
|
||||
import { translateText } from "@/modules/translator/translator-action";
|
||||
import type { TranslateTextOutput } from "@/lib/server/services/types";
|
||||
import { toast } from "sonner";
|
||||
import FolderSelector from "./FolderSelector";
|
||||
import { createPair } from "@/lib/server/services/pairService";
|
||||
import { shallowEqual } from "@/lib/utils";
|
||||
import { shallowEqual } from "@/utils/random";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { getTTSUrl, TTS_SUPPORTED_LANGUAGES } from "@/lib/server/bigmodel/tts";
|
||||
import { getTTSUrl, TTS_SUPPORTED_LANGUAGES } from "@/lib/bigmodel/tts";
|
||||
|
||||
export default function TranslatorPage() {
|
||||
const t = useTranslations("translator");
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useState, useActionState, startTransition } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { signInAction, signUpAction, SignUpState } from "@/lib/actions/auth";
|
||||
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";
|
||||
|
||||
Reference in New Issue
Block a user