diff --git a/messages/en-US.json b/messages/en-US.json index 0f12714..7517e70 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -241,6 +241,7 @@ "definitionLanguage": "Definition Language", "definitionLanguageHint": "What language do you want the definitions in", "otherLanguagePlaceholder": "Or enter another language...", + "other": "Other", "currentSettings": "Current settings: Query {queryLang}, Definition {definitionLang}", "relookup": "Re-search", "saveToFolder": "Save to folder", @@ -267,7 +268,9 @@ "unknownUser": "Unknown User", "favorite": "Favorite", "unfavorite": "Unfavorite", - "pleaseLogin": "Please login first" + "pleaseLogin": "Please login first", + "sortByFavorites": "Sort by favorites", + "sortByFavoritesActive": "Undo sort by favorites" }, "favorites": { "title": "My Favorites", diff --git a/messages/zh-CN.json b/messages/zh-CN.json index 9e9a752..c7830e6 100644 --- a/messages/zh-CN.json +++ b/messages/zh-CN.json @@ -241,6 +241,7 @@ "definitionLanguage": "释义语言", "definitionLanguageHint": "你希望用什么语言查看释义", "otherLanguagePlaceholder": "或输入其他语言...", + "other": "其他", "currentSettings": "当前设置:查询 {queryLang},释义 {definitionLang}", "relookup": "重新查询", "saveToFolder": "保存到文件夹", @@ -267,7 +268,9 @@ "unknownUser": "未知用户", "favorite": "收藏", "unfavorite": "取消收藏", - "pleaseLogin": "请先登录" + "pleaseLogin": "请先登录", + "sortByFavorites": "按收藏数排序", + "sortByFavoritesActive": "取消按收藏数排序" }, "favorites": { "title": "收藏", diff --git a/src/app/(auth)/logout/page.tsx b/src/app/(auth)/logout/page.tsx index e748151..03734da 100644 --- a/src/app/(auth)/logout/page.tsx +++ b/src/app/(auth)/logout/page.tsx @@ -8,7 +8,7 @@ export default async function LogoutPage( } ) { const searchParams = await props.searchParams; - const redirectTo = props.searchParams ?? null; + const redirectTo = searchParams.redirect ?? null; const session = await auth.api.getSession({ headers: await headers() diff --git a/src/app/(features)/dictionary/DictionaryClient.tsx b/src/app/(features)/dictionary/DictionaryClient.tsx new file mode 100644 index 0000000..3e62918 --- /dev/null +++ b/src/app/(features)/dictionary/DictionaryClient.tsx @@ -0,0 +1,239 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useTranslations } from "next-intl"; +import { useRouter, useSearchParams } from "next/navigation"; +import { useDictionaryStore } from "./stores/dictionaryStore"; +import { PageLayout } from "@/components/ui/PageLayout"; +import { LightButton } from "@/design-system/base/button"; +import { Input } from "@/design-system/base/input"; +import { Plus, RefreshCw } from "lucide-react"; +import { DictionaryEntry } from "./DictionaryEntry"; +import { LanguageSelector } from "./LanguageSelector"; +import { authClient } from "@/lib/auth-client"; +import { actionGetFoldersByUserId, actionCreatePair } from "@/modules/folder/folder-aciton"; +import { TSharedFolder } from "@/shared/folder-type"; +import { toast } from "sonner"; + +interface DictionaryClientProps { + initialFolders: TSharedFolder[]; +} + +export function DictionaryClient({ initialFolders }: DictionaryClientProps) { + const t = useTranslations("dictionary"); + const router = useRouter(); + const searchParams = useSearchParams(); + + const { + query, + queryLang, + definitionLang, + searchResult, + isSearching, + setQuery, + setQueryLang, + setDefinitionLang, + search, + relookup, + syncFromUrl, + } = useDictionaryStore(); + + const { data: session } = authClient.useSession(); + const [folders, setFolders] = useState(initialFolders); + + useEffect(() => { + const q = searchParams.get("q") || undefined; + const ql = searchParams.get("ql") || undefined; + const dl = searchParams.get("dl") || undefined; + + syncFromUrl({ q, ql, dl }); + + if (q) { + search(); + } + }, [searchParams, syncFromUrl, search]); + + useEffect(() => { + if (session?.user?.id) { + actionGetFoldersByUserId(session.user.id).then((result) => { + if (result.success && result.data) { + setFolders(result.data); + } + }); + } + }, [session?.user?.id]); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + + if (!query.trim()) return; + + const params = new URLSearchParams({ + q: query, + ql: queryLang, + dl: definitionLang, + }); + + router.push(`/dictionary?${params.toString()}`); + }; + + 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; + + if (!searchResult) return; + + 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 ( + +
+

+ {t("title")} +

+

+ {t("description")} +

+
+ +
+ setQuery(e.target.value)} + placeholder={t("searchPlaceholder")} + variant="search" + required + /> + + {t("search")} + +
+ +
+
+ {t("languageSettings")} +
+ +
+ + + +
+
+ +
+ {isSearching ? ( +
+
+

{t("searching")}

+
+ ) : query && !searchResult ? ( +
+

No results found

+

Try other words

+
+ ) : searchResult ? ( +
+
+
+

+ {searchResult.standardForm} +

+
+
+ {session && folders.length > 0 && ( + + )} + + + +
+
+ +
+ {searchResult.entries.map((entry, index) => ( +
+ +
+ ))} +
+ +
+ + + Re-lookup + +
+
+ ) : ( +
+
📚
+

{t("welcomeTitle")}

+

{t("welcomeHint")}

+
+ )} +
+
+ ); +} diff --git a/src/app/(features)/dictionary/LanguageSelector.tsx b/src/app/(features)/dictionary/LanguageSelector.tsx new file mode 100644 index 0000000..f89dcb1 --- /dev/null +++ b/src/app/(features)/dictionary/LanguageSelector.tsx @@ -0,0 +1,80 @@ +"use client"; + +import { useState } from "react"; +import { LightButton } from "@/design-system/base/button"; +import { Input } from "@/design-system/base/input"; +import { POPULAR_LANGUAGES } from "./constants"; +import { useTranslations } from "next-intl"; + +interface LanguageSelectorProps { + label: string; + hint: string; + value: string; + onChange: (value: string) => void; +} + +export function LanguageSelector({ label, hint, value, onChange }: LanguageSelectorProps) { + const t = useTranslations("dictionary"); + const [showCustomInput, setShowCustomInput] = useState(false); + const [customLang, setCustomLang] = useState(""); + + const isPresetLanguage = POPULAR_LANGUAGES.some((lang) => lang.code === value); + + const handlePresetSelect = (code: string) => { + onChange(code); + setShowCustomInput(false); + setCustomLang(""); + }; + + const handleCustomToggle = () => { + setShowCustomInput(!showCustomInput); + if (!showCustomInput && customLang.trim()) { + onChange(customLang.trim()); + } + }; + + const handleCustomChange = (newValue: string) => { + setCustomLang(newValue); + if (newValue.trim()) { + onChange(newValue.trim()); + } + }; + + return ( +
+ +
+ {POPULAR_LANGUAGES.map((lang) => ( + handlePresetSelect(lang.code)} + className="text-sm px-3 py-1" + > + {lang.nativeName} + + ))} + + {t("other")} + +
+ {(showCustomInput || (!isPresetLanguage && value)) && ( + handleCustomChange(e.target.value)} + placeholder={t("otherLanguagePlaceholder")} + className="text-sm" + /> + )} +
+ ); +} diff --git a/src/app/(features)/dictionary/SearchForm.tsx b/src/app/(features)/dictionary/SearchForm.tsx deleted file mode 100644 index 687b7a9..0000000 --- a/src/app/(features)/dictionary/SearchForm.tsx +++ /dev/null @@ -1,117 +0,0 @@ -"use client"; - -import { LightButton } from "@/design-system/base/button"; -import { Input } from "@/design-system/base/input"; -import { useTranslations } from "next-intl"; -import { useState } from "react"; -import { useRouter } from "next/navigation"; -import { POPULAR_LANGUAGES } from "./constants"; - -interface SearchFormProps { - defaultQueryLang?: string; - defaultDefinitionLang?: string; -} - -export function SearchForm({ defaultQueryLang = "english", defaultDefinitionLang = "chinese" }: SearchFormProps) { - const t = useTranslations("dictionary"); - const [queryLang, setQueryLang] = useState(defaultQueryLang); - const [definitionLang, setDefinitionLang] = useState(defaultDefinitionLang); - const router = useRouter(); - - const handleSubmit = (e: React.FormEvent) => { - 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 ( - <> - {/* 页面标题 */} -
-

- {t("title")} -

-

- {t("description")} -

-
- - {/* 搜索表单 */} -
- - - {t("search")} - -
- - {/* 语言设置 */} -
-
- {t("languageSettings")} -
- -
- {/* 查询语言 */} -
- -
- {POPULAR_LANGUAGES.map((lang) => ( - setQueryLang(lang.code)} - className="text-sm px-3 py-1" - > - {lang.nativeName} - - ))} -
-
- - {/* 释义语言 */} -
- -
- {POPULAR_LANGUAGES.map((lang) => ( - setDefinitionLang(lang.code)} - className="text-sm px-3 py-1" - > - {lang.nativeName} - - ))} -
-
-
-
- - ); -} diff --git a/src/app/(features)/dictionary/SearchResult.client.tsx b/src/app/(features)/dictionary/SearchResult.client.tsx deleted file mode 100644 index 97609f2..0000000 --- a/src/app/(features)/dictionary/SearchResult.client.tsx +++ /dev/null @@ -1,122 +0,0 @@ -"use client"; - -import { Plus, RefreshCw } from "lucide-react"; -import { CircleButton, LightButton } from "@/design-system/base/button"; -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 ( - - - - ); -} - -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 = { - 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 ( - } - > - Re-lookup - - ); -} diff --git a/src/app/(features)/dictionary/SearchResult.tsx b/src/app/(features)/dictionary/SearchResult.tsx deleted file mode 100644 index 2c56efb..0000000 --- a/src/app/(features)/dictionary/SearchResult.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { auth } from "@/auth"; -import { DictionaryEntry } from "./DictionaryEntry"; -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"; - -interface SearchResultProps { - searchResult: TSharedItem | null; - searchQuery: string; - queryLang: string; - definitionLang: string; -} - -export async function SearchResult({ - searchResult, - searchQuery, - queryLang, - definitionLang -}: SearchResultProps) { - // 获取用户会话和文件夹 - const session = await auth.api.getSession({ headers: await headers() }); - let folders: TSharedFolder[] = []; - - if (session?.user?.id) { - const result = await actionGetFoldersByUserId(session.user.id as string); - if (result.success && result.data) { - folders = result.data; - } - } - - return ( -
- {!searchResult ? ( -
-

No results found

-

Try other words

-
- ) : ( -
- {/* 标题和保存按钮 */} -
-
-

- {searchResult.standardForm} -

-
-
- {session && folders.length > 0 && ( - - )} - -
-
- - {/* 条目列表 */} -
- {searchResult.entries.map((entry, index) => ( -
- -
- ))} -
- - {/* 重新查询按钮 */} -
- -
-
- )} -
- ); -} diff --git a/src/app/(features)/dictionary/page.tsx b/src/app/(features)/dictionary/page.tsx index 1b1df0e..f3f8559 100644 --- a/src/app/(features)/dictionary/page.tsx +++ b/src/app/(features)/dictionary/page.tsx @@ -1,75 +1,20 @@ -import { PageLayout } from "@/components/ui/PageLayout"; -import { SearchForm } from "./SearchForm"; -import { SearchResult } from "./SearchResult"; -import { getTranslations } from "next-intl/server"; -import { actionLookUpDictionary } from "@/modules/dictionary/dictionary-action"; -import { TSharedItem } from "@/shared/dictionary-type"; +import { DictionaryClient } from "./DictionaryClient"; +import { auth } from "@/auth"; +import { headers } from "next/headers"; +import { actionGetFoldersByUserId } from "@/modules/folder/folder-aciton"; +import { TSharedFolder } from "@/shared/folder-type"; -interface DictionaryPageProps { - searchParams: Promise<{ q?: string; ql?: string; dl?: string; }>; -} - -export default async function DictionaryPage({ searchParams }: DictionaryPageProps) { - const t = await getTranslations("dictionary"); - - // 从 searchParams 获取搜索参数 - const { q: searchQuery, ql: queryLang = "english", dl: definitionLang = "chinese" } = await searchParams; - - // 如果有搜索查询,获取搜索结果 - let searchResult: TSharedItem | undefined | null = null; - if (searchQuery) { - const getNativeName = (code: string): string => { - const popularLanguages: Record = { - english: "English", - chinese: "中文", - japanese: "日本語", - 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; - } +export default async function DictionaryPage() { + const session = await auth.api.getSession({ headers: await headers() }); + + let folders: TSharedFolder[] = []; + + if (session?.user?.id) { + const result = await actionGetFoldersByUserId(session.user.id as string); + if (result.success && result.data) { + folders = result.data; } + } - return ( - - {/* 搜索区域 */} -
- -
- - {/* 搜索结果区域 */} -
- {searchQuery && ( - - )} - {!searchQuery && ( -
-
📚
-

{t("welcomeTitle")}

-

{t("welcomeHint")}

-
- )} -
-
- ); + return ; } diff --git a/src/app/(features)/dictionary/stores/dictionaryStore.ts b/src/app/(features)/dictionary/stores/dictionaryStore.ts new file mode 100644 index 0000000..42590b2 --- /dev/null +++ b/src/app/(features)/dictionary/stores/dictionaryStore.ts @@ -0,0 +1,148 @@ +"use client"; + +import { create } from 'zustand'; +import { devtools } from 'zustand/middleware'; +import { TSharedItem } from "@/shared/dictionary-type"; +import { actionLookUpDictionary } from "@/modules/dictionary/dictionary-action"; +import { toast } from "sonner"; + +const POPULAR_LANGUAGES_MAP: Record = { + english: "English", + chinese: "中文", + japanese: "日本語", + korean: "한국어", + italian: "Italiano", + uyghur: "ئۇيغۇرچە", +}; + +export function getNativeName(code: string): string { + return POPULAR_LANGUAGES_MAP[code] || code; +} + +export interface DictionaryState { + query: string; + queryLang: string; + definitionLang: string; + searchResult: TSharedItem | null; + isSearching: boolean; +} + +export interface DictionaryActions { + setQuery: (query: string) => void; + setQueryLang: (lang: string) => void; + setDefinitionLang: (lang: string) => void; + setSearchResult: (result: TSharedItem | null) => void; + search: () => Promise; + relookup: () => Promise; + syncFromUrl: (params: { q?: string; ql?: string; dl?: string }) => void; +} + +export type DictionaryStore = DictionaryState & DictionaryActions; + +const initialState: DictionaryState = { + query: "", + queryLang: "english", + definitionLang: "chinese", + searchResult: null, + isSearching: false, +}; + +export const useDictionaryStore = create()( + devtools( + (set, get) => ({ + ...initialState, + + setQuery: (query) => set({ query }), + + setQueryLang: (queryLang) => set({ queryLang }), + + setDefinitionLang: (definitionLang) => set({ definitionLang }), + + setSearchResult: (searchResult) => set({ searchResult }), + + search: async () => { + const { query, queryLang, definitionLang } = get(); + + if (!query.trim()) { + return; + } + + set({ isSearching: true }); + + try { + const result = await actionLookUpDictionary({ + text: query, + queryLang: getNativeName(queryLang), + definitionLang: getNativeName(definitionLang), + forceRelook: false, + }); + + if (result.success && result.data) { + set({ searchResult: result.data }); + } else { + set({ searchResult: null }); + if (result.message) { + toast.error(result.message); + } + } + } catch (error) { + set({ searchResult: null }); + toast.error("Search failed"); + } finally { + set({ isSearching: false }); + } + }, + + relookup: async () => { + const { query, queryLang, definitionLang } = get(); + + if (!query.trim()) { + return; + } + + set({ isSearching: true }); + + try { + const result = await actionLookUpDictionary({ + text: query, + queryLang: getNativeName(queryLang), + definitionLang: getNativeName(definitionLang), + forceRelook: true, + }); + + if (result.success && result.data) { + set({ searchResult: result.data }); + toast.success("Re-lookup successful"); + } else { + if (result.message) { + toast.error(result.message); + } + } + } catch (error) { + toast.error("Re-lookup failed"); + } finally { + set({ isSearching: false }); + } + }, + + syncFromUrl: (params) => { + const updates: Partial = {}; + + if (params.q !== undefined) { + updates.query = params.q; + } + if (params.ql !== undefined) { + updates.queryLang = params.ql; + } + if (params.dl !== undefined) { + updates.definitionLang = params.dl; + } + + if (Object.keys(updates).length > 0) { + set(updates); + } + }, + }), + { name: 'dictionary-store' } + ) +); diff --git a/src/app/(features)/explore/ExploreClient.tsx b/src/app/(features)/explore/ExploreClient.tsx index 50711ea..0b82dbb 100644 --- a/src/app/(features)/explore/ExploreClient.tsx +++ b/src/app/(features)/explore/ExploreClient.tsx @@ -4,6 +4,7 @@ import { Folder as Fd, Heart, Search, + ArrowUpDown, } from "lucide-react"; import { CircleButton } from "@/design-system/base/button"; import { useEffect, useState } from "react"; @@ -61,40 +62,38 @@ const PublicFolderCard = ({ folder, currentUserId, onUpdateFavorite }: PublicFol return (
{ router.push(`/explore/${folder.id}`); }} > -
-
- +
+
+ +
-

{folder.name}

+

{folder.name}

-

+

{t("folderInfo", { userName: folder.userName ?? folder.userUsername ?? t("unknownUser"), totalPairs: folder.totalPairs, })}

-
- +
+ {favoriteCount}
@@ -111,6 +110,7 @@ export function ExploreClient({ initialPublicFolders }: ExploreClientProps) { const [publicFolders, setPublicFolders] = useState(initialPublicFolders); const [loading, setLoading] = useState(false); const [searchQuery, setSearchQuery] = useState(""); + const [sortByFavorites, setSortByFavorites] = useState(false); const { data: session } = authClient.useSession(); const currentUserId = session?.user?.id; @@ -128,6 +128,14 @@ export function ExploreClient({ initialPublicFolders }: ExploreClientProps) { setLoading(false); }; + const handleToggleSort = () => { + setSortByFavorites((prev) => !prev); + }; + + const sortedFolders = sortByFavorites + ? [...publicFolders].sort((a, b) => b.favoriteCount - a.favoriteCount) + : publicFolders; + const handleUpdateFavorite = (folderId: number, _isFavorited: boolean, favoriteCount: number) => { setPublicFolders((prev) => prev.map((f) => @@ -152,6 +160,13 @@ export function ExploreClient({ initialPublicFolders }: ExploreClientProps) { className="w-full pl-10 pr-4 py-2 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500" />
+ + + @@ -162,7 +177,7 @@ export function ExploreClient({ initialPublicFolders }: ExploreClientProps) {

{t("loading")}

- ) : publicFolders.length === 0 ? ( + ) : sortedFolders.length === 0 ? (
@@ -171,7 +186,7 @@ export function ExploreClient({ initialPublicFolders }: ExploreClientProps) {
) : (
- {publicFolders.map((folder) => ( + {sortedFolders.map((folder) => ( {currentSubtitle && currentSubtitle.text.split(" ").map((s, i) => ( - {s} diff --git a/src/app/folders/FoldersClient.tsx b/src/app/folders/FoldersClient.tsx index f33fd42..5462108 100644 --- a/src/app/folders/FoldersClient.tsx +++ b/src/app/folders/FoldersClient.tsx @@ -143,10 +143,6 @@ export function FoldersClient({ userId }: FoldersClientProps) { const [folders, setFolders] = useState([]); const [loading, setLoading] = useState(true); - useEffect(() => { - loadFolders(); - }, [userId]); - const loadFolders = async () => { setLoading(true); const result = await actionGetFoldersWithTotalPairsByUserId(userId); @@ -156,6 +152,10 @@ export function FoldersClient({ userId }: FoldersClientProps) { setLoading(false); }; + useEffect(() => { + loadFolders(); + }, [userId]); + const handleUpdateFolder = (folderId: number, updates: Partial) => { setFolders((prev) => prev.map((f) => (f.id === folderId ? { ...f, ...updates } : f)) diff --git a/src/lib/bigmodel/dictionary/orchestrator.ts b/src/lib/bigmodel/dictionary/orchestrator.ts index 35acdcb..782d154 100644 --- a/src/lib/bigmodel/dictionary/orchestrator.ts +++ b/src/lib/bigmodel/dictionary/orchestrator.ts @@ -19,12 +19,12 @@ export async function executeDictionaryLookup( if (!analysis.isValid) { log.debug("[Stage 1] Invalid input", { reason: analysis.reason }); - throw analysis.reason || "无效输入"; + throw new LookUpError(analysis.reason || "无效输入"); } if (analysis.isEmpty) { log.debug("[Stage 1] Empty input"); - throw "输入为空"; + throw new LookUpError("输入为空"); } log.debug("[Stage 1] Analysis complete", { analysis }); @@ -33,7 +33,7 @@ export async function executeDictionaryLookup( const semanticMapping = await determineSemanticMapping( text, queryLang, - analysis.inputLanguage || text + analysis.inputLanguage ?? text ); log.debug("[Stage 2] Semantic mapping complete", { semanticMapping }); diff --git a/src/lib/bigmodel/dictionary/stage2-semanticMapping.ts b/src/lib/bigmodel/dictionary/stage2-semanticMapping.ts index 5547eed..7052b5b 100644 --- a/src/lib/bigmodel/dictionary/stage2-semanticMapping.ts +++ b/src/lib/bigmodel/dictionary/stage2-semanticMapping.ts @@ -74,7 +74,7 @@ b) 输入是明确、基础、可词典化的语义概念 role: "user", content: prompt, }, - ]).then(parseAIGeneratedJSON); + ]).then(parseAIGeneratedJSON); // 代码层面的数据验证 if (typeof result.shouldMap !== "boolean") { diff --git a/src/lib/bigmodel/dictionary/stage3-standardForm.ts b/src/lib/bigmodel/dictionary/stage3-standardForm.ts index 75f0be1..5409eda 100644 --- a/src/lib/bigmodel/dictionary/stage3-standardForm.ts +++ b/src/lib/bigmodel/dictionary/stage3-standardForm.ts @@ -66,7 +66,7 @@ ${originalInput ? ` role: "user", content: prompt, }, - ]).then(parseAIGeneratedJSON); + ]).then(parseAIGeneratedJSON); // 代码层面的数据验证 if (!result.standardForm || result.standardForm.trim().length === 0) { diff --git a/src/lib/bigmodel/dictionary/stage4-entriesGeneration.ts b/src/lib/bigmodel/dictionary/stage4-entriesGeneration.ts index 7445adc..6a745ea 100644 --- a/src/lib/bigmodel/dictionary/stage4-entriesGeneration.ts +++ b/src/lib/bigmodel/dictionary/stage4-entriesGeneration.ts @@ -98,10 +98,6 @@ ${isWord ? ` if (isWord && !entry.partOfSpeech) { throw new Error("阶段4:单词条目缺少 partOfSpeech"); } - - if (isWord && !entry.ipa) { - throw new Error("阶段4:单词条目缺少 ipa"); - } } return result; diff --git a/src/modules/folder/folder-service-dto.ts b/src/modules/folder/folder-service-dto.ts index e69de29..af91b9e 100644 --- a/src/modules/folder/folder-service-dto.ts +++ b/src/modules/folder/folder-service-dto.ts @@ -0,0 +1,108 @@ +import { Visibility } from "../../../generated/prisma/enums"; + +export type ServiceInputCreateFolder = { + name: string; + userId: string; +}; + +export type ServiceInputRenameFolder = { + folderId: number; + newName: string; +}; + +export type ServiceInputDeleteFolder = { + folderId: number; +}; + +export type ServiceInputSetVisibility = { + folderId: number; + visibility: Visibility; +}; + +export type ServiceInputCheckOwnership = { + folderId: number; + userId: string; +}; + +export type ServiceInputCheckPairOwnership = { + pairId: number; + userId: string; +}; + +export type ServiceInputCreatePair = { + folderId: number; + text1: string; + text2: string; + language1: string; + language2: string; +}; + +export type ServiceInputUpdatePair = { + pairId: number; + text1?: string; + text2?: string; + language1?: string; + language2?: string; +}; + +export type ServiceInputDeletePair = { + pairId: number; +}; + +export type ServiceInputGetPublicFolders = { + limit?: number; + offset?: number; +}; + +export type ServiceInputSearchPublicFolders = { + query: string; + limit?: number; +}; + +export type ServiceInputToggleFavorite = { + folderId: number; + userId: string; +}; + +export type ServiceInputCheckFavorite = { + folderId: number; + userId: string; +}; + +export type ServiceInputGetUserFavorites = { + userId: string; + limit?: number; + offset?: number; +}; + +export type ServiceOutputFolder = { + id: number; + name: string; + visibility: Visibility; + createdAt: Date; + userId: string; +}; + +export type ServiceOutputFolderWithDetails = ServiceOutputFolder & { + userName: string | null; + userUsername: string | null; + totalPairs: number; + favoriteCount: number; +}; + +export type ServiceOutputFavoriteStatus = { + isFavorited: boolean; + favoriteCount: number; +}; + +export type ServiceOutputUserFavorite = { + id: number; + folderId: number; + folderName: string; + folderCreatedAt: Date; + folderTotalPairs: number; + folderOwnerId: string; + folderOwnerName: string | null; + folderOwnerUsername: string | null; + favoritedAt: Date; +};