diff --git a/prisma/migrations/20260113125222_optimize_dicttionary/migration.sql b/prisma/migrations/20260113125222_optimize_dicttionary/migration.sql
new file mode 100644
index 0000000..a24a98c
--- /dev/null
+++ b/prisma/migrations/20260113125222_optimize_dicttionary/migration.sql
@@ -0,0 +1,94 @@
+/*
+ Warnings:
+
+ - You are about to drop the column `dictionary_phrase_id` on the `dictionary_lookups` table. All the data in the column will be lost.
+ - You are about to drop the column `dictionary_word_id` on the `dictionary_lookups` table. All the data in the column will be lost.
+ - You are about to drop the `dictionary_phrase_entries` table. If the table is not empty, all the data it contains will be lost.
+ - You are about to drop the `dictionary_phrases` table. If the table is not empty, all the data it contains will be lost.
+ - You are about to drop the `dictionary_word_entries` table. If the table is not empty, all the data it contains will be lost.
+ - You are about to drop the `dictionary_words` table. If the table is not empty, all the data it contains will be lost.
+
+*/
+-- DropForeignKey
+ALTER TABLE "dictionary_lookups" DROP CONSTRAINT "dictionary_lookups_dictionary_phrase_id_fkey";
+
+-- DropForeignKey
+ALTER TABLE "dictionary_lookups" DROP CONSTRAINT "dictionary_lookups_dictionary_word_id_fkey";
+
+-- DropForeignKey
+ALTER TABLE "dictionary_phrase_entries" DROP CONSTRAINT "dictionary_phrase_entries_phrase_id_fkey";
+
+-- DropForeignKey
+ALTER TABLE "dictionary_word_entries" DROP CONSTRAINT "dictionary_word_entries_word_id_fkey";
+
+-- DropIndex
+DROP INDEX "dictionary_lookups_text_query_lang_definition_lang_idx";
+
+-- AlterTable
+ALTER TABLE "dictionary_lookups" DROP COLUMN "dictionary_phrase_id",
+DROP COLUMN "dictionary_word_id",
+ADD COLUMN "dictionary_item_id" INTEGER,
+ADD COLUMN "normalized_text" TEXT NOT NULL DEFAULT '';
+
+-- DropTable
+DROP TABLE "dictionary_phrase_entries";
+
+-- DropTable
+DROP TABLE "dictionary_phrases";
+
+-- DropTable
+DROP TABLE "dictionary_word_entries";
+
+-- DropTable
+DROP TABLE "dictionary_words";
+
+-- CreateTable
+CREATE TABLE "dictionary_items" (
+ "id" SERIAL NOT NULL,
+ "frequency" INTEGER NOT NULL DEFAULT 1,
+ "standard_form" TEXT NOT NULL,
+ "query_lang" TEXT NOT NULL,
+ "definition_lang" TEXT NOT NULL,
+ "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updated_at" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "dictionary_items_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "dictionary_entries" (
+ "id" SERIAL NOT NULL,
+ "item_id" INTEGER NOT NULL,
+ "ipa" TEXT,
+ "definition" TEXT NOT NULL,
+ "part_of_speech" TEXT,
+ "example" TEXT NOT NULL,
+ "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updated_at" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "dictionary_entries_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE INDEX "dictionary_items_standard_form_idx" ON "dictionary_items"("standard_form");
+
+-- CreateIndex
+CREATE INDEX "dictionary_items_query_lang_definition_lang_idx" ON "dictionary_items"("query_lang", "definition_lang");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "dictionary_items_standard_form_query_lang_definition_lang_key" ON "dictionary_items"("standard_form", "query_lang", "definition_lang");
+
+-- CreateIndex
+CREATE INDEX "dictionary_entries_item_id_idx" ON "dictionary_entries"("item_id");
+
+-- CreateIndex
+CREATE INDEX "dictionary_entries_created_at_idx" ON "dictionary_entries"("created_at");
+
+-- CreateIndex
+CREATE INDEX "dictionary_lookups_normalized_text_idx" ON "dictionary_lookups"("normalized_text");
+
+-- AddForeignKey
+ALTER TABLE "dictionary_lookups" ADD CONSTRAINT "dictionary_lookups_dictionary_item_id_fkey" FOREIGN KEY ("dictionary_item_id") REFERENCES "dictionary_items"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "dictionary_entries" ADD CONSTRAINT "dictionary_entries_item_id_fkey" FOREIGN KEY ("item_id") REFERENCES "dictionary_items"("id") ON DELETE CASCADE ON UPDATE CASCADE;
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index c402a6e..0eb2714 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -107,27 +107,27 @@ model Folder {
}
model DictionaryLookUp {
- id Int @id @default(autoincrement())
- userId String? @map("user_id")
- text String
- queryLang String @map("query_lang")
- definitionLang String @map("definition_lang")
- createdAt DateTime @default(now()) @map("created_at")
- dictionaryWordId Int? @map("dictionary_word_id")
- dictionaryPhraseId Int? @map("dictionary_phrase_id")
+ id Int @id @default(autoincrement())
+ userId String? @map("user_id")
+ text String
+ normalizedText String @default("") @map("normalized_text")
+ queryLang String @map("query_lang")
+ definitionLang String @map("definition_lang")
+ createdAt DateTime @default(now()) @map("created_at")
+ dictionaryItemId Int? @map("dictionary_item_id")
- user User? @relation(fields: [userId], references: [id])
- dictionaryWord DictionaryWord? @relation(fields: [dictionaryWordId], references: [id], onDelete: SetNull)
- dictionaryPhrase DictionaryPhrase? @relation(fields: [dictionaryPhraseId], references: [id], onDelete: SetNull)
+ user User? @relation(fields: [userId], references: [id])
+ dictionaryItem DictionaryItem? @relation(fields: [dictionaryItemId], references: [id], onDelete: SetNull)
@@index([userId])
@@index([createdAt])
- @@index([text, queryLang, definitionLang])
+ @@index([normalizedText])
@@map("dictionary_lookups")
}
-model DictionaryWord {
+model DictionaryItem {
id Int @id @default(autoincrement())
+ frequency Int @default(1)
standardForm String @map("standard_form")
queryLang String @map("query_lang")
definitionLang String @map("definition_lang")
@@ -135,59 +135,29 @@ model DictionaryWord {
updatedAt DateTime @updatedAt @map("updated_at")
lookups DictionaryLookUp[]
- entries DictionaryWordEntry[]
+ entries DictionaryEntry[]
+ @@unique([standardForm, queryLang, definitionLang])
@@index([standardForm])
@@index([queryLang, definitionLang])
- @@map("dictionary_words")
+ @@map("dictionary_items")
}
-model DictionaryPhrase {
- id Int @id @default(autoincrement())
- standardForm String @map("standard_form")
- queryLang String @map("query_lang")
- definitionLang String @map("definition_lang")
- createdAt DateTime @default(now()) @map("created_at")
- updatedAt DateTime @updatedAt @map("updated_at")
-
- lookups DictionaryLookUp[]
- entries DictionaryPhraseEntry[]
-
- @@index([standardForm])
- @@index([queryLang, definitionLang])
- @@map("dictionary_phrases")
-}
-
-model DictionaryWordEntry {
+model DictionaryEntry {
id Int @id @default(autoincrement())
- wordId Int @map("word_id")
- ipa String
+ itemId Int @map("item_id")
+ ipa String?
definition String
- partOfSpeech String @map("part_of_speech")
+ partOfSpeech String? @map("part_of_speech")
example String
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
- word DictionaryWord @relation(fields: [wordId], references: [id], onDelete: Cascade)
+ item DictionaryItem @relation(fields: [itemId], references: [id], onDelete: Cascade)
- @@index([wordId])
+ @@index([itemId])
@@index([createdAt])
- @@map("dictionary_word_entries")
-}
-
-model DictionaryPhraseEntry {
- id Int @id @default(autoincrement())
- phraseId Int @map("phrase_id")
- definition String
- example String
- createdAt DateTime @default(now()) @map("created_at")
- updatedAt DateTime @updatedAt @map("updated_at")
-
- phrase DictionaryPhrase @relation(fields: [phraseId], references: [id], onDelete: Cascade)
-
- @@index([phraseId])
- @@index([createdAt])
- @@map("dictionary_phrase_entries")
+ @@map("dictionary_entries")
}
model TranslationHistory {
diff --git a/src/app/(features)/dictionary/DictionaryEntry.tsx b/src/app/(features)/dictionary/DictionaryEntry.tsx
index a76f3b5..5d09954 100644
--- a/src/app/(features)/dictionary/DictionaryEntry.tsx
+++ b/src/app/(features)/dictionary/DictionaryEntry.tsx
@@ -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 (
-
- {/* 音标和词性 */}
-
- {wordEntry.ipa && (
-
- [{wordEntry.ipa}]
-
- )}
- {wordEntry.partOfSpeech && (
-
- {wordEntry.partOfSpeech}
-
- )}
-
-
- {/* 释义 */}
-
-
- 释义
-
-
{wordEntry.definition}
-
-
- {/* 例句 */}
- {wordEntry.example && (
-
-
- 例句
-
-
- {wordEntry.example}
-
-
- )}
-
- );
- }
-
- // 短语条目
- const phraseEntry = entry as DictPhraseEntry;
return (
+ {/* 音标和词性 */}
+
+ {entry.ipa && (
+
+ [{entry.ipa}]
+
+ )}
+ {entry.partOfSpeech && (
+
+ {entry.partOfSpeech}
+
+ )}
+
+
{/* 释义 */}
释义
-
{phraseEntry.definition}
+
{entry.definition}
{/* 例句 */}
- {phraseEntry.example && (
+ {entry.example && (
例句
- {phraseEntry.example}
+ {entry.example}
)}
diff --git a/src/app/(features)/dictionary/DictionaryPage.tsx b/src/app/(features)/dictionary/DictionaryPage.tsx
deleted file mode 100644
index 3dfbe1e..0000000
--- a/src/app/(features)/dictionary/DictionaryPage.tsx
+++ /dev/null
@@ -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
(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(null);
- const [folders, setFolders] = useState([]);
- 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 (
-
- {/* 搜索区域 */}
-
-
-
-
-
-
- {/* 搜索结果区域 */}
-
-
- {isSearching && (
-
- )}
-
- {!isSearching && hasSearched && !searchResult && (
-
-
{t("noResults")}
-
{t("tryOtherWords")}
-
- )}
-
- {!isSearching && searchResult && (
-
- )}
-
- {!hasSearched && (
-
-
📚
-
{t("welcomeTitle")}
-
{t("welcomeHint")}
-
- )}
-
-
-
- );
-}
diff --git a/src/app/(features)/dictionary/SearchResult.tsx b/src/app/(features)/dictionary/SearchResult.tsx
index f445774..cf351a8 100644
--- a/src/app/(features)/dictionary/SearchResult.tsx
+++ b/src/app/(features)/dictionary/SearchResult.tsx
@@ -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);
diff --git a/src/app/(features)/dictionary/index.ts b/src/app/(features)/dictionary/index.ts
deleted file mode 100644
index 8cb51b8..0000000
--- a/src/app/(features)/dictionary/index.ts
+++ /dev/null
@@ -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";
diff --git a/src/app/(features)/dictionary/page.tsx b/src/app/(features)/dictionary/page.tsx
index dd8e42b..1a9c1cd 100644
--- a/src/app/(features)/dictionary/page.tsx
+++ b/src/app/(features)/dictionary/page.tsx
@@ -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(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(null);
+ const [folders, setFolders] = useState([]);
+ 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 (
+
+ {/* 搜索区域 */}
+
+
+
+
+
+
+ {/* 搜索结果区域 */}
+
+
+ {isSearching && (
+
+ )}
+
+ {!isSearching && hasSearched && !searchResult && (
+
+
{t("noResults")}
+
{t("tryOtherWords")}
+
+ )}
+
+ {!isSearching && searchResult && (
+
+ )}
+
+ {!hasSearched && (
+
+
📚
+
{t("welcomeTitle")}
+
{t("welcomeHint")}
+
+ )}
+
+
+
+ );
+}
diff --git a/src/app/(features)/dictionary/types.ts b/src/app/(features)/dictionary/types.ts
deleted file mode 100644
index 5df1500..0000000
--- a/src/app/(features)/dictionary/types.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-// 从 shared 文件夹导出所有词典类型和类型守卫
-export * from "@/lib/shared";
diff --git a/src/app/(features)/dictionary/utils.ts b/src/app/(features)/dictionary/utils.ts
index f7893af..857f77c 100644
--- a/src/app/(features)/dictionary/utils.ts
+++ b/src/app/(features)/dictionary/utils.ts
@@ -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 {
- const { text, queryLang, definitionLang, forceRelook = false } = options;
+): Promise {
+ 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;
}
diff --git a/src/app/(features)/memorize/Memorize.tsx b/src/app/(features)/memorize/Memorize.tsx
index d9522cb..9c7c222 100644
--- a/src/app/(features)/memorize/Memorize.tsx
+++ b/src/app/(features)/memorize/Memorize.tsx
@@ -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({
diff --git a/src/app/(features)/memorize/page.tsx b/src/app/(features)/memorize/page.tsx
index a2d4707..339dc5e 100644
--- a/src/app/(features)/memorize/page.tsx
+++ b/src/app/(features)/memorize/page.tsx
@@ -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";
diff --git a/src/app/(features)/text-speaker/page.tsx b/src/app/(features)/text-speaker/page.tsx
index 6758e8f..bd3e539 100644
--- a/src/app/(features)/text-speaker/page.tsx
+++ b/src/app/(features)/text-speaker/page.tsx
@@ -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");
diff --git a/src/app/(features)/translator/page.tsx b/src/app/(features)/translator/page.tsx
index 34f852e..c4443b6 100644
--- a/src/app/(features)/translator/page.tsx
+++ b/src/app/(features)/translator/page.tsx
@@ -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");
diff --git a/src/app/auth/AuthForm.tsx b/src/app/auth/AuthForm.tsx
index 0e7f2a7..00a9706 100644
--- a/src/app/auth/AuthForm.tsx
+++ b/src/app/auth/AuthForm.tsx
@@ -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";
diff --git a/src/components/ui/Button.tsx b/src/components/ui/Button.tsx
index 267f47a..e11b8fc 100644
--- a/src/components/ui/Button.tsx
+++ b/src/components/ui/Button.tsx
@@ -87,7 +87,7 @@ export default function Button({
const selectedClass = variant === "secondary" && selected ? "bg-gray-100" : "";
// Background color for primary variant
- const backgroundColor = variant === "primary" ? COLORS.primary : undefined;
+ const backgroundColor = variant === "primary" ? '#35786f' : undefined;
// Combine all classes
const combinedClasses = `
diff --git a/src/lib/server/bigmodel/dictionary/README.md b/src/lib/bigmodel/dictionary/README.md
similarity index 100%
rename from src/lib/server/bigmodel/dictionary/README.md
rename to src/lib/bigmodel/dictionary/README.md
diff --git a/src/lib/server/bigmodel/dictionary/index.ts b/src/lib/bigmodel/dictionary/index.ts
similarity index 100%
rename from src/lib/server/bigmodel/dictionary/index.ts
rename to src/lib/bigmodel/dictionary/index.ts
diff --git a/src/lib/server/bigmodel/dictionary/orchestrator.ts b/src/lib/bigmodel/dictionary/orchestrator.ts
similarity index 88%
rename from src/lib/server/bigmodel/dictionary/orchestrator.ts
rename to src/lib/bigmodel/dictionary/orchestrator.ts
index c2ecff5..b720787 100644
--- a/src/lib/server/bigmodel/dictionary/orchestrator.ts
+++ b/src/lib/bigmodel/dictionary/orchestrator.ts
@@ -1,21 +1,15 @@
-import { DictLookUpResponse } from "@/lib/shared";
-
+import { LookUpServiceOutputDto } from "@/modules/dictionary/dictionary-service-dto";
import { analyzeInput } from "./stage1-inputAnalysis";
import { determineSemanticMapping } from "./stage2-semanticMapping";
import { generateStandardForm } from "./stage3-standardForm";
import { generateEntries } from "./stage4-entriesGeneration";
+import { LookUpError } from "@/lib/errors";
-/**
- * 词典查询主编排器
- *
- * 将多个独立的 LLM 调用串联起来,每个阶段都有代码层面的数据验证
- * 只要有一环失败,直接返回错误
- */
export async function executeDictionaryLookup(
text: string,
queryLang: string,
definitionLang: string
-): Promise {
+): Promise {
try {
// ========== 阶段 1:输入分析 ==========
console.log("[阶段1] 开始输入分析...");
@@ -80,7 +74,7 @@ export async function executeDictionaryLookup(
console.log("[阶段4] 词条生成完成:", entriesResult);
// ========== 组装最终结果 ==========
- const finalResult: DictLookUpResponse = {
+ const finalResult: LookUpServiceOutputDto = {
standardForm: standardFormResult.standardForm,
entries: entriesResult.entries,
};
@@ -91,8 +85,7 @@ export async function executeDictionaryLookup(
} catch (error) {
console.error("[错误] 词典查询失败:", error);
- // 任何阶段失败都返回错误(包含 reason)
const errorMessage = error instanceof Error ? error.message : "未知错误";
- throw errorMessage;
+ throw new LookUpError(errorMessage);
}
}
diff --git a/src/lib/server/bigmodel/dictionary/stage1-inputAnalysis.ts b/src/lib/bigmodel/dictionary/stage1-inputAnalysis.ts
similarity index 97%
rename from src/lib/server/bigmodel/dictionary/stage1-inputAnalysis.ts
rename to src/lib/bigmodel/dictionary/stage1-inputAnalysis.ts
index eb6b183..45c677f 100644
--- a/src/lib/server/bigmodel/dictionary/stage1-inputAnalysis.ts
+++ b/src/lib/bigmodel/dictionary/stage1-inputAnalysis.ts
@@ -1,5 +1,5 @@
import { getAnswer } from "../zhipu";
-import { parseAIGeneratedJSON } from "@/lib/utils";
+import { parseAIGeneratedJSON } from "@/utils/json";
import { InputAnalysisResult } from "./types";
/**
diff --git a/src/lib/server/bigmodel/dictionary/stage2-semanticMapping.ts b/src/lib/bigmodel/dictionary/stage2-semanticMapping.ts
similarity index 98%
rename from src/lib/server/bigmodel/dictionary/stage2-semanticMapping.ts
rename to src/lib/bigmodel/dictionary/stage2-semanticMapping.ts
index 353811e..cd99fa1 100644
--- a/src/lib/server/bigmodel/dictionary/stage2-semanticMapping.ts
+++ b/src/lib/bigmodel/dictionary/stage2-semanticMapping.ts
@@ -1,5 +1,5 @@
import { getAnswer } from "../zhipu";
-import { parseAIGeneratedJSON } from "@/lib/utils";
+import { parseAIGeneratedJSON } from "@/utils/json";
import { SemanticMappingResult } from "./types";
/**
diff --git a/src/lib/server/bigmodel/dictionary/stage3-standardForm.ts b/src/lib/bigmodel/dictionary/stage3-standardForm.ts
similarity index 98%
rename from src/lib/server/bigmodel/dictionary/stage3-standardForm.ts
rename to src/lib/bigmodel/dictionary/stage3-standardForm.ts
index 0da253d..0e9162e 100644
--- a/src/lib/server/bigmodel/dictionary/stage3-standardForm.ts
+++ b/src/lib/bigmodel/dictionary/stage3-standardForm.ts
@@ -1,5 +1,5 @@
import { getAnswer } from "../zhipu";
-import { parseAIGeneratedJSON } from "@/lib/utils";
+import { parseAIGeneratedJSON } from "@/utils/json";
import { StandardFormResult } from "./types";
/**
diff --git a/src/lib/server/bigmodel/dictionary/stage4-entriesGeneration.ts b/src/lib/bigmodel/dictionary/stage4-entriesGeneration.ts
similarity index 98%
rename from src/lib/server/bigmodel/dictionary/stage4-entriesGeneration.ts
rename to src/lib/bigmodel/dictionary/stage4-entriesGeneration.ts
index 9285b45..a2f269d 100644
--- a/src/lib/server/bigmodel/dictionary/stage4-entriesGeneration.ts
+++ b/src/lib/bigmodel/dictionary/stage4-entriesGeneration.ts
@@ -1,5 +1,5 @@
import { getAnswer } from "../zhipu";
-import { parseAIGeneratedJSON } from "@/lib/utils";
+import { parseAIGeneratedJSON } from "@/utils/json";
import { EntriesGenerationResult } from "./types";
/**
diff --git a/src/lib/server/bigmodel/dictionary/types.ts b/src/lib/bigmodel/dictionary/types.ts
similarity index 100%
rename from src/lib/server/bigmodel/dictionary/types.ts
rename to src/lib/bigmodel/dictionary/types.ts
diff --git a/src/lib/server/bigmodel/tts.ts b/src/lib/bigmodel/tts.ts
similarity index 100%
rename from src/lib/server/bigmodel/tts.ts
rename to src/lib/bigmodel/tts.ts
diff --git a/src/lib/server/bigmodel/zhipu.ts b/src/lib/bigmodel/zhipu.ts
similarity index 100%
rename from src/lib/server/bigmodel/zhipu.ts
rename to src/lib/bigmodel/zhipu.ts
diff --git a/src/lib/browser/localStorageOperators.ts b/src/lib/browser/localStorageOperators.ts
deleted file mode 100644
index f55daaf..0000000
--- a/src/lib/browser/localStorageOperators.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-"use client";
-
-import {
- TranslationHistoryArraySchema,
- TranslationHistorySchema,
-} from "@/lib/interfaces";
-import z from "zod";
-import { shallowEqual } from "../utils";
-import { logger } from "@/lib/logger";
-
-export const getLocalStorageOperator = (
- key: string,
- schema: T,
-) => {
- return {
- get: (): z.infer => {
- try {
- if (!globalThis.localStorage) return [] as z.infer;
- const item = globalThis.localStorage.getItem(key);
-
- if (!item) return [] as z.infer;
-
- const rawData = JSON.parse(item) as z.infer;
- const result = schema.safeParse(rawData);
-
- if (result.success) {
- return result.data;
- } else {
- logger.error(
- "Invalid data structure in localStorage:",
- result.error,
- );
- return [] as z.infer;
- }
- } catch (e) {
- logger.error(`Failed to parse ${key} data:`, e);
- return [] as z.infer;
- }
- },
- set: (data: z.infer) => {
- if (!globalThis.localStorage) return;
- globalThis.localStorage.setItem(key, JSON.stringify(data));
- return data;
- },
- };
-};
-
-
-const MAX_HISTORY_LENGTH = 50;
-export const tlso = getLocalStorageOperator<
- typeof TranslationHistoryArraySchema
->("translator", TranslationHistoryArraySchema);
-
-export const tlsoPush = (item: z.infer) => {
- const oldHistory = tlso.get();
- if (oldHistory.some((v) => shallowEqual(v, item))) return oldHistory;
-
- const newHistory = [...oldHistory, item].slice(-MAX_HISTORY_LENGTH);
- tlso.set(newHistory);
-
- return newHistory;
-};
diff --git a/src/lib/errors.ts b/src/lib/errors.ts
new file mode 100644
index 0000000..283f085
--- /dev/null
+++ b/src/lib/errors.ts
@@ -0,0 +1,2 @@
+export class ValidateError extends Error { };
+export class LookUpError extends Error { };
\ No newline at end of file
diff --git a/src/lib/interfaces.ts b/src/lib/interfaces.ts
deleted file mode 100644
index 7c502a7..0000000
--- a/src/lib/interfaces.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-import z from "zod";
-
-export interface Word {
- word: string;
- x: number;
- y: number;
-}
-export interface Letter {
- letter: string;
- letter_name_ipa: string;
- letter_sound_ipa: string;
- roman_letter?: string;
-}
-export type SupportedAlphabets =
- | "japanese"
- | "english"
- | "esperanto"
- | "uyghur";
-export const TextSpeakerItemSchema = z.object({
- text: z.string(),
- ipa: z.string().optional(),
- language: z.string(),
-});
-export const TextSpeakerArraySchema = z.array(TextSpeakerItemSchema);
-
-export const WordDataSchema = z.object({
- languages: z
- .tuple([z.string(), z.string()])
- .refine(([first, second]) => first !== second, {
- message: "Languages must be different",
- }),
- wordPairs: z
- .array(z.tuple([z.string(), z.string()]))
- .min(1, "At least one word pair is required")
- .refine(
- (pairs) => {
- return pairs.every(
- ([first, second]) => first.trim() !== "" && second.trim() !== "",
- );
- },
- {
- message: "Word pairs cannot contain empty strings",
- },
- ),
-});
-
-export const TranslationHistorySchema = z.object({
- text1: z.string(),
- text2: z.string(),
- language1: z.string(),
- language2: z.string(),
-});
-
-export const TranslationHistoryArraySchema = z.array(TranslationHistorySchema);
-
-export type WordData = z.infer;
diff --git a/src/lib/logger.ts b/src/lib/logger.ts
deleted file mode 100644
index 70b6dfc..0000000
--- a/src/lib/logger.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-/**
- * 统一的日志工具
- * 在生产环境中可以通过环境变量控制日志级别
- */
-
-type LogLevel = 'info' | 'warn' | 'error';
-
-const isDevelopment = process.env.NODE_ENV === 'development';
-
-export const logger = {
- error: (message: string, error?: unknown) => {
- if (isDevelopment) {
- console.error(message, error);
- }
- // 在生产环境中,这里可以发送到错误追踪服务(如 Sentry)
- },
-
- warn: (message: string, data?: unknown) => {
- if (isDevelopment) {
- console.warn(message, data);
- }
- },
-
- info: (message: string, data?: unknown) => {
- if (isDevelopment) {
- console.info(message, data);
- }
- },
-};
diff --git a/src/lib/server/bigmodel/dictionaryActions.ts b/src/lib/server/bigmodel/dictionaryActions.ts
deleted file mode 100644
index 40014d9..0000000
--- a/src/lib/server/bigmodel/dictionaryActions.ts
+++ /dev/null
@@ -1,140 +0,0 @@
-"use server";
-
-import { executeDictionaryLookup } from "./dictionary";
-import { createLookUp, createPhrase, createWord, createPhraseEntry, createWordEntry, selectLastLookUp } from "../services/dictionaryService";
-import { DictLookUpRequest, DictWordResponse, isDictPhraseResponse, isDictWordResponse, type DictLookUpResponse } from "@/lib/shared";
-import { lookUpValidation } from "@/lib/shared/validations/dictionaryValidations";
-
-const saveResult = async (req: DictLookUpRequest, res: DictLookUpResponse) => {
- if (isDictPhraseResponse(res)) {
- // 先创建 Phrase
- const phrase = await createPhrase({
- standardForm: res.standardForm,
- queryLang: req.queryLang,
- definitionLang: req.definitionLang,
- });
-
- // 创建 Lookup
- await createLookUp({
- userId: req.userId,
- text: req.text,
- queryLang: req.queryLang,
- definitionLang: req.definitionLang,
- dictionaryPhraseId: phrase.id,
- });
-
- // 创建 Entries
- for (const entry of res.entries) {
- await createPhraseEntry({
- phraseId: phrase.id,
- definition: entry.definition,
- example: entry.example,
- });
- }
- } else if (isDictWordResponse(res)) {
- // 先创建 Word
- const word = await createWord({
- standardForm: (res as DictWordResponse).standardForm,
- queryLang: req.queryLang,
- definitionLang: req.definitionLang,
- });
-
- // 创建 Lookup
- await createLookUp({
- userId: req.userId,
- text: req.text,
- queryLang: req.queryLang,
- definitionLang: req.definitionLang,
- dictionaryWordId: word.id,
- });
-
- // 创建 Entries
- for (const entry of (res as DictWordResponse).entries) {
- await createWordEntry({
- wordId: word.id,
- ipa: entry.ipa,
- definition: entry.definition,
- partOfSpeech: entry.partOfSpeech,
- example: entry.example,
- });
- }
- }
-};
-
-/**
- * 查询单词或短语
- *
- * 使用模块化的词典查询系统,将提示词拆分为6个阶段:
- * - 阶段0:基础系统提示
- * - 阶段1:输入解析与语言识别
- * - 阶段2:跨语言语义映射决策
- * - 阶段3:standardForm 生成与规范化
- * - 阶段4:释义与词条生成
- * - 阶段5:错误处理
- * - 阶段6:最终输出封装
- */
-export const lookUp = async (req: DictLookUpRequest): Promise => {
- const {
- text,
- queryLang,
- forceRelook = false,
- definitionLang,
- userId
- } = req;
-
- lookUpValidation(req);
-
- const lastLookUp = await selectLastLookUp({
- text,
- queryLang,
- definitionLang
- });
-
- if (forceRelook || !lastLookUp) {
- // 使用新的模块化查询系统
- const response = await executeDictionaryLookup(
- text,
- queryLang,
- definitionLang
- );
-
- saveResult({
- text,
- queryLang,
- definitionLang,
- userId,
- forceRelook
- }, response);
-
- return response;
- } else {
- // 从数据库返回缓存的结果
- if (lastLookUp.dictionaryWordId) {
- createLookUp({
- userId: userId,
- text: text,
- queryLang: queryLang,
- definitionLang: definitionLang,
- dictionaryWordId: lastLookUp.dictionaryWordId,
- });
- return {
- standardForm: lastLookUp.dictionaryWord!.standardForm,
- entries: lastLookUp.dictionaryWord!.entries
- };
- } else if (lastLookUp.dictionaryPhraseId) {
- createLookUp({
- userId: userId,
- text: text,
- queryLang: queryLang,
- definitionLang: definitionLang,
- dictionaryPhraseId: lastLookUp.dictionaryPhraseId
- });
- return {
- standardForm: lastLookUp.dictionaryPhrase!.standardForm,
- entries: lastLookUp.dictionaryPhrase!.entries
- };
- } else {
- throw "错误D101";
- }
- }
-};
diff --git a/src/lib/server/services/dictionaryService.ts b/src/lib/server/services/dictionaryService.ts
deleted file mode 100644
index 7a157b8..0000000
--- a/src/lib/server/services/dictionaryService.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-"use server";
-
-import {
- CreateDictionaryLookUpInput,
- DictionaryLookUpQuery,
- CreateDictionaryPhraseInput,
- CreateDictionaryPhraseEntryInput,
- CreateDictionaryWordInput,
- CreateDictionaryWordEntryInput
-} from "./types";
-import prisma from "../../db";
-
-export async function selectLastLookUp(content: DictionaryLookUpQuery) {
- return prisma.dictionaryLookUp.findFirst({
- where: content,
- include: {
- dictionaryPhrase: {
- include: {
- entries: true
- }
- },
- dictionaryWord: {
- include: {
- entries: true
- }
- }
- },
- orderBy: {
- createdAt: 'desc'
- }
- });
-}
-
-export async function createPhraseEntry(content: CreateDictionaryPhraseEntryInput) {
- return prisma.dictionaryPhraseEntry.create({
- data: content
- });
-}
-
-export async function createWordEntry(content: CreateDictionaryWordEntryInput) {
- return prisma.dictionaryWordEntry.create({
- data: content
- });
-}
-
-export async function createPhrase(content: CreateDictionaryPhraseInput) {
- return prisma.dictionaryPhrase.create({
- data: content
- });
-}
-
-export async function createWord(content: CreateDictionaryWordInput) {
- return prisma.dictionaryWord.create({
- data: content
- });
-}
-
-export async function createLookUp(content: CreateDictionaryLookUpInput) {
- return prisma.dictionaryLookUp.create({
- data: content
- });
-}
diff --git a/src/lib/server/services/pairService.ts b/src/lib/server/services/pairService.ts
deleted file mode 100644
index c909288..0000000
--- a/src/lib/server/services/pairService.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-"use server";
-
-import { CreatePairInput, UpdatePairInput } from "./types";
-import prisma from "../../db";
-
-export async function createPair(data: CreatePairInput) {
- return prisma.pair.create({
- data: data,
- });
-}
-
-export async function deletePairById(id: number) {
- return prisma.pair.delete({
- where: {
- id: id,
- },
- });
-}
-
-export async function updatePairById(
- id: number,
- data: UpdatePairInput,
-) {
- return prisma.pair.update({
- where: {
- id: id,
- },
- data: data,
- });
-}
-
-export async function getPairCountByFolderId(folderId: number) {
- return prisma.pair.count({
- where: {
- folderId: folderId,
- },
- });
-}
-
-export async function getPairsByFolderId(folderId: number) {
- return prisma.pair.findMany({
- where: {
- folderId: folderId,
- },
- });
-}
diff --git a/src/lib/server/services/userService.ts b/src/lib/server/services/userService.ts
deleted file mode 100644
index 608c3fd..0000000
--- a/src/lib/server/services/userService.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import prisma from "@/lib/db";
-import { randomUUID } from "crypto";
-
-export async function createUserIfNotExists(email: string, name?: string | null) {
- const user = await prisma.user.upsert({
- where: {
- email: email,
- },
- update: {},
- create: {
- id: randomUUID(),
- email: email,
- name: name || "New User",
- },
- });
- return user;
-}
-
-export async function getUserIdByEmail(email: string) {
- const user = await prisma.user.findUnique({
- where: {
- email: email,
- },
- select: {
- id: true,
- },
- });
- return user ? user.id : null;
-}
diff --git a/src/lib/shared/dictionaryTypes.ts b/src/lib/shared/dictionaryTypes.ts
deleted file mode 100644
index 7b4d5f0..0000000
--- a/src/lib/shared/dictionaryTypes.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-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 DictWordResponse = {
- standardForm: string;
- entries: DictWordEntry[];
-};
-
-export type DictPhraseResponse = {
- standardForm: string;
- entries: DictPhraseEntry[];
-};
-
-export type DictLookUpResponse =
- | DictWordResponse
- | DictPhraseResponse;
-
-// 类型守卫:判断是否为单词响应
-export function isDictWordResponse(
- response: DictLookUpResponse
-): response is DictWordResponse {
- 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 {
- const entries = (response as DictWordResponse | DictPhraseResponse).entries;
- return entries.length > 0 && !("ipa" in entries[0] || "partOfSpeech" in entries[0]);
-}
diff --git a/src/lib/shared/index.ts b/src/lib/shared/index.ts
deleted file mode 100644
index 30779b4..0000000
--- a/src/lib/shared/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export * from "./dictionaryTypes";
diff --git a/src/lib/shared/validations/dictionaryValidations.ts b/src/lib/shared/validations/dictionaryValidations.ts
deleted file mode 100644
index e4dc2c3..0000000
--- a/src/lib/shared/validations/dictionaryValidations.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { DictLookUpRequest } from "@/lib/shared";
-
-export const lookUpValidation = (req: DictLookUpRequest) => {
- const {
- text,
- queryLang,
- definitionLang,
- } = req;
-
- if (text.length > 30)
- throw Error("The input should not exceed 30 characters.");
- if (queryLang.length > 20)
- throw Error("The query language should not exceed 20 characters.");
- if (definitionLang.length > 20)
- throw Error("The definition language should not exceed 20 characters.");
- if (queryLang.length > 20)
- throw Error("The query language should not exceed 20 characters.");
- if (queryLang.length > 20)
- throw Error("The query language should not exceed 20 characters.");
- if (queryLang.length > 20)
- throw Error("The query language should not exceed 20 characters.");
-};
diff --git a/src/lib/theme/colors.ts b/src/lib/theme/colors.ts
deleted file mode 100644
index 33eacfe..0000000
--- a/src/lib/theme/colors.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-/**
- * 主题配色常量
- * 集中管理应用的品牌颜色
- *
- * 注意:Tailwind CSS 已有的标准颜色(gray、red 等)请直接使用 Tailwind 类名
- * 这里只定义项目独有的品牌色
- */
-export const COLORS = {
- // ===== 主色调 =====
- /** 主绿色 - 应用主题色,用于页面背景、主要按钮 */
- primary: '#35786f',
- /** 悬停绿色 - 按钮悬停状态 */
- primaryHover: '#2d5f58'
-} as const;
diff --git a/src/modules/dictionary/dictionary-action-dto.ts b/src/modules/dictionary/dictionary-action-dto.ts
new file mode 100644
index 0000000..881a4af
--- /dev/null
+++ b/src/modules/dictionary/dictionary-action-dto.ts
@@ -0,0 +1,30 @@
+import { ValidateError } from "@/lib/errors";
+import { TSharedItem } from "@/shared";
+import z from "zod";
+
+const DictionaryActionInputDtoSchema = z.object({
+ text: z.string().min(1, 'Empty text.').max(30, 'Text too long.'),
+ queryLang: z.string().min(1, 'Query lang too short.').max(20, 'Query lang too long.'),
+ forceRelook: z.boolean(),
+ definitionLang: z.string().min(1, 'Definition lang too short.').max(20, 'Definition lang too long.'),
+ userId: z.string().optional()
+});
+
+export type DictionaryActionInputDto = z.infer;
+
+export const validateDictionaryActionInput = (dto: DictionaryActionInputDto): DictionaryActionInputDto => {
+ const result = DictionaryActionInputDtoSchema.safeParse(dto);
+ if (result.success) return result.data;
+
+ const errorMessages = result.error.issues.map((issue) =>
+ `${issue.path.join('.')}: ${issue.message}`
+ ).join('; ');
+
+ throw new ValidateError(`Validation failed: ${errorMessages}`);
+};
+
+export type DictionaryActionOutputDto = {
+ message: string,
+ success: boolean;
+ data?: TSharedItem;
+};
diff --git a/src/modules/dictionary/dictionary-action.ts b/src/modules/dictionary/dictionary-action.ts
new file mode 100644
index 0000000..c508d2a
--- /dev/null
+++ b/src/modules/dictionary/dictionary-action.ts
@@ -0,0 +1,27 @@
+"use server";
+
+import { DictionaryActionInputDto, DictionaryActionOutputDto, validateDictionaryActionInput } from "./dictionary-action-dto";
+import { ValidateError } from "@/lib/errors";
+import { lookUpService } from "./dictionary-service";
+
+export const lookUpDictionaryAction = async (dto: DictionaryActionInputDto): Promise => {
+ try {
+ return {
+ message: 'success',
+ success: true,
+ data: await lookUpService(validateDictionaryActionInput(dto))
+ };
+ } catch (e) {
+ if (e instanceof ValidateError) {
+ return {
+ success: false,
+ message: e.message
+ };
+ }
+ console.log(e);
+ return {
+ success: false,
+ message: 'Unknown error occured.'
+ };
+ }
+};
diff --git a/src/modules/dictionary/dictionary-repository-dto.ts b/src/modules/dictionary/dictionary-repository-dto.ts
new file mode 100644
index 0000000..add7e7a
--- /dev/null
+++ b/src/modules/dictionary/dictionary-repository-dto.ts
@@ -0,0 +1,38 @@
+import { TSharedItem } from "@/shared";
+
+export type CreateDictionaryLookUpInputDto = {
+ userId?: string;
+ text: string;
+ queryLang: string;
+ definitionLang: string;
+ dictionaryItemId?: number;
+};
+
+export type SelectLastLookUpResultOutputDto = TSharedItem & {id: number} | null;
+
+export type CreateDictionaryItemInputDto = {
+ standardForm: string;
+ queryLang: string;
+ definitionLang: string;
+};
+
+export type CreateDictionaryEntryInputDto = {
+ itemId: number;
+ ipa?: string;
+ definition: string;
+ partOfSpeech?: string;
+ example: string;
+};
+
+export type CreateDictionaryEntryWithoutItemIdInputDto = {
+ ipa?: string;
+ definition: string;
+ partOfSpeech?: string;
+ example: string;
+};
+
+export type SelectLastLookUpResultInputDto = {
+ text: string,
+ queryLang: string,
+ definitionLang: string;
+};
diff --git a/src/modules/dictionary/dictionary-repository.ts b/src/modules/dictionary/dictionary-repository.ts
new file mode 100644
index 0000000..fb41555
--- /dev/null
+++ b/src/modules/dictionary/dictionary-repository.ts
@@ -0,0 +1,86 @@
+import { stringNormalize } from "@/utils/string";
+import {
+ CreateDictionaryEntryInputDto,
+ CreateDictionaryEntryWithoutItemIdInputDto,
+ CreateDictionaryItemInputDto,
+ CreateDictionaryLookUpInputDto,
+ SelectLastLookUpResultInputDto,
+ SelectLastLookUpResultOutputDto,
+} from "./dictionary-repository-dto";
+import prisma from "@/lib/db";
+
+export async function selectLastLookUpResult(dto: SelectLastLookUpResultInputDto): Promise {
+ const result = await prisma.dictionaryLookUp.findFirst({
+ where: {
+ normalizedText: stringNormalize(dto.text),
+ queryLang: dto.queryLang,
+ definitionLang: dto.definitionLang,
+ dictionaryItemId: {
+ not: null
+ }
+ },
+ include: {
+ dictionaryItem: {
+ include: {
+ entries: true
+ }
+ }
+ },
+ orderBy: {
+ createdAt: 'desc'
+ }
+ });
+ if (result && result.dictionaryItem) {
+ const item = result.dictionaryItem;
+ return {
+ id: item.id,
+ standardForm: item.standardForm,
+ entries: item.entries.filter(v => !!v).map(v => {
+ return {
+ ipa: v.ipa || undefined,
+ definition: v.definition,
+ partOfSpeech: v.partOfSpeech || undefined,
+ example: v.example
+ };
+ })
+ };
+ }
+ return null;
+}
+
+export async function createLookUp(content: CreateDictionaryLookUpInputDto) {
+ return (await prisma.dictionaryLookUp.create({
+ data: { ...content, normalizedText: stringNormalize(content.text) }
+ })).id;
+}
+
+export async function createLookUpWithItemAndEntries(
+ itemData: CreateDictionaryItemInputDto,
+ lookUpData: CreateDictionaryLookUpInputDto,
+ entries: CreateDictionaryEntryWithoutItemIdInputDto[]
+) {
+ return await prisma.$transaction(async (tx) => {
+ const item = await tx.dictionaryItem.create({
+ data: itemData
+ });
+
+ await tx.dictionaryLookUp.create({
+ data: {
+ ...lookUpData,
+ normalizedText: stringNormalize(lookUpData.text),
+ dictionaryItemId: item.id
+ }
+ });
+
+ for (const entry of entries) {
+ await tx.dictionaryEntry.create({
+ data: {
+ ...entry,
+ itemId: item.id
+ }
+ });
+ }
+
+ return item.id;
+ });
+}
diff --git a/src/modules/dictionary/dictionary-service-dto.ts b/src/modules/dictionary/dictionary-service-dto.ts
new file mode 100644
index 0000000..bc90218
--- /dev/null
+++ b/src/modules/dictionary/dictionary-service-dto.ts
@@ -0,0 +1,11 @@
+import { TSharedItem } from "@/shared";
+
+export type LookUpServiceInputDto = {
+ text: string,
+ queryLang: string,
+ definitionLang: string,
+ forceRelook: boolean,
+ userId?: string;
+};
+
+export type LookUpServiceOutputDto = TSharedItem;
diff --git a/src/modules/dictionary/dictionary-service.ts b/src/modules/dictionary/dictionary-service.ts
new file mode 100644
index 0000000..5591e5a
--- /dev/null
+++ b/src/modules/dictionary/dictionary-service.ts
@@ -0,0 +1,61 @@
+import { executeDictionaryLookup } from "@/lib/bigmodel/dictionary";
+import { createLookUp, createLookUpWithItemAndEntries, selectLastLookUpResult } from "./dictionary-repository";
+import { LookUpServiceInputDto } from "./dictionary-service-dto";
+
+export const lookUpService = async (dto: LookUpServiceInputDto) => {
+ const {
+ text,
+ queryLang,
+ userId,
+ definitionLang,
+ forceRelook
+ } = dto;
+
+ const lastLookUpResult = await selectLastLookUpResult({
+ text,
+ queryLang,
+ definitionLang,
+ });
+
+ if (forceRelook || !lastLookUpResult) {
+ const response = await executeDictionaryLookup(
+ text,
+ queryLang,
+ definitionLang
+ );
+
+ // 使用事务确保数据一致性
+ createLookUpWithItemAndEntries(
+ {
+ standardForm: response.standardForm,
+ queryLang,
+ definitionLang
+ },
+ {
+ userId,
+ text,
+ queryLang,
+ definitionLang,
+ },
+ response.entries
+ ).catch(error => {
+ console.error('Failed to save dictionary data:', error);
+ });
+
+ return response;
+ } else {
+ createLookUp({
+ userId: userId,
+ text: text,
+ queryLang: queryLang,
+ definitionLang: definitionLang,
+ dictionaryItemId: lastLookUpResult.id
+ }).catch(error => {
+ console.error('Failed to save dictionary data:', error);
+ });
+ return {
+ standardForm: lastLookUpResult.standardForm,
+ entries: lastLookUpResult.entries
+ };
+ }
+};
\ No newline at end of file
diff --git a/src/modules/dictionary/index.ts b/src/modules/dictionary/index.ts
new file mode 100644
index 0000000..b6517a0
--- /dev/null
+++ b/src/modules/dictionary/index.ts
@@ -0,0 +1,2 @@
+export * from "./dictionary-action";
+export * from "./dictionary-action-dto";
diff --git a/src/modules/folder/folder-aciton.ts b/src/modules/folder/folder-aciton.ts
new file mode 100644
index 0000000..e69de29
diff --git a/src/modules/folder/folder-action-dto.ts b/src/modules/folder/folder-action-dto.ts
new file mode 100644
index 0000000..e69de29
diff --git a/src/modules/folder/folder-repository.ts b/src/modules/folder/folder-repository.ts
new file mode 100644
index 0000000..e69de29
diff --git a/src/modules/folder/folder-service-dto.ts b/src/modules/folder/folder-service-dto.ts
new file mode 100644
index 0000000..e69de29
diff --git a/src/lib/server/services/folderService.ts b/src/modules/folder/folder-service.ts
similarity index 91%
rename from src/lib/server/services/folderService.ts
rename to src/modules/folder/folder-service.ts
index 5c00dfc..9ce74d3 100644
--- a/src/lib/server/services/folderService.ts
+++ b/src/modules/folder/folder-service.ts
@@ -1,7 +1,5 @@
-"use server";
-
-import { CreateFolderInput, UpdateFolderInput } from "./types";
-import prisma from "../../db";
+import { CreateFolderInput, UpdateFolderInput } from "../translator/translator-dto";
+import prisma from "@/lib/db";
export async function getFoldersByUserId(userId: string) {
return prisma.folder.findMany({
diff --git a/src/modules/pair/pair-action-dto.ts b/src/modules/pair/pair-action-dto.ts
new file mode 100644
index 0000000..e69de29
diff --git a/src/modules/pair/pair-action.ts b/src/modules/pair/pair-action.ts
new file mode 100644
index 0000000..6ae8d13
--- /dev/null
+++ b/src/modules/pair/pair-action.ts
@@ -0,0 +1 @@
+"use server";
\ No newline at end of file
diff --git a/src/modules/pair/pair-repository.ts b/src/modules/pair/pair-repository.ts
new file mode 100644
index 0000000..6d4b63b
--- /dev/null
+++ b/src/modules/pair/pair-repository.ts
@@ -0,0 +1,44 @@
+import { CreatePairInput, UpdatePairInput } from "../translator/translator-dto";
+import prisma from "@/lib/db";
+
+export async function createPair(data: CreatePairInput) {
+ return (await prisma.pair.create({
+ data: data,
+ })).id;
+}
+
+export async function deletePairById(id: number) {
+ await prisma.pair.delete({
+ where: {
+ id: id,
+ },
+ });
+}
+
+export async function updatePairById(
+ id: number,
+ data: UpdatePairInput,
+) {
+ await prisma.pair.update({
+ where: {
+ id: id,
+ },
+ data: data,
+ });
+}
+
+export async function getPairCountByFolderId(folderId: number) {
+ return prisma.pair.count({
+ where: {
+ folderId: folderId,
+ },
+ });
+}
+
+export async function getPairsByFolderId(folderId: number) {
+ return prisma.pair.findMany({
+ where: {
+ folderId: folderId,
+ },
+ });
+}
diff --git a/src/modules/pair/pair-service-dto.ts b/src/modules/pair/pair-service-dto.ts
new file mode 100644
index 0000000..e69de29
diff --git a/src/modules/pair/pair-service.ts b/src/modules/pair/pair-service.ts
new file mode 100644
index 0000000..e69de29
diff --git a/src/modules/translator/translator-action-dto.ts b/src/modules/translator/translator-action-dto.ts
new file mode 100644
index 0000000..e69de29
diff --git a/src/lib/server/bigmodel/translatorActions.ts b/src/modules/translator/translator-action.ts
similarity index 98%
rename from src/lib/server/bigmodel/translatorActions.ts
rename to src/modules/translator/translator-action.ts
index 9f8e47f..fbf686b 100644
--- a/src/lib/server/bigmodel/translatorActions.ts
+++ b/src/modules/translator/translator-action.ts
@@ -1,8 +1,8 @@
"use server";
-import { getAnswer } from "./zhipu";
-import { selectLatestTranslation, createTranslationHistory } from "../services/translatorService";
-import { TranslateTextInput, TranslateTextOutput, TranslationLLMResponse } from "../services/types";
+import { getAnswer } from "@/lib/bigmodel/zhipu";
+import { selectLatestTranslation, createTranslationHistory } from "./translator-service";
+import { TranslateTextInput, TranslateTextOutput, TranslationLLMResponse } from "./translator-dto";
/**
* @deprecated 请使用 translateText 函数代替
diff --git a/src/lib/server/services/types.ts b/src/modules/translator/translator-dto.ts
similarity index 65%
rename from src/lib/server/services/types.ts
rename to src/modules/translator/translator-dto.ts
index 0094ca2..5ac0671 100644
--- a/src/lib/server/services/types.ts
+++ b/src/modules/translator/translator-dto.ts
@@ -50,51 +50,6 @@ export interface TranslationHistoryQuery {
targetLanguage: string;
}
-// Dictionary 相关
-export interface CreateDictionaryLookUpInput {
- userId?: string;
- text: string;
- queryLang: string;
- definitionLang: string;
- dictionaryWordId?: number;
- dictionaryPhraseId?: number;
-}
-
-export interface DictionaryLookUpQuery {
- userId?: string;
- text?: string;
- queryLang?: string;
- definitionLang?: string;
- dictionaryWordId?: number;
- dictionaryPhraseId?: number;
-}
-
-export interface CreateDictionaryWordInput {
- standardForm: string;
- queryLang: string;
- definitionLang: string;
-}
-
-export interface CreateDictionaryPhraseInput {
- standardForm: string;
- queryLang: string;
- definitionLang: string;
-}
-
-export interface CreateDictionaryWordEntryInput {
- wordId: number;
- ipa: string;
- definition: string;
- partOfSpeech: string;
- example: string;
-}
-
-export interface CreateDictionaryPhraseEntryInput {
- phraseId: number;
- definition: string;
- example: string;
-}
-
// 翻译相关 - 统一翻译函数
export interface TranslateTextInput {
sourceText: string;
diff --git a/src/modules/translator/translator-repository.ts b/src/modules/translator/translator-repository.ts
new file mode 100644
index 0000000..e69de29
diff --git a/src/modules/translator/translator-service-dto.ts b/src/modules/translator/translator-service-dto.ts
new file mode 100644
index 0000000..e69de29
diff --git a/src/lib/server/services/translatorService.ts b/src/modules/translator/translator-service.ts
similarity index 92%
rename from src/lib/server/services/translatorService.ts
rename to src/modules/translator/translator-service.ts
index d5e8423..679d757 100644
--- a/src/lib/server/services/translatorService.ts
+++ b/src/modules/translator/translator-service.ts
@@ -1,7 +1,7 @@
"use server";
-import { CreateTranslationHistoryInput, TranslationHistoryQuery } from "./types";
-import prisma from "../../db";
+import { CreateTranslationHistoryInput, TranslationHistoryQuery } from "./translator-dto";
+import prisma from "@/lib/db";
/**
* 创建翻译历史记录
diff --git a/src/modules/user/user-action-dto.ts b/src/modules/user/user-action-dto.ts
new file mode 100644
index 0000000..e69de29
diff --git a/src/lib/actions/auth.ts b/src/modules/user/user-action.ts
similarity index 100%
rename from src/lib/actions/auth.ts
rename to src/modules/user/user-action.ts
diff --git a/src/modules/user/user-repository.ts b/src/modules/user/user-repository.ts
new file mode 100644
index 0000000..e69de29
diff --git a/src/modules/user/user-service-dto.ts b/src/modules/user/user-service-dto.ts
new file mode 100644
index 0000000..e69de29
diff --git a/src/modules/user/user-service.ts b/src/modules/user/user-service.ts
new file mode 100644
index 0000000..e69de29
diff --git a/src/shared/dictionary-type.ts b/src/shared/dictionary-type.ts
new file mode 100644
index 0000000..4d41b1d
--- /dev/null
+++ b/src/shared/dictionary-type.ts
@@ -0,0 +1,11 @@
+export type TSharedEntry = {
+ definition: string,
+ example: string,
+ partOfSpeech?: string;
+ ipa?: string;
+};
+
+export type TSharedItem = {
+ standardForm: string,
+ entries: TSharedEntry[];
+};
\ No newline at end of file
diff --git a/src/shared/folder-related-type.ts b/src/shared/folder-related-type.ts
new file mode 100644
index 0000000..4950db4
--- /dev/null
+++ b/src/shared/folder-related-type.ts
@@ -0,0 +1,3 @@
+export type TSharedPair = {
+
+};
\ No newline at end of file
diff --git a/src/shared/index.ts b/src/shared/index.ts
new file mode 100644
index 0000000..7c3ea2a
--- /dev/null
+++ b/src/shared/index.ts
@@ -0,0 +1 @@
+export * from './dictionary-type';
\ No newline at end of file
diff --git a/src/utils/json.ts b/src/utils/json.ts
new file mode 100644
index 0000000..234aa98
--- /dev/null
+++ b/src/utils/json.ts
@@ -0,0 +1,25 @@
+export function parseAIGeneratedJSON(aiResponse: string): T {
+ // 匹配 ```json ... ``` 包裹的内容
+ const jsonMatch = aiResponse.match(/```json\s*([\s\S]*?)\s*```/);
+
+ if (jsonMatch && jsonMatch[1]) {
+ try {
+ return JSON.parse(jsonMatch[1].trim());
+ } catch (error) {
+ if (error instanceof Error) {
+ throw new Error(`Failed to parse JSON: ${error.message}`);
+ } else if (typeof error === 'string') {
+ throw new Error(`Failed to parse JSON: ${error}`);
+ } else {
+ throw new Error('Failed to parse JSON: Unknown error');
+ }
+ }
+ }
+
+ // 如果没有找到json代码块,尝试直接解析整个字符串
+ try {
+ return JSON.parse(aiResponse.trim());
+ } catch (error) {
+ throw new Error('No valid JSON found in the response');
+ }
+}
\ No newline at end of file
diff --git a/src/lib/utils.ts b/src/utils/random.ts
similarity index 81%
rename from src/lib/utils.ts
rename to src/utils/random.ts
index a3390dd..1e35e58 100644
--- a/src/lib/utils.ts
+++ b/src/utils/random.ts
@@ -1,3 +1,4 @@
+
export function isNonNegativeInteger(str: string): boolean {
return /^\d+$/.test(str);
}
@@ -5,19 +6,19 @@ export function isNonNegativeInteger(str: string): boolean {
export function shallowEqual(obj1: T, obj2: T): boolean {
const keys1 = Object.keys(obj1) as Array;
const keys2 = Object.keys(obj2) as Array;
-
+
// 首先检查键的数量是否相同
if (keys1.length !== keys2.length) {
return false;
}
-
+
// 然后逐个比较键值对
for (const key of keys1) {
if (obj1[key] !== obj2[key]) {
return false;
}
}
-
+
return true;
}
export class SeededRandom {
@@ -147,29 +148,3 @@ export class SeededRandom {
return shuffled;
}
}
-
-export function parseAIGeneratedJSON(aiResponse: string): T {
- // 匹配 ```json ... ``` 包裹的内容
- const jsonMatch = aiResponse.match(/```json\s*([\s\S]*?)\s*```/);
-
- if (jsonMatch && jsonMatch[1]) {
- try {
- return JSON.parse(jsonMatch[1].trim());
- } catch (error) {
- if (error instanceof Error) {
- throw new Error(`Failed to parse JSON: ${error.message}`);
- } else if (typeof error === 'string') {
- throw new Error(`Failed to parse JSON: ${error}`);
- } else {
- throw new Error('Failed to parse JSON: Unknown error');
- }
- }
- }
-
- // 如果没有找到json代码块,尝试直接解析整个字符串
- try {
- return JSON.parse(aiResponse.trim());
- } catch (error) {
- throw new Error('No valid JSON found in the response');
- }
-}
\ No newline at end of file
diff --git a/src/utils/string.ts b/src/utils/string.ts
new file mode 100644
index 0000000..bd805c9
--- /dev/null
+++ b/src/utils/string.ts
@@ -0,0 +1 @@
+export const stringNormalize = (s: string) => s.trim().toLowerCase();
\ No newline at end of file