Compare commits

..

3 Commits

Author SHA1 Message Date
f1d706e20c ... 2026-01-13 14:46:27 +08:00
c7cdf40f2f change varchar to text 2026-01-08 10:18:05 +08:00
a55e763525 解决dictionary搜索框溢出问题 2026-01-08 09:45:08 +08:00
10 changed files with 176 additions and 134 deletions

View File

@@ -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;

View File

@@ -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")

View File

@@ -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}

View File

@@ -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>

View File

@@ -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 = () => {

View 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) };
}
}

View File

@@ -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,
};
} }
} }

View File

@@ -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" };
}
}; };

View File

@@ -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]);
} }

View 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.");
};