Compare commits
3 Commits
main
...
f1d706e20c
| Author | SHA1 | Date | |
|---|---|---|---|
| f1d706e20c | |||
| c7cdf40f2f | |||
| a55e763525 |
@@ -0,0 +1,7 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "pairs" ALTER COLUMN "language1" SET DATA TYPE TEXT,
|
||||||
|
ALTER COLUMN "language2" SET DATA TYPE TEXT;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "translation_history" ALTER COLUMN "source_language" SET DATA TYPE TEXT,
|
||||||
|
ALTER COLUMN "target_language" SET DATA TYPE TEXT;
|
||||||
@@ -77,8 +77,8 @@ model Pair {
|
|||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
text1 String
|
text1 String
|
||||||
text2 String
|
text2 String
|
||||||
language1 String @db.VarChar(20)
|
language1 String
|
||||||
language2 String @db.VarChar(20)
|
language2 String
|
||||||
ipa1 String?
|
ipa1 String?
|
||||||
ipa2 String?
|
ipa2 String?
|
||||||
folderId Int @map("folder_id")
|
folderId Int @map("folder_id")
|
||||||
@@ -194,8 +194,8 @@ model TranslationHistory {
|
|||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
userId String? @map("user_id")
|
userId String? @map("user_id")
|
||||||
sourceText String @map("source_text")
|
sourceText String @map("source_text")
|
||||||
sourceLanguage String @map("source_language") @db.VarChar(20)
|
sourceLanguage String @map("source_language")
|
||||||
targetLanguage String @map("target_language") @db.VarChar(20)
|
targetLanguage String @map("target_language")
|
||||||
translatedText String @map("translated_text")
|
translatedText String @map("translated_text")
|
||||||
sourceIpa String? @map("source_ipa")
|
sourceIpa String? @map("source_ipa")
|
||||||
targetIpa String? @map("target_ipa")
|
targetIpa String? @map("target_ipa")
|
||||||
|
|||||||
@@ -2,16 +2,15 @@
|
|||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import Container from "@/components/ui/Container";
|
import Container from "@/components/ui/Container";
|
||||||
import { lookUp } from "@/lib/server/bigmodel/dictionaryActions";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { authClient } from "@/lib/auth-client";
|
import { authClient } from "@/lib/auth-client";
|
||||||
import { Folder } from "../../../../generated/prisma/browser";
|
import { Folder } from "../../../../generated/prisma/browser";
|
||||||
import { getFoldersByUserId } from "@/lib/server/services/folderService";
|
import { getFoldersByUserId } from "@/lib/server/services/folderService";
|
||||||
import { DictLookUpResponse, isDictErrorResponse } from "./types";
|
import { DictLookUpResponse } from "./types";
|
||||||
import { SearchForm } from "./SearchForm";
|
import { SearchForm } from "./SearchForm";
|
||||||
import { SearchResult } from "./SearchResult";
|
import { SearchResult } from "./SearchResult";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { POPULAR_LANGUAGES } from "./constants";
|
import { POPULAR_LANGUAGES } from "./constants";
|
||||||
|
import { performDictionaryLookup } from "./utils";
|
||||||
|
|
||||||
export default function Dictionary() {
|
export default function Dictionary() {
|
||||||
const t = useTranslations("dictionary");
|
const t = useTranslations("dictionary");
|
||||||
@@ -52,29 +51,22 @@ export default function Dictionary() {
|
|||||||
setHasSearched(true);
|
setHasSearched(true);
|
||||||
setSearchResult(null);
|
setSearchResult(null);
|
||||||
|
|
||||||
try {
|
const result = await performDictionaryLookup(
|
||||||
// 使用查询语言和释义语言的 nativeName
|
{
|
||||||
const result = await lookUp({
|
|
||||||
text: searchQuery,
|
text: searchQuery,
|
||||||
definitionLang: getNativeName(definitionLang),
|
|
||||||
queryLang: getNativeName(queryLang),
|
queryLang: getNativeName(queryLang),
|
||||||
forceRelook: false
|
definitionLang: getNativeName(definitionLang)
|
||||||
})
|
},
|
||||||
|
t
|
||||||
|
);
|
||||||
|
|
||||||
// 检查是否为错误响应
|
if (result.success && result.data) {
|
||||||
if (isDictErrorResponse(result)) {
|
setSearchResult(result.data);
|
||||||
toast.error(result.error);
|
|
||||||
setSearchResult(null);
|
|
||||||
} else {
|
} else {
|
||||||
setSearchResult(result);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("词典查询失败:", error);
|
|
||||||
toast.error(t("lookupFailed"));
|
|
||||||
setSearchResult(null);
|
setSearchResult(null);
|
||||||
} finally {
|
|
||||||
setIsSearching(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setIsSearching(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -112,7 +104,7 @@ export default function Dictionary() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!isSearching && searchResult && !isDictErrorResponse(searchResult) && (
|
{!isSearching && searchResult && (
|
||||||
<SearchResult
|
<SearchResult
|
||||||
searchResult={searchResult}
|
searchResult={searchResult}
|
||||||
searchQuery={searchQuery}
|
searchQuery={searchQuery}
|
||||||
|
|||||||
@@ -38,18 +38,18 @@ export function SearchForm({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 搜索表单 */}
|
{/* 搜索表单 */}
|
||||||
<form onSubmit={onSearch} className="flex gap-2">
|
<form onSubmit={onSearch} className="flex flex-col sm:flex-row gap-2">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => onSearchQueryChange(e.target.value)}
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => onSearchQueryChange(e.target.value)}
|
||||||
placeholder={t("searchPlaceholder")}
|
placeholder={t("searchPlaceholder")}
|
||||||
className="flex-1 px-4 py-3 text-lg text-gray-800 focus:outline-none border-b-2 border-gray-600 bg-white/90 rounded"
|
className="flex-1 min-w-0 px-4 py-3 text-lg text-gray-800 focus:outline-none border-b-2 border-gray-600 bg-white/90 rounded"
|
||||||
/>
|
/>
|
||||||
<LightButton
|
<LightButton
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={isSearching || !searchQuery.trim()}
|
disabled={isSearching || !searchQuery.trim()}
|
||||||
className="px-6 py-3"
|
className="px-6 py-3 whitespace-nowrap text-center sm:min-w-30"
|
||||||
>
|
>
|
||||||
{isSearching ? t("searching") : t("search")}
|
{isSearching ? t("searching") : t("search")}
|
||||||
</LightButton>
|
</LightButton>
|
||||||
|
|||||||
@@ -3,17 +3,15 @@ import { toast } from "sonner";
|
|||||||
import { authClient } from "@/lib/auth-client";
|
import { authClient } from "@/lib/auth-client";
|
||||||
import { Folder } from "../../../../generated/prisma/browser";
|
import { Folder } from "../../../../generated/prisma/browser";
|
||||||
import { createPair } from "@/lib/server/services/pairService";
|
import { createPair } from "@/lib/server/services/pairService";
|
||||||
import { lookUp } from "@/lib/server/bigmodel/dictionaryActions";
|
|
||||||
import {
|
import {
|
||||||
DictWordResponse,
|
DictWordResponse,
|
||||||
DictPhraseResponse,
|
DictPhraseResponse,
|
||||||
isDictWordResponse,
|
isDictWordResponse,
|
||||||
DictWordEntry,
|
DictWordEntry,
|
||||||
isDictErrorResponse,
|
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { DictionaryEntry } from "./DictionaryEntry";
|
import { DictionaryEntry } from "./DictionaryEntry";
|
||||||
import { POPULAR_LANGUAGES } from "./constants";
|
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
import { performDictionaryLookup } from "./utils";
|
||||||
|
|
||||||
interface SearchResultProps {
|
interface SearchResultProps {
|
||||||
searchResult: DictWordResponse | DictPhraseResponse;
|
searchResult: DictWordResponse | DictPhraseResponse;
|
||||||
@@ -46,26 +44,21 @@ export function SearchResult({
|
|||||||
const handleRelookup = async () => {
|
const handleRelookup = async () => {
|
||||||
onSearchingChange(true);
|
onSearchingChange(true);
|
||||||
|
|
||||||
try {
|
const result = await performDictionaryLookup(
|
||||||
const result = await lookUp({
|
{
|
||||||
text: searchQuery,
|
text: searchQuery,
|
||||||
definitionLang: getNativeName(definitionLang),
|
|
||||||
queryLang: getNativeName(queryLang),
|
queryLang: getNativeName(queryLang),
|
||||||
|
definitionLang: getNativeName(definitionLang),
|
||||||
forceRelook: true
|
forceRelook: true
|
||||||
});
|
},
|
||||||
|
t
|
||||||
|
);
|
||||||
|
|
||||||
if (isDictErrorResponse(result)) {
|
if (result.success && result.data) {
|
||||||
toast.error(result.error);
|
onResultUpdate(result.data);
|
||||||
} else {
|
|
||||||
onResultUpdate(result);
|
|
||||||
toast.success(t("relookupSuccess"));
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error("词典重新查询失败:", error);
|
|
||||||
toast.error(t("lookupFailed"));
|
|
||||||
} finally {
|
|
||||||
onSearchingChange(false);
|
onSearchingChange(false);
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
|
|||||||
51
src/app/(features)/dictionary/utils.ts
Normal file
51
src/app/(features)/dictionary/utils.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { toast } from "sonner";
|
||||||
|
import { lookUp } from "@/lib/server/bigmodel/dictionaryActions";
|
||||||
|
import {
|
||||||
|
DictWordResponse,
|
||||||
|
DictPhraseResponse,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
|
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,
|
||||||
|
t?: (key: string) => string
|
||||||
|
): Promise<LookupResult> {
|
||||||
|
const { text, queryLang, definitionLang, forceRelook = false } = options;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await lookUp({
|
||||||
|
text,
|
||||||
|
queryLang,
|
||||||
|
definitionLang,
|
||||||
|
forceRelook
|
||||||
|
});
|
||||||
|
|
||||||
|
// 成功时显示提示(仅强制重新查询时)
|
||||||
|
if (forceRelook && t) {
|
||||||
|
toast.success(t("relookupSuccess"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, data: result };
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(String(error));
|
||||||
|
return { success: false, error: String(error) };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,16 +24,12 @@ export async function executeDictionaryLookup(
|
|||||||
// 代码层面验证:输入是否有效
|
// 代码层面验证:输入是否有效
|
||||||
if (!analysis.isValid) {
|
if (!analysis.isValid) {
|
||||||
console.log("[阶段1] 输入无效:", analysis.reason);
|
console.log("[阶段1] 输入无效:", analysis.reason);
|
||||||
return {
|
throw analysis.reason || "无效输入";
|
||||||
error: analysis.reason || "无效输入",
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (analysis.isEmpty) {
|
if (analysis.isEmpty) {
|
||||||
console.log("[阶段1] 输入为空");
|
console.log("[阶段1] 输入为空");
|
||||||
return {
|
throw "输入为空";
|
||||||
error: "输入为空",
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("[阶段1] 输入分析完成:", analysis);
|
console.log("[阶段1] 输入分析完成:", analysis);
|
||||||
@@ -65,9 +61,7 @@ export async function executeDictionaryLookup(
|
|||||||
// 代码层面验证:标准形式不能为空
|
// 代码层面验证:标准形式不能为空
|
||||||
if (!standardFormResult.standardForm) {
|
if (!standardFormResult.standardForm) {
|
||||||
console.error("[阶段3] 标准形式为空");
|
console.error("[阶段3] 标准形式为空");
|
||||||
return {
|
throw "无法生成标准形式";
|
||||||
error: "无法生成标准形式",
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("[阶段3] 标准形式生成完成:", standardFormResult);
|
console.log("[阶段3] 标准形式生成完成:", standardFormResult);
|
||||||
@@ -99,8 +93,6 @@ export async function executeDictionaryLookup(
|
|||||||
|
|
||||||
// 任何阶段失败都返回错误(包含 reason)
|
// 任何阶段失败都返回错误(包含 reason)
|
||||||
const errorMessage = error instanceof Error ? error.message : "未知错误";
|
const errorMessage = error instanceof Error ? error.message : "未知错误";
|
||||||
return {
|
throw errorMessage;
|
||||||
error: errorMessage,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
|
|
||||||
import { executeDictionaryLookup } from "./dictionary";
|
import { executeDictionaryLookup } from "./dictionary";
|
||||||
import { createLookUp, createPhrase, createWord, createPhraseEntry, createWordEntry, selectLastLookUp } from "../services/dictionaryService";
|
import { createLookUp, createPhrase, createWord, createPhraseEntry, createWordEntry, selectLastLookUp } from "../services/dictionaryService";
|
||||||
import { DictLookUpRequest, DictWordResponse, isDictErrorResponse, isDictPhraseResponse, isDictWordResponse, type DictLookUpResponse } from "@/lib/shared";
|
import { DictLookUpRequest, DictWordResponse, isDictPhraseResponse, isDictWordResponse, type DictLookUpResponse } from "@/lib/shared";
|
||||||
|
import { lookUpValidation } from "@/lib/shared/validations/dictionaryValidations";
|
||||||
|
|
||||||
const saveResult = async (req: DictLookUpRequest, res: DictLookUpResponse) => {
|
const saveResult = async (req: DictLookUpRequest, res: DictLookUpResponse) => {
|
||||||
if (isDictErrorResponse(res)) return;
|
if (isDictPhraseResponse(res)) {
|
||||||
else if (isDictPhraseResponse(res)) {
|
|
||||||
// 先创建 Phrase
|
// 先创建 Phrase
|
||||||
const phrase = await createPhrase({
|
const phrase = await createPhrase({
|
||||||
standardForm: res.standardForm,
|
standardForm: res.standardForm,
|
||||||
@@ -73,14 +73,17 @@ const saveResult = async (req: DictLookUpRequest, res: DictLookUpResponse) => {
|
|||||||
* - 阶段5:错误处理
|
* - 阶段5:错误处理
|
||||||
* - 阶段6:最终输出封装
|
* - 阶段6:最终输出封装
|
||||||
*/
|
*/
|
||||||
export const lookUp = async ({
|
export const lookUp = async (req: DictLookUpRequest): Promise<DictLookUpResponse> => {
|
||||||
|
const {
|
||||||
text,
|
text,
|
||||||
queryLang,
|
queryLang,
|
||||||
|
forceRelook = false,
|
||||||
definitionLang,
|
definitionLang,
|
||||||
userId,
|
userId
|
||||||
forceRelook = false
|
} = req;
|
||||||
}: DictLookUpRequest): Promise<DictLookUpResponse> => {
|
|
||||||
try {
|
lookUpValidation(req);
|
||||||
|
|
||||||
const lastLookUp = await selectLastLookUp({
|
const lastLookUp = await selectLastLookUp({
|
||||||
text,
|
text,
|
||||||
queryLang,
|
queryLang,
|
||||||
@@ -131,11 +134,7 @@ export const lookUp = async ({
|
|||||||
entries: lastLookUp.dictionaryPhrase!.entries
|
entries: lastLookUp.dictionaryPhrase!.entries
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return { error: "Database structure error!" };
|
throw "错误D101";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
return { error: "LOOK_UP_ERROR" };
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ export type DictLookUpRequest = {
|
|||||||
queryLang: string,
|
queryLang: string,
|
||||||
definitionLang: string,
|
definitionLang: string,
|
||||||
userId?: string,
|
userId?: string,
|
||||||
forceRelook: boolean;
|
forceRelook?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DictWordEntry = {
|
export type DictWordEntry = {
|
||||||
@@ -18,10 +18,6 @@ export type DictPhraseEntry = {
|
|||||||
example: string;
|
example: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DictErrorResponse = {
|
|
||||||
error: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type DictWordResponse = {
|
export type DictWordResponse = {
|
||||||
standardForm: string;
|
standardForm: string;
|
||||||
entries: DictWordEntry[];
|
entries: DictWordEntry[];
|
||||||
@@ -33,22 +29,13 @@ export type DictPhraseResponse = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type DictLookUpResponse =
|
export type DictLookUpResponse =
|
||||||
| DictErrorResponse
|
|
||||||
| DictWordResponse
|
| DictWordResponse
|
||||||
| DictPhraseResponse;
|
| DictPhraseResponse;
|
||||||
|
|
||||||
// 类型守卫:判断是否为错误响应
|
|
||||||
export function isDictErrorResponse(
|
|
||||||
response: DictLookUpResponse
|
|
||||||
): response is DictErrorResponse {
|
|
||||||
return "error" in response;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 类型守卫:判断是否为单词响应
|
// 类型守卫:判断是否为单词响应
|
||||||
export function isDictWordResponse(
|
export function isDictWordResponse(
|
||||||
response: DictLookUpResponse
|
response: DictLookUpResponse
|
||||||
): response is DictWordResponse {
|
): response is DictWordResponse {
|
||||||
if (isDictErrorResponse(response)) return false;
|
|
||||||
const entries = (response as DictWordResponse | DictPhraseResponse).entries;
|
const entries = (response as DictWordResponse | DictPhraseResponse).entries;
|
||||||
return entries.length > 0 && "ipa" in entries[0] && "partOfSpeech" in entries[0];
|
return entries.length > 0 && "ipa" in entries[0] && "partOfSpeech" in entries[0];
|
||||||
}
|
}
|
||||||
@@ -57,7 +44,6 @@ export function isDictWordResponse(
|
|||||||
export function isDictPhraseResponse(
|
export function isDictPhraseResponse(
|
||||||
response: DictLookUpResponse
|
response: DictLookUpResponse
|
||||||
): response is DictPhraseResponse {
|
): response is DictPhraseResponse {
|
||||||
if (isDictErrorResponse(response)) return false;
|
|
||||||
const entries = (response as DictWordResponse | DictPhraseResponse).entries;
|
const entries = (response as DictWordResponse | DictPhraseResponse).entries;
|
||||||
return entries.length > 0 && !("ipa" in entries[0] || "partOfSpeech" in entries[0]);
|
return entries.length > 0 && !("ipa" in entries[0] || "partOfSpeech" in entries[0]);
|
||||||
}
|
}
|
||||||
|
|||||||
22
src/lib/shared/validations/dictionaryValidations.ts
Normal file
22
src/lib/shared/validations/dictionaryValidations.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
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.");
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user